Compare commits

..

No commits in common. "main" and "HEAD" have entirely different histories.
main ... HEAD

909 changed files with 55329 additions and 100325 deletions

View file

@ -29,10 +29,10 @@ ENV
*.swo *.swo
*~ *~
# Environment files (env-gateway-*.env will be copied as .env by workflow) # Environment files (env_gcp.env will be copied as .env by workflow)
env-*.env env_*.env
.env.local .env.local
# Note: .env is NOT ignored - it will be created from env-gateway-*.env by the workflow # Note: .env is NOT ignored - it will be created from env_gcp.env by the workflow
# Logs # Logs
*.log *.log

View file

@ -1,58 +0,0 @@
name: Deploy Plattform-Core (Int)
on:
push:
branches:
- int
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Tests auf Infomaniak VM
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
echo "StrictHostKeyChecking=no" >> ~/.ssh/config
echo "UserKnownHostsFile=/dev/null" >> ~/.ssh/config
ssh -i ~/.ssh/deploy_key ubuntu@api-int.poweron.swiss "
set -e
cd /srv/gateway/current
git remote set-url origin ssh://git@git.poweron.swiss:2222/PowerOn/platform-core.git
git fetch origin int
git reset --hard origin/int
test -f env-int.env
cp env-int.env .env
rm -f env-*.env
source .venv/bin/activate
pip install -r requirements.txt --no-cache-dir
python -m pytest tests/ --ignore=tests/demo
"
deploy:
runs-on: ubuntu-latest
needs: test
steps:
- name: Deploy to Infomaniak VM
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
echo "StrictHostKeyChecking=no" >> ~/.ssh/config
echo "UserKnownHostsFile=/dev/null" >> ~/.ssh/config
ssh -i ~/.ssh/deploy_key ubuntu@api-int.poweron.swiss "
set -e
cd /srv/gateway/current
git remote set-url origin ssh://git@git.poweron.swiss:2222/PowerOn/platform-core.git
git fetch origin int
git reset --hard origin/int
test -f env-int.env
cp env-int.env .env
rm -f env-*.env
source .venv/bin/activate
pip install -r requirements.txt --no-cache-dir
sudo systemctl restart gateway
"

View file

@ -1,58 +0,0 @@
name: Deploy Plattform-Core
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Tests auf Infomaniak VM
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
echo "StrictHostKeyChecking=no" >> ~/.ssh/config
echo "UserKnownHostsFile=/dev/null" >> ~/.ssh/config
ssh -i ~/.ssh/deploy_key ubuntu@api.poweron.swiss "
set -e
cd /srv/gateway/current
git remote set-url origin ssh://git@git.poweron.swiss:2222/PowerOn/platform-core.git
git fetch origin main
git reset --hard origin/main
test -f env-prod.env
cp env-prod.env .env
rm -f env-*.env
source .venv/bin/activate
pip install -r requirements.txt --no-cache-dir
python -m pytest tests/ --ignore=tests/demo
"
deploy:
runs-on: ubuntu-latest
needs: test
steps:
- name: Deploy to Infomaniak VM
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
echo "StrictHostKeyChecking=no" >> ~/.ssh/config
echo "UserKnownHostsFile=/dev/null" >> ~/.ssh/config
ssh -i ~/.ssh/deploy_key ubuntu@api.poweron.swiss "
set -e
cd /srv/gateway/current
git remote set-url origin ssh://git@git.poweron.swiss:2222/PowerOn/platform-core.git
git fetch origin main
git reset --hard origin/main
test -f env-prod.env
cp env-prod.env .env
rm -f env-*.env
source .venv/bin/activate
pip install -r requirements.txt --no-cache-dir
sudo systemctl restart gateway
"

View file

@ -30,7 +30,7 @@ ENV
*~ *~
# Environment files (will be handled separately) # Environment files (will be handled separately)
env-*.env env_*.env
.env.local .env.local
# Logs # Logs

151
.github/workflows/deploy-gcp.yml vendored Normal file
View file

@ -0,0 +1,151 @@
# GitHub Actions workflow for deploying Gateway to Google Cloud Run
# Documentation: https://cloud.google.com/run/docs/deploying
#
# Required GitHub Secrets:
# - GCP_PROJECT_ID: Your Google Cloud Project ID
# - GCP_SA_KEY: Service Account JSON key with Cloud Run Admin and Cloud Build Editor roles
# - GCP_SERVICE_ACCOUNT_EMAIL: Email of the service account to run Cloud Run service as
#
# Required Google Cloud Setup:
# 1. Create a service account with Cloud Run Admin and Cloud Build Editor roles
# 2. Create secret "CONFIG_KEY" in Secret Manager with your master key
# 3. Grant the service account access to Secret Manager secrets
# 4. Create Cloud SQL instance (if not exists)
# 5. Create env_prod.env and env_int.env files with your configuration
#
# Environment Selection:
# - Push to 'main' branch → uses env_prod.env (production)
# - Push to 'int' branch → uses env_int.env (integration)
# - Manual dispatch → select environment (prod/int) to use corresponding env file
name: Deploy Gateway to Google Cloud Run
on:
push:
branches:
- main
- int
paths:
- 'gateway/**'
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy to'
required: true
default: 'prod'
type: choice
options:
- prod
- int
# Cancel in-progress runs when a new run is triggered (saves logs/storage)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
REGION: europe-west6 # Zurich region
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for Workload Identity Federation
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine environment
id: env
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
ENV_TYPE="${{ github.event.inputs.environment }}"
elif [ "${{ github.ref }}" == "refs/heads/int" ]; then
ENV_TYPE="int"
else
ENV_TYPE="prod"
fi
echo "env_type=$ENV_TYPE" >> $GITHUB_OUTPUT
echo "service_name=gateway-$ENV_TYPE" >> $GITHUB_OUTPUT
echo "env_file=env_${ENV_TYPE}.env" >> $GITHUB_OUTPUT
echo "Determined environment: $ENV_TYPE"
echo "Service name: gateway-$ENV_TYPE"
echo "Env file: env_${ENV_TYPE}.env"
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
# Alternative: Use Workload Identity Federation (more secure)
# workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
# service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker for GCR
run: |
gcloud auth configure-docker
- name: Set environment file
run: |
cd gateway
ENV_FILE="${{ steps.env.outputs.env_file }}"
if [ -f "$ENV_FILE" ]; then
echo "Using $ENV_FILE"
cp "$ENV_FILE" .env
else
echo "Warning: $ENV_FILE not found, using env_prod.env as fallback"
cp env_prod.env .env
fi
# Clean up other env files (optional, for security)
rm -f env_*.env
- name: Build and push container image
working-directory: ./gateway
run: |
# Build container image using Cloud Build
# If Dockerfile exists, it will be used; otherwise Cloud Buildpacks will be used
SERVICE_NAME="${{ steps.env.outputs.service_name }}"
gcloud builds submit \
--tag gcr.io/${{ env.PROJECT_ID }}/$SERVICE_NAME:${{ github.sha }} \
--tag gcr.io/${{ env.PROJECT_ID }}/$SERVICE_NAME:latest \
--project ${{ env.PROJECT_ID }}
- name: Deploy to Cloud Run
run: |
SERVICE_NAME="${{ steps.env.outputs.service_name }}"
ENV_TYPE="${{ steps.env.outputs.env_type }}"
gcloud run deploy $SERVICE_NAME \
--image gcr.io/${{ env.PROJECT_ID }}/$SERVICE_NAME:${{ github.sha }} \
--region ${{ env.REGION }} \
--platform managed \
--allow-unauthenticated \
--project ${{ env.PROJECT_ID }} \
--set-env-vars "APP_ENV_TYPE=$ENV_TYPE" \
--set-secrets "CONFIG_KEY=CONFIG_KEY:latest" \
--memory 2Gi \
--cpu 2 \
--timeout 300 \
--max-instances 10 \
--min-instances 1 \
--port 8000 \
--service-account ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
- name: Get service URL
id: service-url
run: |
SERVICE_NAME="${{ steps.env.outputs.service_name }}"
SERVICE_URL=$(gcloud run services describe $SERVICE_NAME \
--region ${{ env.REGION }} \
--project ${{ env.PROJECT_ID }} \
--format 'value(status.url)')
echo "url=$SERVICE_URL" >> $GITHUB_OUTPUT
- name: Output deployment URL
run: |
echo "🚀 Deployment successful!"
echo "Service URL: ${{ steps.service-url.outputs.url }}"

88
.github/workflows/int_gateway-int.yml vendored Normal file
View file

@ -0,0 +1,88 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions
name: Build and deploy Python app to Azure Web App - gateway-int
on:
push:
branches:
- int
workflow_dispatch:
# Cancel in-progress runs when a new run is triggered (saves logs/storage)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read #This is required for actions/checkout
steps:
- uses: actions/checkout@v4
- name: Set up Python version
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Create and start virtual environment
run: |
python -m venv venv
source venv/bin/activate
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.lock ]; then
pip install -r requirements.lock --no-cache-dir
else
pip install -r requirements.txt --no-cache-dir
fi
# Optional: Add step to run tests here (PyTest, Django test suites, etc.)
- name: Zip artifact for deployment
run: zip release.zip ./* -r
- name: Upload artifact for deployment jobs
uses: actions/upload-artifact@v4
with:
name: python-app
path: |
release.zip
!venv/
retention-days: 5
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: python-app
- name: Unzip artifact for deployment
run: unzip release.zip
- name: Set productive environment
run: cp env_int.env .env
- name: Clean up environment files
run: rm -f env_*.env
- name: 'Deploy to Azure Web App'
uses: azure/webapps-deploy@v3
id: deploy-to-webapp
with:
app-name: 'gateway-int'
slot-name: 'Production'
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_GATEWAY_INT }}

88
.github/workflows/main_gateway-prod.yml vendored Normal file
View file

@ -0,0 +1,88 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions
name: Build and deploy Python app to Azure Web App - gateway-prod
on:
push:
branches:
- main
workflow_dispatch:
# Cancel in-progress runs when a new run is triggered (saves logs/storage)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read #This is required for actions/checkout
steps:
- uses: actions/checkout@v4
- name: Set up Python version
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Create and start virtual environment
run: |
python -m venv venv
source venv/bin/activate
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.lock ]; then
pip install -r requirements.lock --no-cache-dir
else
pip install -r requirements.txt --no-cache-dir
fi
# Optional: Add step to run tests here (PyTest, Django test suites, etc.)
- name: Zip artifact for deployment
run: zip release.zip ./* -r
- name: Upload artifact for deployment jobs
uses: actions/upload-artifact@v4
with:
name: python-app
path: |
release.zip
!venv/
retention-days: 5
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: python-app
- name: Unzip artifact for deployment
run: unzip release.zip
- name: Set productive environment
run: cp env_prod.env .env
- name: Clean up environment files
run: rm -f env_*.env
- name: 'Deploy to Azure Web App'
uses: azure/webapps-deploy@v3
id: deploy-to-webapp
with:
app-name: 'gateway-prod'
slot-name: 'Production'
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_GATEWAY_PROD }}

View file

@ -0,0 +1,51 @@
# Generates requirements.lock from requirements.txt using Python 3.11 (same as build).
# Run manually (workflow_dispatch) or on changes to requirements.txt.
# After running, commit the generated requirements.lock so builds use it for fast installs.
name: Update requirements.lock
on:
workflow_dispatch:
push:
branches:
- main
- int
paths:
- 'requirements.txt'
# Cancel in-progress runs when a new run is triggered (saves logs/storage)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
update-lock:
runs-on: ubuntu-latest
permissions:
contents: write # push requirements.lock
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install pip-tools
run: python -m pip install --upgrade "pip>=24,<26" pip-tools
- name: Generate requirements.lock
run: pip-compile requirements.txt -o requirements.lock
- name: Commit and push requirements.lock
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add requirements.lock
if git diff --staged --quiet; then
echo "No changes to requirements.lock"
else
git commit -m "chore: update requirements.lock"
git push
fi

2
.gitignore vendored
View file

@ -131,7 +131,7 @@ env.bak/
venv.bak/ venv.bak/
# Don't ignore environment templates # Don't ignore environment templates
!env-*.env !env*.env
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject

View file

@ -28,13 +28,13 @@ COPY requirements.lock .
RUN pip install --no-cache-dir --upgrade pip && \ RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.lock pip install --no-cache-dir -r requirements.lock
# Copy application code (includes .env file created by workflow from env-gateway-*.env) # Copy application code (includes .env file created by workflow from env_gcp.env)
COPY . . COPY . .
# Create directories for logs (Cloud Run uses /tmp for writable storage) # Create directories for logs (Cloud Run uses /tmp for writable storage)
RUN mkdir -p /tmp/logs /tmp/debug RUN mkdir -p /tmp/logs /tmp/debug
# Note: .env file (created from env-gateway-*.env by workflow) contains encrypted secrets # Note: .env file (created from env_gcp.env by workflow) contains encrypted secrets
# These are decrypted at runtime using the master key from Secret Manager # These are decrypted at runtime using the master key from Secret Manager
# (mounted as CONFIG_KEY environment variable in Cloud Run) # (mounted as CONFIG_KEY environment variable in Cloud Run)
@ -46,4 +46,5 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/api/admin/health', timeout=5)" || exit 1 CMD python -c "import requests; requests.get('http://localhost:8000/api/admin/health', timeout=5)" || exit 1
# Run the application # Run the application
CMD exec gunicorn app:app --bind 0.0.0.0:${PORT:-8000} --timeout 600 --worker-class uvicorn.workers.UvicornWorker --workers 1 # Cloud Run will set PORT env var, uvicorn reads it automatically
CMD exec uvicorn app:app --host 0.0.0.0 --port ${PORT:-8000} --workers 1

343
app.py
View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
import os import os
import sys import sys
@ -61,13 +61,6 @@ class DailyRotatingFileHandler(RotatingFileHandler):
return True return True
return False return False
def doRollover(self):
"""Size-based rollover that tolerates Windows file locks."""
try:
super().doRollover()
except PermissionError:
pass
def emit(self, record): def emit(self, record):
"""Emit a log record, switching files if date has changed""" """Emit a log record, switching files if date has changed"""
# Check if we need to switch to a new file # Check if we need to switch to a new file
@ -289,7 +282,7 @@ initLogging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL") instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
# Pre-warm AI connectors on process load (before lifespan). Critical for AI/agent latency. # Pre-warm AI connectors on process load (before lifespan). Critical for chatbot latency.
try: try:
import modules.aicore.aicoreModelRegistry # noqa: F401 import modules.aicore.aicoreModelRegistry # noqa: F401
logger.info("AI connectors pre-warm (app load) triggered") logger.info("AI connectors pre-warm (app load) triggered")
@ -301,41 +294,8 @@ except Exception as e:
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
logger.info("Application is starting up") logger.info("Application is starting up")
# Validate FK metadata on all Pydantic models (fail-fast, no silent fallbacks)
from modules.dbHelpers.fkRegistry import validateFkTargets
fkErrors = validateFkTargets()
if fkErrors:
for err in fkErrors:
logger.error("FK metadata validation: %s", err)
raise SystemExit(f"FK metadata validation failed ({len(fkErrors)} error(s)) — fix datamodels before starting")
# AI connectors already pre-warmed at module-load via _eager_prewarm() in aicoreModelRegistry. # AI connectors already pre-warmed at module-load via _eager_prewarm() in aicoreModelRegistry.
# Register system-component lifecycle hooks (Composition Root — inverts L4->L5b dependency)
from modules.shared.systemComponentRegistry import registerLifecycleHook
from modules.workflowAutomation.mainWorkflowAutomation import (
onBootstrap as _waOnBootstrap,
onMandateDelete as _waOnMandateDelete,
onInstanceCreate as _waOnInstanceCreate,
)
from modules.interfaces.interfaceDbBilling import (
onMandateDelete as _billingOnMandateDelete,
onMandateProvision as _billingOnMandateProvision,
onStorageChanged as _billingOnStorageChanged,
onUserMandateCreate as _billingOnUserMandateCreate,
onUserMandateDelete as _billingOnUserMandateDelete,
onUserBudgetAdjust as _billingOnUserBudgetAdjust,
)
registerLifecycleHook("onBootstrap", _waOnBootstrap)
registerLifecycleHook("onMandateDelete", _waOnMandateDelete)
registerLifecycleHook("onMandateDelete", _billingOnMandateDelete)
registerLifecycleHook("onMandateProvision", _billingOnMandateProvision)
registerLifecycleHook("onStorageChanged", _billingOnStorageChanged)
registerLifecycleHook("onInstanceCreate", _waOnInstanceCreate)
registerLifecycleHook("onUserMandateCreate", _billingOnUserMandateCreate)
registerLifecycleHook("onUserMandateDelete", _billingOnUserMandateDelete)
registerLifecycleHook("onUserBudgetAdjust", _billingOnUserBudgetAdjust)
# Bootstrap database if needed (creates initial users, mandates, roles, etc.) # Bootstrap database if needed (creates initial users, mandates, roles, etc.)
# This must happen before getting root interface # This must happen before getting root interface
from modules.security.rootAccess import getRootDbAppConnector from modules.security.rootAccess import getRootDbAppConnector
@ -350,49 +310,18 @@ async def lifespan(app: FastAPI):
# Register all feature definitions in RBAC catalog (for /api/features/ endpoint) # Register all feature definitions in RBAC catalog (for /api/features/ endpoint)
try: try:
from modules.security.rbacCatalog import getCatalogService from modules.security.rbacCatalog import getCatalogService
from modules.system.registry import registerAllFeaturesInCatalog, syncCatalogFeaturesToDb from modules.system.registry import registerAllFeaturesInCatalog
catalogService = getCatalogService() catalogService = getCatalogService()
registerAllFeaturesInCatalog(catalogService) registerAllFeaturesInCatalog(catalogService)
logger.info("Feature catalog registration completed") logger.info("Feature catalog registration completed")
# Register service center RBAC objects (Composition Root — avoids system→serviceCenter import)
try:
from modules.serviceCenter import registerServiceObjects
registerServiceObjects(catalogService)
except Exception as e:
logger.warning(f"Service center RBAC registration failed: {e}")
# Persist the in-memory feature registry into the Feature DB-table so
# the FeatureInstance.featureCode FK has real targets. Without this
# every FeatureInstance row would be flagged as orphan by the
# SysAdmin DB-health scan (cf. interfaceFeatures.upsertFeature).
try:
syncCatalogFeaturesToDb(catalogService)
except Exception as e:
logger.error(f"Feature DB sync failed: {e}")
except Exception as e: except Exception as e:
logger.error(f"Feature catalog registration failed: {e}") logger.error(f"Feature catalog registration failed: {e}")
# Sync gateway i18n registry to DB and load translation cache # Sync gateway i18n registry to DB and load translation cache
try: try:
from modules.system.i18nBootSync import syncRegistryToDb, loadCache from modules.shared.i18nRegistry import _syncRegistryToDb, _loadCache
from modules.serviceCenter.registry import IMPORTABLE_SERVICES await _syncRegistryToDb()
serviceLabels = [svc.get("label") for svc in IMPORTABLE_SERVICES.values()] await _loadCache()
accountingLabels = []
try:
from modules.features.trustee.accounting.accountingRegistry import getAccountingRegistry
registry = getAccountingRegistry()
for connectorType, connector in (registry._connectors or {}).items():
for field in connector.getRequiredConfigFields():
label = getattr(field, "label", "") or ""
if label:
accountingLabels.append({"label": label, "connectorType": connectorType})
except Exception:
pass
await syncRegistryToDb(serviceLabels=serviceLabels, accountingLabels=accountingLabels)
await loadCache()
logger.info("i18n registry sync + cache load completed") logger.info("i18n registry sync + cache load completed")
except Exception as e: except Exception as e:
logger.warning(f"i18n registry sync failed (non-critical): {e}") logger.warning(f"i18n registry sync failed (non-critical): {e}")
@ -424,74 +353,14 @@ async def lifespan(app: FastAPI):
except Exception as e: except Exception as e:
logger.warning(f"Could not initialize feature containers: {e}") logger.warning(f"Could not initialize feature containers: {e}")
# Bootstrap Stripe prices for paid plans (composition root — upward import allowed here)
try:
from modules.serviceCenter.services.serviceSubscription.stripeBootstrap import bootstrapStripePrices
bootstrapStripePrices()
except Exception as e:
logger.error(f"Stripe price bootstrap failed: {e}")
# Bootstrap MIME map into ComponentObjects (composition root — upward import allowed here)
try:
from modules.serviceCenter.services.serviceExtraction.subRegistry import ExtractorRegistry
from modules.interfaces.interfaceDbManagement import ComponentObjects
_mimeRegistry = ExtractorRegistry()
_extensionToMime = _mimeRegistry.getExtensionToMimeMap()
_textMimes: set = set()
_seen: set = set()
for _ext in _mimeRegistry._map.values():
_eid = id(_ext)
if _eid in _seen:
continue
_seen.add(_eid)
_mimes = _ext.getSupportedMimeTypes()
if any(m.startswith("text/") for m in _mimes):
_textMimes.update(_mimes)
_textMimes.update({"application/json", "application/xml", "application/javascript", "application/sql", "application/x-yaml", "application/x-toml"})
ComponentObjects.setMimeMap(_extensionToMime, _textMimes)
except Exception as e:
logger.warning(f"MIME map bootstrap failed: {e}")
# --- Init Managers --- # --- Init Managers ---
import asyncio import asyncio
try: try:
main_loop = asyncio.get_running_loop() main_loop = asyncio.get_running_loop()
eventManager.set_event_loop(main_loop) eventManager.set_event_loop(main_loop)
from modules.workflowAutomation.scheduler.mainScheduler import setMainLoop as setSchedulerMainLoop, setOnRunFailedCallback from modules.workflows.scheduler.mainScheduler import setMainLoop as setSchedulerMainLoop
setSchedulerMainLoop(main_loop) setSchedulerMainLoop(main_loop)
# Inject run-failed notification callback (Composition Root — avoids workflows→serviceCenter import)
def _onRunFailed(workflowId, runId, error, mandateId=None, workflowLabel=None):
from modules.serviceCenter import getService
from modules.serviceCenter.context import ServiceCenterContext
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelMessaging import MessagingEventParameters
rootInterface = getRootInterface()
if not rootInterface:
return
eventUser = rootInterface.getUserByUsername("event")
if not eventUser:
return
ctx = ServiceCenterContext(
user=eventUser,
mandate_id=mandateId or "",
feature_instance_id="",
feature_code="workflowAutomation",
)
messagingService = getService("messaging", ctx)
subscriptionId = "WorkflowAutomationRunFailed"
eventParams = MessagingEventParameters(triggerData={
"workflowId": workflowId,
"workflowLabel": workflowLabel or workflowId,
"runId": runId,
"error": error,
"mandateId": mandateId or "",
})
messagingService.executeSubscription(subscriptionId, eventParams)
setOnRunFailedCallback(_onRunFailed)
# Suppress noisy ConnectionResetError from ProactorEventLoop on Windows # Suppress noisy ConnectionResetError from ProactorEventLoop on Windows
# when clients (browsers) close connections abruptly. This is a known # when clients (browsers) close connections abruptly. This is a known
# asyncio issue on Windows: https://bugs.python.org/issue39010 # asyncio issue on Windows: https://bugs.python.org/issue39010
@ -501,138 +370,35 @@ async def lifespan(app: FastAPI):
return return
if isinstance(exc, ConnectionAbortedError): if isinstance(exc, ConnectionAbortedError):
return return
if exc and "LocalProtocolError" in type(exc).__name__:
return
loop.default_exception_handler(ctx) loop.default_exception_handler(ctx)
main_loop.set_exception_handler(_suppressClientDisconnect) main_loop.set_exception_handler(_suppressClientDisconnect)
except RuntimeError: except RuntimeError:
pass pass
eventManager.start() eventManager.start()
# --- WorkflowAutomation: Scheduler boot (System-Lifespan, not Feature-onStart) ---
try:
from modules.workflowAutomation.scheduler.mainScheduler import start as _startWorkflowScheduler
_startWorkflowScheduler(eventUser)
logger.info("WorkflowAutomation scheduler started (system lifespan)")
except Exception as e:
logger.error(f"WorkflowAutomation scheduler failed to start: {e}")
# Register audit log cleanup scheduler # Register audit log cleanup scheduler
from modules.dbHelpers.auditLogger import registerAuditLogCleanupScheduler from modules.shared.auditLogger import registerAuditLogCleanupScheduler
registerAuditLogCleanupScheduler() registerAuditLogCleanupScheduler()
# Register enterprise subscription auto-renewal scheduler
from modules.serviceCenter.services.serviceSubscription.enterpriseRenewalScheduler import registerEnterpriseRenewalScheduler
registerEnterpriseRenewalScheduler()
# Recover background jobs that were RUNNING when the previous worker died
try:
from modules.serviceCenter.services.serviceBackgroundJobs.mainBackgroundJobService import (
recoverInterruptedJobs,
registerZombieKillerScheduler,
)
recoverInterruptedJobs()
registerZombieKillerScheduler(intervalMinutes=5)
except Exception as e:
logger.warning(f"BackgroundJob recovery failed (non-critical): {e}")
# Subscribe knowledge ingestion to connection lifecycle events so OAuth
# connect/disconnect reliably trigger bootstrap/purge.
try:
from modules.serviceCenter.services.serviceKnowledge.subConnectorIngestConsumer import (
registerKnowledgeIngestionConsumer,
)
registerKnowledgeIngestionConsumer()
# Side-effect import: registers all walker progress message keys
# in the i18n registry so `syncRegistryToDb` picks them up.
from modules.serviceCenter.services.serviceKnowledge import _progressMessages # noqa: F401
except Exception as e:
logger.warning(f"KnowledgeIngestionConsumer registration failed (non-critical): {e}")
# Install force-exit handler AFTER uvicorn has registered its own SIGINT
# handler. Uvicorn's default timeout-graceful-shutdown is None (wait
# forever), so frontend polling keep-alive connections block the process.
# This wraps uvicorn's handler: on Ctrl+C, start a 3s timer that calls
# os._exit() if the graceful shutdown hasn't completed by then.
import signal as _sig
import threading as _thr
_prevSigint = _sig.getsignal(_sig.SIGINT)
def _onSigint(signum, frame):
_t = _thr.Timer(3.0, lambda: os._exit(0))
_t.daemon = True
_t.start()
if callable(_prevSigint) and _prevSigint not in (_sig.SIG_DFL, _sig.SIG_IGN):
_prevSigint(signum, frame)
else:
raise KeyboardInterrupt
_sig.signal(_sig.SIGINT, _onSigint)
yield yield
# --- Shutdown sequence (protected against CancelledError) --- # --- Stop Managers ---
eventManager.stop()
# --- Stop Feature Containers (Plug&Play) ---
try: try:
# 1. Drain SSE queues and cancel agent tasks FIRST so that open mainModules = loadFeatureMainModules()
# streaming connections break out of their queue.get() loop for featureName, module in mainModules.items():
# immediately. Without this, uvicorn waits for the SSE generators if hasattr(module, "onStop"):
# to finish (up to 120 s keepalive timeout) before the rest of try:
# the shutdown can proceed. await module.onStop(eventUser)
try: logger.info(f"Feature '{featureName}' stopped")
from modules.shared.eventManager import get_event_manager as _getStreamingEM except Exception as e:
_getStreamingEM().shutdown() logger.error(f"Feature '{featureName}' failed to stop: {e}")
except Exception as e: except Exception as e:
logger.warning(f"Streaming EventManager shutdown failed: {e}") logger.warning(f"Could not shutdown feature containers: {e}")
# 2. Signal DB layer to abort in-flight borrow waits immediately. logger.info("Application has been shut down")
# This MUST happen early so that sync worker threads stuck in
# _acquireConn (30 s poll loop) bail out within one backoff tick
# instead of blocking process exit for the full borrow timeout.
try:
from modules.connectors.connectorDbPostgre import closeAllPools
closeAllPools()
except Exception as e:
logger.warning(f"Closing DB connection pools failed: {e}")
# 3. Stop scheduler (removes all pending cron/interval jobs)
eventManager.stop()
# 3.5 Stop WorkflowAutomation scheduler + email poller (System-Lifespan)
try:
from modules.workflowAutomation.scheduler.mainScheduler import stop as _stopWorkflowScheduler
_stopWorkflowScheduler()
except Exception as e:
logger.warning(f"WorkflowAutomation scheduler stop failed: {e}")
try:
from modules.workflowAutomation.scheduler.emailPoller import stop as _stopEmailPoller
_stopEmailPoller(eventUser)
except Exception as e:
logger.warning(f"Email poller stop failed: {e}")
# 4. Stop Feature Containers (Plug&Play)
try:
mainModules = loadFeatureMainModules()
for featureName, module in mainModules.items():
if hasattr(module, "onStop"):
try:
await module.onStop(eventUser)
logger.info(f"Feature '{featureName}' stopped")
except Exception as e:
logger.error(f"Feature '{featureName}' failed to stop: {e}")
except Exception as e:
logger.warning(f"Could not shutdown feature containers: {e}")
# 5. Close shared HTTP sessions (ResilientHttp) to avoid TCP keepalive hang
try:
from modules.shared.httpResilience import closeAllResilientHttp
await closeAllResilientHttp()
except Exception as e:
logger.warning(f"Closing HTTP sessions failed: {e}")
logger.info("Application has been shut down")
except asyncio.CancelledError:
logger.info("Shutdown interrupted (CancelledError) -- resources released")
# Custom function to generate readable operation IDs for Swagger UI # Custom function to generate readable operation IDs for Swagger UI
@ -705,8 +471,8 @@ def getAllowedOrigins():
# CORS origin regex pattern for wildcard subdomain support # CORS origin regex pattern for wildcard subdomain support
# Matches all subdomains of poweron.swiss # Matches all subdomains of poweron.swiss and poweron-center.net
CORS_ORIGIN_REGEX = r"https://.*\.poweron\.swiss" CORS_ORIGIN_REGEX = r"https://.*\.(poweron\.swiss|poweron-center\.net)"
# SlowAPI rate limiter initialization # SlowAPI rate limiter initialization
@ -736,18 +502,14 @@ from modules.auth import (
ProactiveTokenRefreshMiddleware, ProactiveTokenRefreshMiddleware,
) )
# Per-request context middleware: language (Accept-Language) + user timezone (X-User-Timezone). # i18n language detection middleware (sets per-request language from Accept-Language header)
# Both are written into ContextVars and consumed by t() / resolveText() and getRequestNow() from modules.shared.i18nRegistry import _setLanguage, normalizePrimaryLanguageTag
# without having to thread them through every call site.
from modules.shared.i18nRegistry import setLanguage, normalizePrimaryLanguageTag
from modules.shared.timeUtils import setRequestTimezone
@app.middleware("http") @app.middleware("http")
async def _requestContextMiddleware(request: Request, call_next): async def _i18nMiddleware(request: Request, call_next):
acceptLang = request.headers.get("Accept-Language", "") acceptLang = request.headers.get("Accept-Language", "")
lang = normalizePrimaryLanguageTag(acceptLang, "de") lang = normalizePrimaryLanguageTag(acceptLang, "de")
setLanguage(lang) _setLanguage(lang)
setRequestTimezone(request.headers.get("X-User-Timezone", ""))
return await call_next(request) return await call_next(request)
app.add_middleware(CSRFMiddleware) app.add_middleware(CSRFMiddleware)
@ -793,27 +555,15 @@ app.include_router(fileRouter)
from modules.routes.routeDataSources import router as dataSourceRouter from modules.routes.routeDataSources import router as dataSourceRouter
app.include_router(dataSourceRouter) app.include_router(dataSourceRouter)
from modules.routes.routeUdb import router as udbRouter
app.include_router(udbRouter)
from modules.routes.routeDataPrompts import router as promptRouter from modules.routes.routeDataPrompts import router as promptRouter
app.include_router(promptRouter) app.include_router(promptRouter)
from modules.routes.routeDataConnections import router as connectionsRouter from modules.routes.routeDataConnections import router as connectionsRouter
app.include_router(connectionsRouter) app.include_router(connectionsRouter)
from modules.routes.routeRagInventory import router as ragInventoryRouter
app.include_router(ragInventoryRouter)
from modules.routes.routeTableViews import router as tableViewsRouter
app.include_router(tableViewsRouter)
from modules.routes.routeSecurityLocal import router as localRouter from modules.routes.routeSecurityLocal import router as localRouter
app.include_router(localRouter) app.include_router(localRouter)
from modules.routes.routeMfa import router as mfaRouter
app.include_router(mfaRouter)
from modules.routes.routeSecurityMsft import router as msftRouter from modules.routes.routeSecurityMsft import router as msftRouter
app.include_router(msftRouter) app.include_router(msftRouter)
@ -823,9 +573,6 @@ app.include_router(googleRouter)
from modules.routes.routeSecurityClickup import router as clickupRouter from modules.routes.routeSecurityClickup import router as clickupRouter
app.include_router(clickupRouter) app.include_router(clickupRouter)
from modules.routes.routeSecurityInfomaniak import router as infomaniakRouter
app.include_router(infomaniakRouter)
from modules.routes.routeClickup import router as clickupApiRouter from modules.routes.routeClickup import router as clickupApiRouter
app.include_router(clickupApiRouter) app.include_router(clickupApiRouter)
@ -880,9 +627,6 @@ app.include_router(billingRouter)
from modules.routes.routeSubscription import router as subscriptionRouter from modules.routes.routeSubscription import router as subscriptionRouter
app.include_router(subscriptionRouter) app.include_router(subscriptionRouter)
from modules.routes.routeJobs import router as jobsRouter
app.include_router(jobsRouter)
# ============================================================================ # ============================================================================
# SYSTEM ROUTES (Navigation, etc.) # SYSTEM ROUTES (Navigation, etc.)
# ============================================================================ # ============================================================================
@ -890,8 +634,8 @@ from modules.routes.routeSystem import router as systemRouter, navigationRouter
app.include_router(systemRouter) app.include_router(systemRouter)
app.include_router(navigationRouter) app.include_router(navigationRouter)
from modules.routes.routeWorkflowAutomation import router as workflowAutomationRouter from modules.routes.routeWorkflowDashboard import router as workflowDashboardRouter
app.include_router(workflowAutomationRouter) app.include_router(workflowDashboardRouter)
# ============================================================================ # ============================================================================
# PLUG&PLAY FEATURE ROUTERS # PLUG&PLAY FEATURE ROUTERS
@ -900,23 +644,4 @@ app.include_router(workflowAutomationRouter)
from modules.system.registry import loadFeatureRouters from modules.system.registry import loadFeatureRouters
featureLoadResults = loadFeatureRouters(app) featureLoadResults = loadFeatureRouters(app)
logger.info(f"Feature router load results: {featureLoadResults}") logger.info(f"Feature router load results: {featureLoadResults}")
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8000))
try:
import gunicorn.app.wsgiapp # type: ignore[import-untyped] # noqa: F401
import subprocess
import sys
subprocess.run([
sys.executable, "-m", "gunicorn", "app:app",
"--bind", f"0.0.0.0:{port}",
"--timeout", "600",
"--worker-class", "uvicorn.workers.UvicornWorker",
"--workers", "1",
], check=True)
except ImportError:
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=port, workers=1, timeout_graceful_shutdown=2)

Binary file not shown.

View file

@ -45,6 +45,11 @@ Connector_StacSwisstopo_MAX_RETRIES = 3
Connector_StacSwisstopo_RETRY_DELAY = 1.0 Connector_StacSwisstopo_RETRY_DELAY = 1.0
Connector_StacSwisstopo_ENABLE_CACHE = True Connector_StacSwisstopo_ENABLE_CACHE = True
# Demo RMA credentials (same for all demo trustee instances)
Demo_RMA_ApiBaseUrl = https://service.int.runmyaccounts.com/api/latest/clients/
Demo_RMA_ClientName = poweronag
Demo_RMA_ApiKey = pat_tipTbnHU26CrMzAnLSjCR_uzHJv4CDNa7obaQGHIA-4
# Operator company information (shown on invoice emails) # Operator company information (shown on invoice emails)
Operator_CompanyName = PowerOn AG Operator_CompanyName = PowerOn AG
Operator_Address = Birmensdorferstrasse 94, 8003 Zürich Operator_Address = Birmensdorferstrasse 94, 8003 Zürich

View file

@ -1,5 +1,3 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""Generate tenant-dossier.pdf for neutralization demo. Run: python _generateTenantDossierPdf.py """Generate tenant-dossier.pdf for neutralization demo. Run: python _generateTenantDossierPdf.py
Uses ReportLab so the PDF opens reliably in all viewers (stdlib-only PDFs are fragile). Uses ReportLab so the PDF opens reliably in all viewers (stdlib-only PDFs are fragile).

View file

@ -1,127 +0,0 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""Generate the 3 fictitious PWG scan PDFs used by the pilot demo.
Run: python _generateScans.py
Produces:
scans/mieter01-bestaetigt.pdf -> all fields ok, signed
scans/mieter02-abweichung-betrag.pdf -> rent on scan != journal lines
scans/mieter03-keine-unterschrift.pdf -> hasSignature=false
"""
from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
def _renderForm(outPath: Path, *, tenantName: str, tenantAddress: str,
objectAddress: str, period: str, rentChf: float,
tenantNotes: str, hasSignature: bool) -> None:
c = canvas.Canvas(str(outPath), pagesize=A4)
w, h = A4
margin = 60
y = h - margin
c.setFont("Helvetica-Bold", 16)
c.drawString(margin, y, "Stiftung PWG")
y -= 18
c.setFont("Helvetica", 10)
c.drawString(margin, y, "Postfach 1234 · 8000 Zürich")
y -= 30
c.setFont("Helvetica-Bold", 14)
c.drawString(margin, y, f"Jahresmietzinsbestätigung {period}")
y -= 28
c.setFont("Helvetica", 11)
c.drawString(margin, y, "Sehr geehrte Damen und Herren,")
y -= 18
c.drawString(margin, y, "hiermit bestätige ich die nachstehenden Angaben für die o.g. Periode:")
y -= 28
rows = [
("Mieter / in:", tenantName),
("Wohnadresse:", tenantAddress),
("Mietobjekt:", objectAddress),
("Periode:", period),
("Bestätigter Mietzins (CHF, monatlich):", f"{rentChf:.2f}"),
("Anmerkungen:", tenantNotes or "(keine)"),
]
c.setFont("Helvetica", 11)
for lab, val in rows:
c.drawString(margin, y, lab)
c.drawString(margin + 220, y, str(val))
y -= 18
y -= 28
c.drawString(margin, y, "Ort, Datum: Zürich, 12.04.2026")
y -= 28
c.drawString(margin, y, "Unterschrift Mieter / in:")
y -= 36
if hasSignature:
c.setFont("Helvetica-Oblique", 14)
c.drawString(margin + 220, y + 24, _signatureFor(tenantName))
else:
c.setFont("Helvetica", 9)
c.drawString(margin + 220, y + 24, "(handschriftlich)")
c.line(margin + 215, y + 22, margin + 415, y + 22)
c.showPage()
c.save()
def _signatureFor(name: str) -> str:
parts = name.split()
if not parts:
return "____"
return parts[0][0] + ". " + parts[-1]
def _main() -> None:
here = Path(__file__).resolve().parent
outDir = here / "scans"
outDir.mkdir(parents=True, exist_ok=True)
# 1) bestätigt — exakt passend zu seed (Anna Müller, 1850.00)
_renderForm(
outDir / "mieter01-bestaetigt.pdf",
tenantName="Anna Müller",
tenantAddress="Bahnhofstrasse 12, 8001 Zürich",
objectAddress="Bahnhofstrasse 12, 3.OG, 8001 Zürich",
period="2026",
rentChf=1850.00,
tenantNotes="",
hasSignature=True,
)
# 2) abweichung_betrag — Mieter trägt 2300 ein, Buchhaltung sagt 2200
_renderForm(
outDir / "mieter02-abweichung-betrag.pdf",
tenantName="Beat Schneider",
tenantAddress="Limmatquai 45, 8001 Zürich",
objectAddress="Limmatquai 45, 1.OG, 8001 Zürich",
period="2026",
rentChf=2300.00,
tenantNotes="Mietzins gemäss letzter Indexanpassung — bitte prüfen.",
hasSignature=True,
)
# 3) keine_unterschrift — Carla Weber, 1650 stimmt, aber nicht unterschrieben
_renderForm(
outDir / "mieter03-keine-unterschrift.pdf",
tenantName="Carla Weber",
tenantAddress="Seestrasse 88, 8002 Zürich",
objectAddress="Seestrasse 88, EG, 8002 Zürich",
period="2026",
rentChf=1650.00,
tenantNotes="",
hasSignature=False,
)
print(f"Generated 3 scans in {outDir}")
if __name__ == "__main__":
_main()

View file

@ -1,68 +0,0 @@
{
"_comment": "PWG-Demo Seed-Daten — fiktive Mieter (Debitoren) und Mietzins-Buchungen 2026 für Trustee-Feature. Wird von pwgDemo2026.py idempotent geladen.",
"rentAccount": "6000",
"rentAccountLabel": "Mietzinsertrag Wohnen",
"year": 2026,
"tenants": [
{
"contactNumber": "10001",
"name": "Anna Müller",
"address": "Bahnhofstrasse 12",
"zip": "8001",
"city": "Zürich",
"country": "CH",
"email": "anna.mueller@example.ch",
"monthlyRentChf": 1850.00,
"scenario": "bestaetigt",
"_note": "Stimmt exakt — erwarteter Pilot-Status 'bestaetigt'"
},
{
"contactNumber": "10002",
"name": "Beat Schneider",
"address": "Limmatquai 45",
"zip": "8001",
"city": "Zürich",
"country": "CH",
"email": "beat.schneider@example.ch",
"monthlyRentChf": 2200.00,
"scenario": "abweichung_betrag",
"_note": "Scan zeigt 2300 CHF/Monat (Mieter nicht über Erhöhung informiert) — erwarteter Status 'abweichung_betrag'"
},
{
"contactNumber": "10003",
"name": "Carla Weber",
"address": "Seestrasse 88",
"zip": "8002",
"city": "Zürich",
"country": "CH",
"email": "carla.weber@example.ch",
"monthlyRentChf": 1650.00,
"scenario": "keine_unterschrift",
"_note": "Scan ist ohne Unterschrift — erwarteter Status 'keine_unterschrift'"
},
{
"contactNumber": "10004",
"name": "Daniel Keller",
"address": "Hardturmstrasse 200",
"zip": "8005",
"city": "Zürich",
"country": "CH",
"email": "daniel.keller@example.ch",
"monthlyRentChf": 2450.00,
"scenario": "kein_scan",
"_note": "Hat noch nicht zurückgesendet — taucht nicht im Pilot-Run auf"
},
{
"contactNumber": "10005",
"name": "Elena Fischer",
"address": "Rämistrasse 71",
"zip": "8001",
"city": "Zürich",
"country": "CH",
"email": "elena.fischer@example.ch",
"monthlyRentChf": 1990.00,
"scenario": "kein_scan",
"_note": "Reserve-Mieter für spätere Demo-Erweiterungen"
}
]
}

View file

@ -1,80 +0,0 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20260420002638-01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20260420002638-01'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 1 /Kids [ 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 605
>>
stream
Gat$u_2b!='YNm9]OOh_`s.;Y\Ku+!X/aQ:.b.-A/gNQpRp[N%>l++NBXO3A:fg1WZM\=sbo<,Q[3'29Es](/@'O@[I#'OcS8a:5_Y<8fh=lJSmJ`RLh*-1@#UuhX,=8I86m^'+)4?n^b2N-d3/?],U+[TZQ@ZJ8,<0,Yi>eoPABDBdLBA$k+0Ik*9&VW;<a<ghE=ZquneO>5@Mh:Ji.!#+`k%CJr^^%]YVpL:\WM.^h5>]]TUiL[_3bUPl*u7tL)fSq&ABG:._)GlSks3%?6@q<#fWg]-m\(U)K<V<fZ#)"#g-=L)_=g^(43+QjCJ9nCJK5L+ut3!C0@CCq/eFOEnq$^=I2k%!i4NY9D?D2a]>AD%ZQqC(%lgdge#da<N%1N;lT3hpLr?F>uIVqb%d[b>@jSh2'HC<+`WqKT\j."HGbZ/,'GI@L]d5Gq#Bu(=GEa'j*$L`Rna35kpC)q-)VX=iB?Q>cb;U14X_hGR&cJicR65LLeK?KTlcegm"M*#IBaRqVfL6:M.[Wh$KLqAK0+g#D*30YbcTZBVL*J+KQ8j4'43h]r`7UAqHR_2FMW4U(].V2NG5u__ND;RK6I;:rW6,"=tf~>endstream
endobj
xref
0 10
0000000000 65535 f
0000000073 00000 n
0000000124 00000 n
0000000231 00000 n
0000000343 00000 n
0000000458 00000 n
0000000661 00000 n
0000000729 00000 n
0000001025 00000 n
0000001084 00000 n
trailer
<<
/ID
[<621e745f4154d3ac7a42de07bdd8794e><621e745f4154d3ac7a42de07bdd8794e>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 10
>>
startxref
1779
%%EOF

View file

@ -1,80 +0,0 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20260420002638-01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20260420002638-01'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 1 /Kids [ 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 645
>>
stream
Gat$u;/=o?&:X)O\Araq7?SD+a]l*Rm7'_NodC6`..P8W>KNG3^t>i_Ce?:WES)tdE9P%)]=[OMJ;0,-k>h\dEgU/f?l\_X0L5j&\*u7>lf&mdg;Ok]pMom2O]%QZN+CTcK3Z=iK3(.L2\iD9Y:h#JK)F(Z;IH.<dKiU:oJl0Vq46<liGp-8i9;4:8h'ZhP.@f3>9AG%RA'dZ8Tl(;M;Z.lg7m%'r?#V+#+[C[+hXgYl(%>:Lj%@c-Y$GTZ`"76>Gs6G*oW%,BOGaN\3XoX9SV137[hSKN*;q*b!REa+VYE_685)jc=;j2%+poDP+1suFj9/'1o)>"7]VsjQiC>b3a;5CmR!8e_A&5;*gb0YK9R*C%hIFKTIS?Lf./'.4>sU0AXJ?:'Ki%F;f7lOdf8#o"_'B(%Dp*n'!q.>=Br1X_In@U1sS''A`Wjehl1+L*1tN,2no:=PnEL:G0[+39KTbr2jZmOrqY\k!kL,7^BBtD`<kr5,#9U5P`F4jdI8fK7f+/@#uCA.ORb$/6JX,8%UMJt<W=X1r3nMdd^aN[$dRq>;*O?sX)7aI6USk9`Ike3IM.son+Et.<>Zi+<03="'oQ`85>71#[^?PT*K9I,oI;ls,.0QF=X7oSNc#8qr<64SCKL~>endstream
endobj
xref
0 10
0000000000 65535 f
0000000073 00000 n
0000000124 00000 n
0000000231 00000 n
0000000343 00000 n
0000000458 00000 n
0000000661 00000 n
0000000729 00000 n
0000001025 00000 n
0000001084 00000 n
trailer
<<
/ID
[<c69a670760cbecedce0d0f0aa897bce2><c69a670760cbecedce0d0f0aa897bce2>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 10
>>
startxref
1819
%%EOF

View file

@ -1,74 +0,0 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (anonymous) /CreationDate (D:20260420002638-01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20260420002638-01'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 629
>>
stream
Gat$u9okbt'YNU1]OOh_epoJMDS+[%:t8PjKdtN.M\BF4Rp[N%>l*c%BN.Y4;--:2/AITuo>V8jfI,n>q[27)KtHLJJe6?4"Os?2IYXhCeua]=Y\nmRL])O<JRATn*r)6Y3M[D62b"k4=V\0t^+:E*JFq#l,g/G6U^8"Vof29K0aFs:mH03k(:"'&+U$Z..%si4bA2&IPBPm.kMu&o"92)[)Oj?nq'B%I_o?4!V+)6&oT4`B7!m:s7oM%%fPppb%0bIp622oZ<,bku]V<uU]HO_9_0FC<PS/*b%63>YCu^UM"D+]L%$mi4Mg_c9Z*W=TB25q0p'VtnW+DO[lI4"^GhEIMZS%r+4-427/j88s-'(Bb"Di(5HFd8E`+E5?9&t.@c*c7+LKh&MCQ'%;!]]r.FG*TWE*:(lfNGob^n\G/l;h/P5/$kYZ($gE_$jH%mJdC=!KQ!_4S3&rBD-KT3+VX$f4PVo=p]8U1:+q/mK$e4@cA%V:!]??hl@+Wd@MMo'pV'V2F!p8Qn>0Qg]@?"`j7&8S?#Y.\n>pfT2>Qb:NYh\qGUODRXM1&D$AAhDi`&H4"4_,<b\%s4E?o?Kuu'YIscD>'nf.p$SEU*J@`KCfZ[as)_0uXW;~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000536 00000 n
0000000604 00000 n
0000000900 00000 n
0000000959 00000 n
trailer
<<
/ID
[<9b415a84726399a7dd006f60068c5362><9b415a84726399a7dd006f60068c5362>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1678
%%EOF

View file

@ -1,152 +0,0 @@
{
"$schemaVersion": "1.0",
"$kind": "poweron.workflow",
"$exportedAt": "2026-04-16T10:00:00Z",
"$gatewayVersion": "demo-2026-04",
"label": "PWG Pilot: Jahresmietzinsbestätigung",
"description": "Verarbeitet gescannte Rückantworten der Jahresmietzinsbestätigungen: OCR, Abgleich gegen Trustee-DB (Mieter + Mietzins-Buchungen), AI-Klassifikation pro Scan und Zustellung als CSV-Anhang im Outlook-Draft an die Sachbearbeitung. Pilot-Lieferung Sommer 2026.",
"tags": ["pwg", "pilot", "mietzins", "trustee", "ocr"],
"templateScope": "instance",
"sharedReadOnly": false,
"notifyOnFailure": true,
"graph": {
"nodes": [
{
"id": "n1",
"type": "trigger.manual",
"x": 50,
"y": 200,
"title": "Manueller Start",
"parameters": {}
},
{
"id": "n2",
"type": "sharepoint.listFiles",
"x": 320,
"y": 200,
"title": "Scan-Ordner auflisten",
"parameters": {
"connectionReference": "",
"pathQuery": "PWG/Mietzinsbestaetigungen/Scans-Eingang"
}
},
{
"id": "n3",
"type": "flow.loop",
"x": 590,
"y": 200,
"title": "Pro Scan-Dokument",
"parameters": {
"items": {"type": "ref", "nodeId": "n2", "path": ["files"]},
"concurrency": 1
}
},
{
"id": "n4",
"type": "sharepoint.downloadFile",
"x": 860,
"y": 200,
"title": "PDF/Bild laden",
"parameters": {
"connectionReference": "",
"pathQuery": "{{loop.item.path}}"
}
},
{
"id": "n5",
"type": "trustee.extractFromFiles",
"x": 1130,
"y": 200,
"title": "OCR & Felder extrahieren",
"parameters": {
"featureInstanceId": "",
"prompt": "Extrahiere die folgenden Felder aus dieser Jahresmietzinsbestätigung und antworte als JSON: tenantName (string), tenantAddress (string), objectAddress (string), confirmedRentAmount (number|null in CHF), currency ('CHF'), period (string z.B. '2026'), tenantNotes (string|null - alle handschriftlichen Anmerkungen oder Korrekturen), hasSignature (boolean - ist eine Unterschrift vorhanden?), documentDate (ISO date|null), ocrConfidence (number 0-1)."
}
},
{
"id": "n6",
"type": "trustee.queryData",
"x": 1400,
"y": 200,
"title": "Referenzdaten Trustee-DB",
"parameters": {
"featureInstanceId": "",
"mode": "lookup",
"entity": "tenantWithRent",
"tenantNameRef": "{{n5.output.tenantName}}",
"tenantAddressRef": "{{n5.output.tenantAddress}}",
"period": "{{n5.output.period}}",
"rentAccountPattern": "6000-6099"
}
},
{
"id": "n7",
"type": "ai.prompt",
"x": 1670,
"y": 200,
"title": "Prüfung & Klassifikation",
"parameters": {
"outputFormat": "json",
"simpleMode": false,
"documentList": "{{n5.output}}",
"context": "{{n6.output}}",
"aiPrompt": "Du bist ein Sachbearbeitungs-Assistent der Stiftung PWG. Deine Aufgabe ist es, eine eingescannte und OCR-extrahierte Jahresmietzinsbestätigung gegen die Stammdaten der Buchhaltung (Trustee-Feature) abzugleichen.\n\nEingaben:\n1. SCAN_DATEN (extrahiert per OCR aus dem Rückantwort-Dokument):\n{{scan}}\n\n2. REFERENZ_DATEN (aus Trustee-DB für diesen Mieter; ggf. leer wenn nicht eindeutig zuordenbar):\n{{reference}}\n\nVorgehen:\n1. Prüfe Identität: Stimmt SCAN_DATEN.tenantName + SCAN_DATEN.tenantAddress mit einem Datensatz in REFERENZ_DATEN.contacts überein? (Toleranz: kleine Tippfehler, Umlaute, Abkürzungen).\n2. Prüfe Mietzinsbetrag: Stimmt SCAN_DATEN.confirmedRentAmount mit dem aus REFERENZ_DATEN.expectedRentAmount erwarteten Mietzins überein? (Toleranz: ±1 CHF Rundung).\n3. Prüfe Unterschrift: hasSignature muss true sein.\n4. Prüfe OCR-Qualität: ocrConfidence < 0.6 -> 'unleserlich'.\n\nKlassifiziere in EXAKT EINEN Status:\n- 'bestaetigt': Identität stimmt, Betrag stimmt, Unterschrift vorhanden.\n- 'abweichung_betrag': Identität ok, Unterschrift ok, Betrag weicht ab.\n- 'abweichung_anmerkung': tenantNotes enthält substantielle Anmerkung (nicht leer, nicht reine Bestätigung).\n- 'keine_unterschrift': hasSignature == false.\n- 'unleserlich': OCR-Qualität ungenügend ODER Pflichtfelder fehlen.\n- 'kein_match': Mieter nicht in REFERENZ_DATEN auffindbar.\n\nBei Status != 'bestaetigt': Generiere einen kurzen, höflichen Antwortvorschlag (deutsch, Sie-Form, max. 5 Sätze, PWG-Stil) für die Sachbearbeitung. Bei 'bestaetigt': antwortVorschlag = null.\n\nAntworte AUSSCHLIESSLICH als JSON nach folgendem Schema:\n{\n \"tenantName\": string,\n \"objectAddress\": string,\n \"status\": \"bestaetigt\" | \"abweichung_betrag\" | \"abweichung_anmerkung\" | \"keine_unterschrift\" | \"unleserlich\" | \"kein_match\",\n \"scanRentAmount\": number | null,\n \"expectedRentAmount\": number | null,\n \"delta\": number | null,\n \"tenantNotes\": string | null,\n \"antwortVorschlag\": string | null,\n \"matchConfidence\": number,\n \"auditEvidence\": string\n}"
}
},
{
"id": "n8",
"type": "data.aggregate",
"x": 1940,
"y": 200,
"title": "Ergebnisse sammeln (im Loop)",
"parameters": {
"mode": "collect"
}
},
{
"id": "n9",
"type": "data.consolidate",
"x": 2210,
"y": 200,
"title": "CSV bauen (nach Loop)",
"parameters": {
"mode": "csvJoin",
"separator": "\n"
}
},
{
"id": "n10",
"type": "email.draftEmail",
"x": 2480,
"y": 200,
"title": "Draft an Sachbearbeitung",
"parameters": {
"connectionReference": "",
"to": "sachbearbeiter@pwg.ch",
"subject": "Mietzinsbestätigungen Auswertung {{currentDate}}",
"body": "Hallo,\n\nim Anhang die Auswertung der eingegangenen Jahresmietzinsbestätigungen.\nPro Scan eine Zeile mit Status, Betragsabgleich und (bei Abweichung) Antwortvorschlag.\n\nBitte die Zeilen mit Status != 'bestaetigt' manuell sichten.\n\nFreundliche Grüße,\nPWG Automation",
"emailStyle": "business",
"attachments": [
{
"name": "mietzinsbestaetigungen-auswertung",
"mimeType": "text/csv",
"csvFromVariable": "n9.output"
}
]
}
}
],
"connections": [
{"source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0},
{"source": "n2", "target": "n3", "sourceOutput": 0, "targetInput": 0},
{"source": "n3", "target": "n4", "sourceOutput": 0, "targetInput": 0},
{"source": "n4", "target": "n5", "sourceOutput": 0, "targetInput": 0},
{"source": "n5", "target": "n6", "sourceOutput": 0, "targetInput": 0},
{"source": "n6", "target": "n7", "sourceOutput": 0, "targetInput": 0},
{"source": "n7", "target": "n8", "sourceOutput": 0, "targetInput": 0},
{"source": "n8", "target": "n9", "sourceOutput": 0, "targetInput": 0},
{"source": "n9", "target": "n10", "sourceOutput": 0, "targetInput": 0}
]
},
"invocations": []
}

Binary file not shown.

View file

@ -1,225 +0,0 @@
# PowerOn × Abacus — Executive Briefing
*Vertraulich · C-Level Briefing für Abacus Research AG · Stand April 2026*
**Zielgruppe:** Geschäftsleitung Abacus (Strategie, Produkt, Partnerschaften)
**Zweck:** Abacus ein klares, belastbares Bild davon geben, **wer PowerOn ist, was PowerOn/PORTA leistet und wo der strategische Hebel für eine Zusammenarbeit liegt** — inklusive konkretem Zwischenstand zur Abacus-Schnittstelle.
**Lesedauer:** 7 Minuten.
---
## 1. Management Summary (60 Sekunden)
- **PowerOn ist eine Schweizer KI-Plattform** aus Zürich, die Unternehmen KI-gestützte Geschäftsprozesse **sicher, mandantengetrennt und datenschutzkonform** zur Verfügung stellt.
- Das Kernprodukt **PORTA** (Powerful Orchestration & Real-Time Automation) ist eine **in der Schweiz gehostete Multi-Mandanten-Plattform** mit vier Kernfunktionen in einem konsistenten System:
- **Multi-LLM-Orchestrierung** (Anthropic, OpenAI, Mistral, Perplexity, Tavily, Private LLM kein Vendor-Lock-in)
- **Workflow-Automatisierung** (Graph-basierter Flow-Editor, Scheduler, Execution-Engine)
- **Datenneutralisierung** (zentrales AI-Gate, optional Hard-Mode, optional Private LLM)
- **Integriertes Audit- und Compliance-Logging** (DSGVO/revDSG, lückenloser Audit-Trail)
- PORTA ist modular aufgebaut (**Feature-Store**): Mandanten schalten nur die Module frei, die sie brauchen u. a. **AI-Chat-Workspace, Treuhand-/Buchhaltungs-Modul mit Abacus-Anbindung, Kommunikations-/Coaching-Modul, Teams-Meeting-Bot, Machbarkeitsstudie Immobilien, Workflow-Designer / Automation-Studio**. Diese Bausteine laufen **produktiv** der Plattform-Unterbau ist gebaut, Kundenprojekte beschränken sich auf **Konfiguration, Datenanbindung, Tuning, Schulung und Inbetriebnahme**.
- **Das Treuhand-Modul besitzt bereits eine abstrahierte Buchhaltungs-Schnittstelle** mit produktiven Connectoren für **Run my Accounts** und **Bexio** sowie einem **bereits implementierten, lauffähigen Abacus-Connector** (OAuth 2.0, OData V4, Kontenplan, Buchungs-Push, Journal-Read, Debitoren, Kreditoren). Es fehlt nur der produktive Feinschliff mit einem Pilotkunden.
- **Strategischer Kern-Punkt für Abacus:** PowerOn ist **nicht** ein weiterer ERP-Wettbewerber. PowerOn ist die **KI- und Workflow-Schicht oberhalb** des ERP. Für Abacus-Kunden bedeutet das: Abacus bleibt System of Record PORTA liefert Intelligenz, Automatisierung und ein modernes User-Interface auf den Daten, die in Abacus entstehen.
---
## 2. Wer ist PowerOn?
### 2.1 Firma
- **PowerOn AG**, Birmensdorferstrasse 94, 8003 Zürich `www.poweron.swiss`
- Schweizer Unternehmen, Schweizer Datenhaltung, Schweizer Kundenfokus (DACH).
- Entstanden aus der **ValueOn AG** (Strategie-/Beratungshaus) derzeit in strukturierter Verselbständigung zur eigenständigen Organisation.
### 2.2 Gründer-/Kernteam
| Person | Rolle | Profil |
|---|---|---|
| **Patrick Motsch** | CEO / CTO | Langjährige Erfahrung in der Leitung komplexer IT-Implementierungen und innovativer Softwareentwicklung. |
| **Ida Dittrich** | Product Architect | Verbindet wissenschaftliches Know-how mit praktischer IT-Erfahrung und treibt die Produkt-/Architekturentscheide. |
| **Stephan Schellworth** | Business Integration | Verbindet strategisches Denken mit praxisnaher Projektsteuerung; Ansprechpartner für Partnerschaften und Kundenintegrationen. |
### 2.3 Reifegrad & Fokus
- **Produktstatus:** Early Product-Market-Fit mit produktiv laufenden Features; aktiv im Pilotkunden-Modus; Seed-Runde in Vorbereitung.
- **Zielmarkt:** mittelständische Unternehmen in datenschutzsensiblen Branchen **Treuhand, Finanzdienstleistungen, Immobilien, Professional Services, Legal, Healthcare** also eine sehr hohe Überlappung mit Abacus-Kernkunden.
- **Go-to-Market:** aktuell DACH; in einem strategischen Verbund mit **ValueOn (Strategie-Beratung), Aumico (Frontend/MVP) und Modeso (Hosting/SRE)** aufgestellt End-to-End abdeckbar, ohne dass Abacus technologische oder operative Lücken schliessen müsste.
---
## 3. Was macht PowerOn und was kann PORTA?
### 3.1 Die Kernidee in einem Satz
> **PowerOn liefert Unternehmen einen sicheren KI-Arbeitsplatz, der ihre Prozesse versteht, ihre Systeme anbindet und wiederkehrende Arbeit automatisiert ohne dass sensible Daten unkontrolliert in fremde KI-Dienste abfliessen.**
### 3.1.1 Die vier Kernfunktionen von PORTA
PORTA bündelt in **einer** in der Schweiz gehosteten Multi-Mandanten-Plattform das, was sonst über mehrere isolierte Werkzeuge verteilt ist:
| Kernfunktion | Was sie leistet | Status |
|---|---|---|
| **Multi-LLM-Orchestrierung** | Zentrale Modellauswahl und Routing über mehrere Provider (Anthropic, OpenAI, Mistral, Perplexity, Tavily, Private LLM). Billing-Preflight, Streaming, Fallbacks, Operation-Typ-basierte Modellwahl. | **Produktiv** |
| **Workflow-Automatisierung** | Graph-basierter Flow-Editor (n8n-Style), Execution-Engine mit topologischer Sortierung, Scheduler, UDM-Dokumentenmodell, drei Modi (Learning / Actionplan / Automation). | **Produktiv** |
| **Datenneutralisierung** | Zentrales AI-Gate, das Prompt, RAG-Kontext und Messages vor jedem externen Modellaufruf pseudonymisiert. Hard-Mode blockiert Calls, wenn Neutralisierung nicht möglich. Private-LLM-Option für volle On-Prem-Variante. | **Produktiv** |
| **Audit- und Compliance-Logging** | Integrierter, lückenloser Audit-Trail für Zugriffe, Admin-Aktionen, Berechtigungs- und Verschlüsselungs-Events sowie KI-Datenflüsse. DSGVO-/revDSG-Betroffenenrechte als Self-Service. | **Produktiv** |
### 3.1.2 Produktiver Abdeckungsgrad (Stand heute)
Die für Treuhand, Finanz und KMU typischen Bausteine sind **bereits produktiv in der Plattform** und werden nicht erst für ein neues Projekt entwickelt:
- Buchhaltungs-Modul mit **Abacus-Anbindung** (sowie RMA und Bexio)
- **Coaching- und Trainings-Modul** (Kommunikations-Coach, Voice, Dossier, Gamification)
- **KI-Arbeitsplatz** (Power Desktop / AI-Workspace mit RAG und Agent-Tools)
- **Datenneutralisierung** (zentrales AI-Gate + Private-LLM-Option)
- **Workflow-Designer** (grafischer Flow-Editor inkl. Scheduler und Execution-Engine)
Für einen Abacus-nahen Kundenfall oder ein gemeinsames Pilot-Engagement reduziert sich der Projektaufwand damit auf **kundenspezifische Konfiguration, Datenanbindung, Tuning, Schulung und Inbetriebnahme** nicht auf Plattform-Grundlagenentwicklung. Das ist der entscheidende Geschwindigkeitsvorteil gegenüber „We-build-it-from-scratch"-Angeboten.
### 3.2 Die fünf Prinzipien hinter „Erfolgreichem KI-Einsatz"
1. **Use-Cases zuerst:** Schrittweise Einführung statt Big-Bang. Mandanten aktivieren modular die Features, die sie brauchen.
2. **Datenschutz by Design:** Ein zentrales AI-Gate neutralisiert sensible Inhalte *vor* jedem externen Modell-Aufruf. Option: komplett lokaler Betrieb über Private LLM.
3. **Berechtigungen:** Vierstufiges RBAC (System → Mandant → Feature → Feature-Instanz), granular pro Aktion (Lesen/Schreiben/Bearbeiten/Löschen), vollständige Mandantentrennung serverseitig.
4. **Verbindungen:** Toolbox-Registry mit offenen Connectoren zu Microsoft 365, Google Workspace, SharePoint, ClickUp, Jira, E-Mail/SMS, Websuche, Swiss-Topo/Geo-Systemen und **Buchhaltungs-Systemen (RMA, Bexio, Abacus)**.
5. **Regeln / Ethik:** Lückenloser Audit-Trail, DSGVO-Betroffenenrechte als Self-Service, kein Training mit Kundendaten.
### 3.3 PORTA — Feature-Landkarte (Auszug)
| Feature | Was es tut | Relevanz für Abacus-Kundschaft |
|---|---|---|
| **Power Desktop / AI-Workspace** | KI-Chat mit RAG über Firmendokumente, Editor, Playground. 40+ Agent-Tools in thematischen Toolboxes. | Sofort-Nutzen für Treuhänder/Berater, die mit Dokumenten arbeiten. |
| **Treuhand-Modul** | Positionen, Dokumente, Expense Import, Scan/Upload, Buchhaltungs-Sync. Pluggable Connector-Architektur. | **Direkter Touchpoint zu Abacus** siehe Kapitel 5. |
| **Automation Studio (n8n-Style Flow Editor)** | Graphical Flow Editor, Scheduler, Workflow-Runs, UDM-Dokumentenmodell. | Automatisiert Prozesse um/auf Abacus-Daten (Freigaben, Reports, Benachrichtigungen). |
| **Kommunikations-Coach** | KI-gestütztes Gesprächstraining mit Voice (STT/TTS), Dossier, Gamification. | Sales-Coaching, Kundenkommunikation, Onboarding. |
| **Teams-Meeting-Bot** | Nimmt an Teams-Meetings teil, transkribiert, antwortet kontextbezogen. | Meeting-Protokolle, Folge-Aufgaben automatisch aus Gesprächen ableiten. |
| **Machbarkeitsstudie Real Estate** | Extrahiert BZO/Parzellen-Daten und bewertet Immobilienpotenziale. | Spezialisiertes Branchen-Modul (Immobilien-Treuhand, Verwaltungen). |
| **Chatbot / Knowledge Retrieval** | RAG über Firmenwissen, semantische Suche via pgvector. | Interner Helpdesk, Dokumenten-Q&A. |
| **Neutralization / Private LLM** | Pseudonymisiert PII/Geschäftsgeheimnisse vor externen KI-Calls oder hält Daten komplett lokal. | Zwingend für Treuhand/Finanz-Kontext. |
### 3.4 Technische Basis (für das technische Gegenüber bei Abacus)
- **Backend (Gateway):** FastAPI/Python, PostgreSQL inkl. `pgvector` für Embeddings.
- **Frontend (Nyla):** React/TypeScript, Vite.
- **AI-Core:** Multi-Provider (Anthropic, OpenAI, Mistral, Perplexity, Tavily, Private LLM) — **Modellunabhängigkeit, kein Vendor-Lock-in**.
- **Architektur:** saubere Schichtung **Connectors → Interfaces → Services → ServiceCenter** mit zentraler Orchestrierung, `PublicService`-Wrapper für kontrolliertes API-Surface.
- **Workflow-Engine:** eigene Graph-Execution-Engine (topologische Sortierung, Transit-Routing, Schema-Validierung, Resume), drei Modi (Learning, Actionplan, Automation).
- **Security:** AES/Fernet + PBKDF2-HMAC-SHA256 für Secrets, JWT + Cookie-Session, CSRF, Rate-Limiting, parametrisierte Queries, RBAC serverseitig. Orientierung an DSGVO, revDSG und OWASP Top 10. Formale ISO-27001-Zertifizierung noch nicht vorhanden technische Basis dafür vorhanden.
- **Betrieb:** Containerisiert, cloud-native, hosted bei Modeso (Partner) auf Google Cloud Infrastruktur, Deployment-Pipelines via GitHub Actions.
---
## 4. Differenzierung (warum nicht Microsoft Copilot oder n8n?)
| Anforderung | Microsoft Copilot / ChatGPT Enterprise | n8n / Zapier | **PowerOn PORTA** |
|---|---|---|---|
| Datenschutz-Neutralisierer | — | — | **Ja, zentral am AI-Gate** |
| Eigenes/lokales LLM möglich | Teilweise | — | **Ja, Private-LLM-Connector** |
| Multi-Provider, kein Lock-in | Nein | Ja | **Ja** |
| Business-User-fähig (ohne Entwickler) | Ja | Nein | **Ja** |
| Workflow- + Chat- + RAG in einer Plattform | Nein | Nur Workflow | **Ja** |
| Swiss-hosted, Swiss-built | Nein | Nein | **Ja** |
| Branchenmodule (Treuhand, Immobilien, …) | — | — | **Ja, Feature-Store** |
| Direkte Buchhaltungs-Integration | — | Generisch | **Ja (RMA, Bexio, Abacus ready)** |
---
## 5. Die Abacus-Schnittstelle — konkreter Stand
Das ist der wichtigste Abschnitt für Abacus. **PowerOn hat die Abacus-Schnittstelle nicht nur sondiert, sondern bereits implementiert** im Modul `trustee/accounting/connectors/accountingConnectorAbacus.py`.
### 5.1 Was bereits umgesetzt ist
| Bereich | Stand | Technik |
|---|---|---|
| Authentifizierung | **Implementiert** | OAuth 2.0 Client Credentials (Service User) mit OIDC-Discovery (`/.well-known/openid-configuration`), Token-Caching, automatischer Refresh |
| Datenmodell (Abacus ↔ PORTA) | **Implementiert** | Entity-API via OData V4, pro Mandant konfigurierbare `apiBaseUrl` und `clientName` |
| Kontenplan (`Accounts`) | **Implementiert** | Paginiertes Auslesen inkl. `@odata.nextLink`, Mapping auf einheitliches `AccountingChart`-Format |
| Buchung erfassen (`GeneralJournalEntries`) | **Implementiert** | POST mit Mehrzeilen-Journal, Debit/Credit/TaxCode/CostCenter, Rücklieferung `externalId` |
| Buchungsstatus lesen | **Implementiert** | GET auf `GeneralJournalEntries({id})` |
| Journal lesen (Zeitraum-/Filter) | **Implementiert** | `$filter` auf `JournalDate`, paginiertes Streaming |
| Stammdaten | **Implementiert** | `Debtors`, `Creditors` |
| Sicherheit | **Implementiert** | Secrets verschlüsselt gespeichert (`TrusteeAccountingConfig.encryptedConfig`), Plugin-Discovery identisch zu Bexio/RMA |
### 5.2 Architektur-Prinzip
Die Connector-Schicht ist **abstrahiert** (`BaseAccountingConnector`). Alle Buchhaltungs-Integrationen teilen sich dieselben Datenmodelle (`AccountingBooking`, `AccountingBookingLine`, `AccountingChart`, `SyncResult`) und werden über eine **Plugin-Registry** discovered. Das heisst:
- Jeder neue Connector (Abacus, SAP Business One, Sage etc.) wird ohne Änderung am Kernsystem angeflanscht.
- Abacus steht **auf Augenhöhe** mit Bexio und Run my Accounts im Produkt.
- Kunden können in der gleichen PORTA-Oberfläche zwischen den Systemen wählen bzw. umziehen.
### 5.3 Was noch offen ist
- **Produktiv-Pilot mit einem realen Abacus-Mandanten** (Credentials, Mandantenstruktur, Konto-Mapping, Kostenstellen-Logik, Beleg-Anhänge via Dokument-Upload).
- **Feinheiten**: Mehrwährung, spezifische Abacus-Customizings, Dokument-Anhänge an Buchungen (`uploadDocument` ist im Basis-Interface vorgesehen), Rückkanal für Freigabe-Workflows.
- **Zertifizierung/Partner-Listing** auf Abacus-Seite.
### 5.4 Projektcharakter bei einem gemeinsamen Kundenengagement
Weil die Plattform-Bausteine (Buchhaltungs-Modul mit Abacus-Anbindung, KI-Arbeitsplatz, Datenneutralisierung, Workflow-Designer, Coaching-Modul) **produktiv laufen**, reduziert sich ein gemeinsames Kundenprojekt auf klar abgrenzbare, planbare Tätigkeiten nicht auf Plattform-Neuentwicklung:
| Aufwandsblock | Inhalt |
|---|---|
| **Konfiguration** | Aktivierung der benötigten PORTA-Module pro Mandant, Rollen-/RBAC-Modell, Feature-Instanzen, Branding |
| **Datenanbindung** | Abacus-Credentials (OAuth 2.0), Kontenplan-Mapping, Debitoren/Kreditoren-Synchronisation, ggf. weitere Quellen (SharePoint, Mail, DMS) |
| **Tuning** | Prompt-Tuning für die konkreten Use-Cases, Neutralisierungs-Regeln auf Kundenebene, Modellauswahl pro Operation |
| **Schulung** | Onboarding Endanwender, Admin-Training, Enablement für Treuhand-Teams |
| **Inbetriebnahme** | Pilotbetrieb, Abnahme, Go-Live, Hyper-Care, Hand-Over an Betrieb (Modeso / Abacus-Partnerbetrieb) |
Das macht ein JV-Angebot an einen Abacus-Endkunden **kalkulierbar und schnell umsetzbar** ein Setup, das in Wochen, nicht in Quartalen live geht.
### 5.4 Warum das für Abacus strategisch interessant ist
1. **Keine Konkurrenz, echte Ergänzung:** PORTA schreibt in Abacus, es ersetzt es nicht. Abacus bleibt das System of Record.
2. **Moderne UI-Schicht für Abacus-Kunden:** Treuhänder, die heute für KI-Features zu anderen Werkzeugen greifen, bleiben im Abacus-Ökosystem.
3. **Generator für Beleg-Volumen in Abacus:** PORTA verarbeitet Scans, Spesen, Dokumente automatisch und erzeugt saubere Buchungen in Abacus. Das erhöht die Nutzungstiefe pro Abacus-Mandant.
4. **Schweizer Stack Ende-zu-Ende:** Schweizer ERP (Abacus) × Schweizer KI-Plattform (PowerOn) × Schweizer Hosting (Modeso auf GCP CH) ein seltenes Alleinstellungsmerkmal im Markt.
5. **Datenschutz-Thema ist vorgelöst:** Der Neutralisierer ist für genau das Szenario gebaut, das Abacus-Kunden (KMU, Treuhand, Finanz) am meisten Sorge macht, wenn sie über KI nachdenken.
---
## 6. Mögliche Joint-Venture-Thesen
Zur Vorbereitung der nächsten Abacus-Gespräche nicht abschliessend, sondern als Diskussionsgrundlage.
### These A — „Abacus als strategischer Go-To-Market-Kanal"
PowerOn liefert das KI-/Workflow-Produkt, Abacus öffnet die Tür zur bestehenden Treuhand-/KMU-Kundschaft. Co-Marketing, gemeinsame Referenzkunden, Listing im Abacus-Ökosystem.
### These B — „Abacus-Branded KI-Layer"
PORTA wird als Abacus-gelabeltes Modul („AbaAI", „Abacus Intelligence", o. ä.) angeboten. Abacus kontrolliert Pricing und Packaging gegenüber dem Endkunden, PowerOn bleibt die technische Plattform-Basis.
### These C — „Gemeinsame Produktentwicklung mit Fokus Treuhand"
Tiefe Integration des PowerOn-Treuhand-Moduls mit Abacus AbaWeb/AbaNinja inklusive Automation-Templates für typische Treuhand-Use-Cases (Kreditorenflut, Spesenimport, MwSt-Abstimmung, Mandats-Reporting).
### These D — „Beteiligung / Minderheits-Invest"
Abacus beteiligt sich an der anstehenden Seed-Runde und sichert sich damit strategische Einflussmöglichkeiten, ohne PowerOn als eigenständiges Unternehmen zu vereinnahmen.
Alle vier Thesen sind kompatibel und können gestaffelt umgesetzt werden (A → C → B → ggf. D).
---
## 7. Empfohlene nächste Schritte
| # | Schritt | Owner | Zeithorizont |
|---|---|---|---|
| 1 | Technisches Deep-Dive: Live-Demo des Abacus-Connectors auf einer Abacus-Testinstanz | PowerOn (P. Motsch) × Abacus (Tech/Produkt) | 2 Wochen |
| 2 | Gemeinsamer Pilot-Kunde aus dem Abacus-Treuhand-Segment | Abacus (Sales) × PowerOn (S. Schellworth) | 46 Wochen |
| 3 | Strategie-Workshop zu JV-Modell (Thesen AD) | beide GL | 4 Wochen |
| 4 | NDA + DPA für vertiefte technische Zusammenarbeit | Legal beider Seiten | sofort |
| 5 | Gemeinsamer Messeauftritt / Webinar Treuhand-KI | Marketing beider Seiten | Q3 2026 |
---
## 8. Kontakt
**PowerOn AG**
Birmensdorferstrasse 94 · 8003 Zürich · Schweiz
`www.poweron.swiss` · `info@poweron.swiss`
- **Patrick Motsch** CEO / CTO Produkt- und Technikthemen
- **Stephan Schellworth** Business Integration Partnerschaften und Kundenintegration
- **Ida Dittrich** Product Architect Architektur und Roadmap
---
*Dieses Dokument ist eine konsolidierte Aufbereitung des aktuellen Produkt- und Technikstands von PowerOn PORTA auf Basis der internen Wiki-Kanon-Seiten (`a-strategy/product-vision.md`, `a-strategy/product-strategy.md`, `b-reference/product.md`, `b-reference/gateway/architecture.md`, `b-reference/gateway/ai-agent.md`, `b-reference/platform/neutralization.md`, `e-compliance/security-overview.md`) sowie einer direkten Code-Verifikation im Gateway-Repository (Stand April 2026). Angaben ohne Gewähr für verbindliche Zusicherungen gelten die jeweiligen Vertragsvereinbarungen.*

View file

@ -1,100 +0,0 @@
# PowerOn KI-gestützte Automatisierung
*Fertiger Copy-Stand für Canva / PowerPoint / PDF-Export. 5 Folien.*
**Schreibweise:** durchgängig **PowerOn** (nicht PowerON).
---
## Folie 1 von 5 Intro
### PowerOn
**KI, die Ihre Kapazität freisetzt.**
Von manuellen Prozessen zu KI-unterstützten Abläufen schnell, konkret, sicher.
**www.poweron.swiss · info@poweron.swiss**
> **Visual-Hinweis:** Titel-Layout, PowerOn-Logo zentriert, keine Tabellen. Hintergrund clean, ggf. dezentes Grafik-Element.
---
## Folie 2 von 5 Der Weg zur KI-gestützten Automatisierung
### Verschwenden Sie Ihre kostbare Kapazität nicht steigern Sie Ihre Innovationskraft
**Von aufwendigen manuellen Prozessen zu KI-unterstützten automatisierten Prozessen**
| Schritt 1 | Schritt 2 | Schritt 3 | Schritt 4 |
| --- | --- | --- | --- |
| **Verstehen der spezifischen Anforderungen** | **Mögliche KI-Unterstützung identifizieren** | **Komplexität reduzieren** | **Vertrauenswürdige Informationen** |
| Wir analysieren, welche Prozesse manuell, repetitiv oder fehleranfällig sind. | Wir prüfen, wo KI konkret unterstützen oder Aufgaben übernehmen kann. | Unnötige Schritte werden eliminiert und Schnittstellen vereinfacht. | Die KI arbeitet ausschliesslich mit geprüften, klar definierten Daten. |
**Manuell → Automatisiert**
### KI wird Ihr Assistent
| Daten-Extraktion | Fraud Detection | Compliance Check | Smarte Freigabe | Prozessführung |
| --- | --- | --- | --- | --- |
| Relevante Inhalte automatisch aus Dokumenten gewinnen | Auffälligkeiten frühzeitig erkennen | Regelwerke automatisiert prüfen | Freigabeprozesse mit KI-Empfehlung beschleunigen | Abläufe Schritt für Schritt begleiten |
**schnell konkret**
> **Visual-Hinweis:** Vier Schritte als horizontaler Pfeil (links → rechts). Darunter die fünf KI-Outputs als Icon-Leiste. Claim „schnell konkret" rechts unten.
---
## Folie 3 von 5 Ihre Erfolgsstory gezielt und fokussiert
| Ihre Herausforderung | Unser Vorgehen | Ihr Ergebnis |
| --- | --- | --- |
| Sie wissen, dass Digitalisierung und KI wichtig sind aber nicht, wo Sie am wirkungsvollsten ansetzen. | **Fragebogen** gezielte Vorerhebung Ihrer Ausgangslage | **Handlungsfelder mit Massnahmen** Wo liegt der grösste Hebel? |
| | **Interview** vertiefte Analyse Ihrer Prozesse und Engpässe | **Kausalnetz mit Abhängigkeiten** Welche Massnahmen bauen aufeinander auf? |
| | **Initial-Workshop** gemeinsame Erarbeitung mit Ihrem Team | **Priorisierte Umsetzungsroadmap** In welcher Reihenfolge vorgehen? |
| | **Analyse** Synthese und Aufbereitung durch PowerOn | **Massnahmen-Steckbrief** Jede Massnahme einzeln beschrieben und umsetzbar |
| | | → **Digitalisierung** und **Einsatz von KI** |
```mermaid
flowchart LR
subgraph vorgehen [Unser Vorgehen]
Fragebogen --> Interview --> Workshop[Initial-Workshop] --> Analyse
end
subgraph ergebnis [Ihr Ergebnis]
Analyse --> Handlungsfelder
Handlungsfelder --> Kausalnetz
Kausalnetz --> Roadmap[Priorisierte Roadmap]
Roadmap --> Steckbrief[Massnahmen-Steckbrief]
end
```
> **Visual-Hinweis:** Drei-Spalten-Layout. Links: Fragezeichen-Symbolik für die Herausforderung. Mitte: Trichter von oben (Fragebogen) nach unten (Analyse). Rechts: Ergebnis-Artefakte als aufsteigende Liste.
---
## Folie 4 von 5 Das halten Sie am Ende in der Hand
| Handlungsfelder mit Massnahmen | Kausalnetz Massnahmen mit Abhängigkeiten |
| --- | --- |
| Identifizierte Bereiche mit konkreten Massnahmen, zugeordnet zu Ihren Geschäftszielen. | Visualisierung der Wechselwirkungen zwischen Massnahmen inklusive Engpässe und Voraussetzungen. |
| Priorisierte Umsetzungsroadmap | Steckbrief je Massnahme |
| --- | --- |
| Zeitliche Abfolge mit klarer Priorisierung von Quick Wins bis zu strategischen Initiativen. | Beschreibung, Ziel, Aufwand, Abhängigkeiten und nächste Schritte pro Massnahme einzeln dokumentiert. |
> **Visual-Hinweis:** 2×2-Raster, vier gleichgrosse Kacheln. Jede Kachel mit Titel und einem Satz. Optional je ein abstraktes Icon.
---
## Folie 5 von 5 Erfolgreicher Einsatz von KI
### Einsatz von KI
| Prinzip | Erklärung |
| --- | --- |
| **Datenschutz** | Keine sensitiven Daten gelangen nach aussen. Die Verarbeitung bleibt innerhalb klar definierter Grenzen. |
| **Klar definierte Use-Cases** | KI wird nur dort eingesetzt, wo der Anwendungsfall geprüft und freigegeben ist. |
| **Einfache Anbindung** | Bestehende Informationsquellen und Agentensysteme lassen sich ohne grossen Aufwand verbinden. |
| **Vertrauensvoller, fairer Einsatz** | Die KI arbeitet nachvollziehbar. Ergebnisse sind überprüfbar, Entscheidungen bleiben beim Menschen. |
| **Zugriff nur auf definierte Daten** | Die KI hat ausschliesslich Zugriff auf klar freigegebene Datenquellen kein unkontrolliertes Training. |
> **Visual-Hinweis:** Zentrales Label „Einsatz von KI" in der Mitte. Die fünf Prinzipien als Kranz/Stern drumherum angeordnet je mit Icon (Schloss, Zielscheibe, Stecker, Waage, Auge).

View file

@ -1,347 +0,0 @@
# PowerOn Desktop
*Der zentrale AI Workspace fuer Unternehmen, die produktiver, sicherer und schneller arbeiten wollen.*
**Subline:** Ein Workspace. Alle Daten. Alle KI-Faehigkeiten.
---
**1 von 16**
## Seite 1 - Cover
*KI, Daten und Teamarbeit ein gemeinsamer Arbeitsraum.*
PowerOn Desktop bringt KI, Daten und Teamarbeit in eine gemeinsame Arbeitsumgebung.
Sie reduzieren Reibung im Alltag und schaffen messbaren Mehrwert ab dem ersten Use Case.
> **BILD-PROMPT (Nano Banana Pro):**
> Erstelle eine moderne isometrische SaaS-Hero-Illustration eines digitalen Arbeitsplatzes. Zeige ein zentrales Dashboard mit verbundenen Modulen fuer Chat, Dokumente, Datenquellen und Automationen. Stil clean, hochwertig, C-Level-Praesentation. Farbpalette mit Primaerblau #1976d2, Tuerkis-Akzenten, Weiss und dezenten Grautoenen. Licht, Tiefe, klare Linien, keine Personenfotos. Kein Text im Bild. 16:9, hohe Aufloesung.
---
**2 von 16**
## Seite 2 - Die Herausforderung
*Wenn Wissen zerstreut ist, leidet die Wertschoepfung.*
In den meisten Unternehmen ist Wissen verteilt: Dateien, Mails, Fachsysteme und Meetings laufen nebeneinander.
Teams springen zwischen Tools, verlieren Kontext und investieren zu viel Zeit in Suche statt in Entscheidungen.
**Typische Folgen:**
- Lange Recherchezeiten bei jeder wichtigen Frage
- Uneinheitliche Qualitaet in Ergebnissen
- Hoehere Risiken bei Datenschutz und Compliance
- KI bleibt auf einzelne Experimente begrenzt
> **BILD-PROMPT (Nano Banana Pro):**
> Visualisiere eine fragmentierte Unternehmenslandschaft mit vielen isolierten Dateninseln: Dokumente, E-Mail, CRM, Tabellen, Tickets. Verbinde sie nicht direkt, sondern zeige bewusst Brueche und Medienwechsel. Abstrakt, modern, minimalistisch, isometrischer Look. Farben: Grau fuer Fragmentierung, Akzente in Blau fuer Potenzial. Kein Text im Bild. 16:9.
---
**3 von 16**
## Seite 3 - Die Loesung im Ueberblick
*Vier Zugaenge, ein durchgaengiger Kontext.*
PowerOn Desktop schafft einen gemeinsamen Arbeitsraum fuer vier Kernaufgaben:
- **Denken und abstimmen (AI Chat)** Fragen, Entwuerfe, Abstimmung und Entscheidungsvorbereitung an einem Ort
- **Inhalte umsetzen (Editor)** Texte und Dokumente mit KI-Unterstuetzung bearbeiten, aber immer mit Ihrer Freigabe
- **Wissen verbinden (Datenquellen)** Dateien, Clouds und Fachsysteme als durchsuchbaren Kontext einbinden
- **Prozesse beschleunigen (Workflows und Automation)** Wiederkehrende Ablaeufe planbar ausfuehren und Ergebnisse wiederverwenden
**Warum das fuer Fuehrungsteams zaehlt:** Statt fuenf getrennte Tools entsteht ein durchgaengiger Arbeitsfluss. Der Kontext aus Chat, Dateien und Quellen bleibt erhalten. Teams sparen Such- und Abstimmungszeit, und Sie behalten die Steuerung darueber, welche Informationen ueberhaupt in die KI einfliessen.
**Typischer Ablauf im Alltag:** Information beschaffen (Quellen) diskutieren und strukturieren (Chat) Inhalt finalisieren (Editor) bei Bedarf automatisieren (Workflows). Alles in derselben Instanz, ohne Export-Chaos und ohne Kontextverlust zwischen den Schritten.
> **BILD-PROMPT (Nano Banana Pro):**
> Erstelle eine isometrische Uebersichtsillustration mit einem zentralen Hub und vier klar verbundenen Modulen: Chat, Editor, Data Sources, Automation. Datenstroeme sollen in beide Richtungen fliessen. Stil: enterprise SaaS, aufgeraeumt, premium, viel White Space. Farbsystem mit Blau #1976d2 als Leitfarbe, Tuerkis und Violett als Sekundaerfarben. Kein Text im Bild. 16:9.
---
**4 von 16**
## Seite 4 - Arbeitsbereich 1: AI Chat
*Strukturiert denken mit nachvollziehbaren Antworten.*
Der AI Chat ist das Sprungbrett fuer den produktiven Einsatz von KI im Tagesgeschaeft. Teams nutzen ihn fuer Analyse, Formulierung, Zusammenfassungen und Entscheidungsvorbereitung ohne dass Fachwissen in Prompt-Engineering ausarten muss.
**Was Entscheider schaetzen:** Antworten bleiben nachvollziehbar, weil Bezuege zu Quellen und Verarbeitungsschritten sichtbar werden. Das reduziert das Risiko von „halluzinierten“ Fakten und erleichtert interne Freigaben. Optional unterstuetzt Spracheingabe und Sprachausgabe etwa fuer schnelle Notizen unterwegs oder barrierefreies Arbeiten.
**Konkrete Einsatzszenarien:** Erstentwurf fuer Kundenmail oder interne Mitteilung; Strukturierung eines Meetings oder eines Projektbriefings; Einordnung einer laengeren Unterlage mit klaren Bezugspunkten; Vorbereitung einer Praesentation aus gebundenem Kontext statt aus dem Gedaechtnis.
**Im Workspace sichtbar:** Verlauf der Unterhaltung, Anhaenge und Dateibezuege, nachvollziehbare Zwischenschritte bei komplexeren Anfragen damit bleibt nachvollziehbar, *wie* ein Ergebnis zustande kam.
**Business-Nutzen:**
- Schnellere Erstentwuerfe fuer Mails, Konzepte und Entscheidungen
- Weniger Rueckfragen durch besser strukturierten Kontext
- Hoehere Vertrauenswuerdigkeit durch nachpruefbare Herkunft von Inhalten
> **BILD-PROMPT (Nano Banana Pro):**
> Gestalte eine abstrakte Chat-UI-Illustration mit links-rechts angeordneten Nachrichtenblasen, Quellensymbolen und einem dezenten Sprachsymbol fuer Voice-Interaktion. Keine echten Markenlogos. Design modern, klar, professionell. Helle Flaechen mit blauen Akzenten (#1976d2), leichte Tiefenwirkung. Kein Text im Bild. 16:9.
---
**5 von 16**
## Seite 5 - Arbeitsbereich 2: Editor
*Schnelligkeit der KI mit Ihrer letzten Freigabe.*
Im Editor werden KI-Vorschlaege nicht blind uebernommen, sondern kontrolliert geprueft. Aenderungen erscheinen im direkten Vergleich (Vorher und Nachher). Sie entscheiden pro Abschnitt oder gesamt: annehmen, ablehnen oder nachjustieren analog zu professionellen Review-Prozessen in Recht, Compliance oder Technikredaktion.
**Warum das strategisch relevant ist:** Unternehmen wollen Tempo *und* Kontrolle. Der Editor verbindet beides: KI liefert Vorschlaege in grosser Geschwindigkeit, Ihre Organisation behaelt die letzte Instanz. Das senkt das Risiko ungewollter Formulierungen oder inhaltlicher Fehler in nach aussen gerichteten Dokumenten.
**Fuer wen besonders wertvoll:** Fachbereiche mit verbindlichen Texten (Vertraege, Richtlinien, Angebote), Projektleitungen mit Spezifikationen, Qualitaetssicherung und alle Teams, die wiederkehrend aehnliche Dokumente anpassen muessen.
**Mehrstufige Aufgaben:** Fuer umfangreichere Bearbeitungen kann die KI in einem gefuehrten Ablauf mehrere Schritte vorschlagen stets mit der Moeglichkeit, vor der Uebernahme zu pruefen. So bleibt Effizienz mit Governance vereinbar.
**Business-Nutzen:**
- Schnellere Bearbeitung von Dokumenten und Fachtexten bei gleichzeitiger Freigabe-Logik
- Weniger Korrekturschleifen durch klare Sicht auf jede Aenderung
- Skalierbare Qualitaet bei Standarddokumenten und Vorlagen
> **BILD-PROMPT (Nano Banana Pro):**
> Erstelle eine elegante Side-by-Side-Editor-Illustration mit Vorher-Nachher-Ansicht, farblich markierten Aenderungen und klaren Aktionsflaechen fuer Accept/Reject. Stil: clean enterprise software concept art, isometrisch oder halb-isometrisch. Dunkles Editorpanel kombiniert mit hellem UI-Rahmen. Primaerblau #1976d2, Akzentgruen und Rot sehr dezent. Kein Text im Bild. 16:9.
---
**6 von 16**
## Seite 6 - Arbeitsbereich 3: Datenquellen
*Ihre Systeme werden zum nutzbaren Wissensraum.*
PowerOn Desktop verbindet bestehende Systeme mit dem Arbeitskontext Ihrer Teams. Typische Anbindungen umfassen etwa Microsoft 365 (SharePoint, OneDrive, Outlook, Teams), Google (Drive, Gmail), Ticketsysteme wie Jira oder ClickUp, sowie FTP und branchenspezifische Fachsysteme jeweils dort, wo Ihre Organisation bereits arbeitet.
**Zwei praktische Ebenen:** Zum einen **persoenliche Quellen** des Nutzers (z. B. eigene Cloud-Bereiche), zum anderen **Quellen der konkreten Workspace-Instanz** und mandantenbezogene Daten immer abgestimmt auf Ihre Rollen- und Freigaberegeln. Zusaetzlich koennen Dateien direkt im Workspace abgelegt, strukturiert und fuer die KI-Nutzung bereitgestellt werden (inkl. Drag-and-Drop).
**Der Effekt fuer den Alltag:** Statt Informationen manuell zu suchen, zusammenzukopieren und in einen Chat zu pasten, entsteht ein **durchsuchbarer Wissensraum**. Die KI bezieht sich auf Inhalte, die Sie bewusst freigegeben haben nicht auf ein undurchsichtiges „Internet-Gedaechtnis“.
**Business-Nutzen:**
- Entscheidungen und Antworten basieren auf *Ihren* Unterlagen, nicht auf Vermutungen
- Deutlich weniger Medienbrueche und Copy-Paste zwischen Systemen
- Schnellere Einarbeitung neuer Mitarbeitender durch einen klaren, gebundenen Wissenszugang
- Weniger Risiko veralteter oder falscher Versionen, weil der Bezug zur Quelle erhalten bleibt
> **BILD-PROMPT (Nano Banana Pro):**
> Visualisiere mehrere Unternehmensdatenquellen als abstrahierte Knoten (Dokumente, Cloud, Tickets, Mail) die in einen zentralen AI-Workspace-Hub fliessen. Zeige Struktur und Ordnung statt Chaos. Stil modern, isometrisch, B2B-Marketing. Farben: Blau #1976d2, Gruen fuer externe Quellen, Violett fuer Feature-Daten, neutraler Hintergrund. Kein Text im Bild. 16:9.
---
**7 von 16**
## Seite 7 - Arbeitsbereich 4: Workflows und Automation
*Standardisierte Ablaeufe transparent ausgefuehrt.*
Wiederkehrende Aufgaben werden als Workflows **einmal** sinnvoll definiert und danach **zuverlaessig** ausgefuehrt manuell gestartet oder nach Plan. Das ist besonders relevant fuer wiederkehrende Reports, Datenaufbereitungen, Qualitaetschecks oder vorbereitende Schritte vor menschlicher Freigabe.
**Transparenz statt Blackbox:** Laufende und abgeschlossene Ausfuehrungen sind nachvollziehbar dokumentiert (Live-Logs und Status). Fuehrungskraefte sehen, *dass* und *wie* Automatisierung laeuft wichtig fuer Vertrauen und interne Kontrolle.
**Rueckkopplung in den Workspace:** Ergebnisse aus Workflows werden nicht „irgendwo abgelegt“, sondern koennen als neuer Kontext fuer Chat, Editor und weitere Schritte dienen. So schliesst sich der Kreis von ad-hoc-Arbeit und standardisierten Ablaeufen.
**Business-Nutzen:**
- Hoehere Prozessgeschwindigkeit bei gleichbleibender Qualitaet und weniger manuellen Fehlern
- Entlastung von Teams bei Routinethemen; mehr Kapazitaet fuer Urteils- und Beziehungsarbeit
- Bessere Skalierung ueber Teams, Standorte und Zeitzonen hinweg
- Einheitliche Standards statt Inselloesungen („jeder macht es anders“)
> **BILD-PROMPT (Nano Banana Pro):**
> Erstelle eine moderne Workflow-Illustration mit mehreren Prozessstufen, KI-Knoten und Rueckkopplung in ein zentrales Dashboard. Zeige klare Richtungspfeile, modulare Bausteine und Statusindikatoren. Stil: clean, enterprise, minimalistisch-isometrisch. Farbpalette mit Blau #1976d2 und Tuerkis-Akzenten. Kein Text im Bild. 16:9.
---
**8 von 16**
## Seite 8 - USP: Intelligente Wissenssuche in drei Ebenen
*Das passende Wissen zur richtigen Zeit ohne Rauschen.*
Stellen Sie sich drei **Schubladen** vor, die Ihre Organisation ohnehin kennt nur dass sie hier technisch sauber getrennt und fuer die KI nutzbar gemacht werden:
- **Persoenlich:** Notizen, Entwuerfe und Dateien, die dem einzelnen Nutzer zuordenbar sind und nicht automatisch das ganze Team exponieren.
- **Team / Instanz:** Alles, was zu einem konkreten Projekt, einem Mandat-Workspace oder einer definierten Arbeitsgruppe gehoert der gemeinsame Tisch fuer diesen Use Case.
- **Mandat / Unternehmen:** Von der Organisation freigegebenes Wissen (Richtlinien, Vorlagen, Standards), das breiter aber weiterhin regelkonform genutzt werden darf.
**Warum das mehr ist als „eine grosse Datenbank“:** Bei jeder Anfrage wird der **sinnvolle Ausschnitt** aus diesen Ebenen zusammengefuehrt. Antworten werden relevanter, Rauschen sinkt, und Sie vermeiden das typische Problem generischer KI-Tools: zu viel oder zu wenig Kontext, falsch gemischt.
**Fuer die Geschaeftsfuehrung:** Das Modell spiegelt reale Verantwortlichkeiten wider (individuell, teambezogen, unternehmensweit). So laesst sich KI-Nutzung **governancetauglich** erklaeren und auditieren statt als undifferenzierte „alles-in-einen-Topf“-Loesung.
**Das Ergebnis im Alltag:** Schnellere, treffsichere Antworten, weniger irrelevante Treffer, klarere Grenzen zwischen privatem Arbeitskontext und geteiltem Wissen.
> **BILD-PROMPT (Nano Banana Pro):**
> Gestalte eine abstrakte 3-Ebenen-Architektur als konzentrische Kreise oder gestapelte Ebenen: personal, team-instance, mandate-enterprise. Daten sollen von unten nach oben intelligent selektiert werden. Premium-SaaS-Look, klare Geometrie, moderne Schattierung. Blau #1976d2 als Hauptfarbe, Tuerkis und Violett fuer Ebenenunterscheidung. Kein Text im Bild. 16:9.
---
**9 von 16**
## Seite 9 - USP: Privacy Shield
*Sensibel bleibt sensibel auch mit KI.*
Datenschutz wird nicht erst in der Rechtsabteilung „nachgebessert“, sondern direkt im Arbeitsprozess verankert. **Privacy Shield** steht fuer eine kontrollierte Vorverarbeitung: Personenbezogene und besonders sensible Angaben (z. B. Namen, Kontaktdaten, typische Identifikatoren) koennen **vor** der eigentlichen KI-Verarbeitung geschuetzt werden, sodass weniger Rohdaten nach aussen gelangen.
**Was das praktisch bedeutet:** Teams arbeiten weiter mit echten Inhalten im Workspace. Fuer die Verarbeitung durch externe oder interne Modelle werden nur die Teile genutzt, die Sie policykonform freigeben. Ergebnisse bleiben dennoch inhaltlich nutzbar, weil die Zuordnung im geschuetzten Umfeld wiederhergestellt werden kann ohne dass der Nutzer jedes Mal manuell anonymisieren muss.
**Gespraech mit Datenschutz und Compliance:** Sie koennen zeigen, *welche* Kategorie von Daten geschuetzt wird, *wann* das greift und *wer* welche Freigaben hat. Das erhoeht die Akzeptanz bei Datenschutzbeauftragten, Arbeitnehmervertretungen und Kunden mit strengen Auflagen.
**Business-Nutzen:**
- KI-Einsatz auch dort moeglich, wo sensible Inhalte allgegenwaertig sind (HR, Kundenakten, Vertraege)
- Geringeres regulatorisches und Reputationsrisiko bei schneller Pilotierung
- Hoeheres Vertrauen von Vorstand, Aufsicht und externen Pruefern
- Weniger „Schatten-KI“, weil der offizielle Weg sicher genug ist, um genutzt zu werden
> **BILD-PROMPT (Nano Banana Pro):**
> Erzeuge eine abstrakte Cyber-Security-Illustration mit einem Schutzschild zwischen Datenstrom und KI-Kern. Zeige, dass sensible Daten vor Verarbeitung geschuetzt werden. Stil: clean, modern, enterprise trust visual. Keine Bedrohungs-Optik, sondern kontrollierte Sicherheit und Governance. Farben: Blau #1976d2, Tuerkis, dezentes Silber/Grau. Kein Text im Bild. 16:9.
---
**10 von 16**
## Seite 10 - USP: Mandanten-Isolation
*Klare Grenzen technisch abgebildet, nicht nur organisatorisch gewuenscht.*
Jeder **Mandant** sei es ein Kunde, eine Tochtergesellschaft oder eine klar abgegrenzte Organisationseinheit arbeitet in einem **eigenen, logisch getrennten Datenraum**. Daten und Wissensbestaende vermischen sich nicht zwischen Mandanten, selbst wenn dieselbe Plattform genutzt wird.
**Typische Traeger dieser Anforderung:** Treuhand und Revision, Beratung mit mehreren Auftraggebern, Konzerne mit strikten Firewalls zwischen Sparten, sowie jede Organisation, die **Vertraulichkeit** als Verkaufsargument oder gesetzliche Pflicht versteht.
**Need-to-know auf Plattform-Ebene:** Nutzer sehen nur, was ihre Rolle und ihr Mandat erlauben. Das unterstuetzt interne Kontrollsysteme und erleichtert die Kommunikation mit externen Pruefern: Trennung ist nicht nur organisatorisch gewuenscht, sondern **technisch abgebildet**.
**Skalierung ohne Grenzverlust:** Neue Mandanten oder neue Projekte lassen sich hinzufuegen, ohne bestehende Sicherheits- und Vertraulichkeitsmodelle zu verwaessern. Das ist ein Wachstumshebel fuer Dienstleister und fuer Konzerne mit komplexer Struktur.
**Business-Nutzen:**
- Eignung fuer Multi-Client- und Multi-Brand-Setups ohne Datenvermischung
- Deutlich reduziertes Risiko von Vertraulichkeitsverletzungen und „falschen“ Zugriffen
- Bessere Argumentationsgrundlage gegenueber Kunden, die Trennschaerfe verlangen
- Kontrollierbares Wachstum: mehr Nutzung, nicht mehr Risiko pro Nutzer
> **BILD-PROMPT (Nano Banana Pro):**
> Visualisiere mehrere sauber getrennte Datenbereiche als leuchtende, voneinander isolierte Cluster oder Glas-Container, jeweils mit eigenem Zugangspfad. Zeige Ordnung, Trennung und Sicherheit in einer modernen Enterprise-Aesthetik. Farben: Blau #1976d2 als verbindendes System, unterschiedliche Akzentfarben pro Mandant. Kein Text im Bild. 16:9.
---
**11 von 16**
## Seite 11 - USP: Datenkontrolle durch den Nutzer
*Sie steuern, welcher Kontext zaehlt ohne stillschweigende Weitergabe.*
Mit **klaren Sichtbarkeitsstufen** entscheiden Nutzer und Rolleninhaber aktiv, welche Inhalte in welchem Kontext fuer die KI nutzbar sind. Es gibt **keine stillschweigende** Weitergabe: Was geteilt wird, wird bewusst eingestellt beim Einbinden von Dateien, Ordnern oder Quellen.
**Die vier Stufen in Klartext:**
- **Persoenlich:** Nur fuer den anlegenden Nutzer sichtbar und nutzbar ideal fuer Entwuerfe und persoenliche Arbeitsunterlagen.
- **Instanzbezogen:** Fuer alle, die Zugriff auf genau diese Workspace-Instanz haben typisch fuer Projekt- oder Teamarbeitsraeume.
- **Mandatsweit:** Fuer die gesamte Mandantenorganisation freigegeben etwa Richtlinien, die jeder mit Mandatszugang nutzen darf.
- **Global (kontrolliert):** Plattformweite Referenzinhalte, typischerweise **stark reglementiert** und oft nur lesend z. B. offizielle Standards, die zentral gepflegt werden.
**Zusaetzliche Hebel:** Inhalte koennen mit einer **Schutz-Option** markiert werden (Vorverarbeitung / Neutralisierung), bevor sie in die KI-Pipeline gehen. Aenderungen an Sichtbarkeit oder Schutz koennen eine Neu-Einordnung im Wissensindex erfordern damit bleibt das System konsistent mit Ihren Regeln.
**Warum das Fuehrungskraefte interessiert:** Sie reduzieren **Fehlbedienung** und **Social Engineering** im weitesten Sinne nicht jede Datei landet aus Versehen im falschen Kontext. Datenschutz und Informationsklassifikation werden **operationalisierbar**, nicht nur Policy-Papier.
> **BILD-PROMPT (Nano Banana Pro):**
> Erstelle eine abstrakte Control-Panel-Illustration mit Scope-Umschaltern, Toggle-Elementen und klaren Zugriffsebenen. Fokus auf User-Kontrolle und Transparenz. Stil: reduziertes High-End SaaS Interface Concept, flach-isometrisch, aufgeraeumt. Primaerfarbe Blau #1976d2, Akzente in Tuerkis und Violett. Kein Text im Bild. 16:9.
---
**12 von 16**
## Seite 12 - USP: Multi-Model AI Orchestrierung
*Flexibilitaet im Modellmarkt unter Ihren Freigaben und Richtlinien.*
PowerOn Desktop ist **nicht** an einen einzelnen KI-Anbieter gebunden. Hinter den Kulissen waehlt die Plattform passend zur Aufgabe: mal ein Modell, das besonders gut bei **langer Textarbeit** ist, mal eines fuer **schnelle Antworten**, mal Spezialfaehigkeiten fuer **Bildanalyse** oder **Strukturierung** immer im Rahmen Ihrer Freigaben und Richtlinien.
**Was das fuer Einkauf und IT bedeutet:** Sie vermeiden **Single-Source-Abhaengigkeiten** und behalten Verhandlungsmacht. Wenn ein Anbieter Preise aendert, Qualitaet schwankt oder Verfuegbarkeit leidet, ist die Plattform darauf vorbereitet, **auszuweichen** ohne dass Endanwender sofort umlernen muessen.
**Betrieb und Risiko:** Ausfallsicherheit steigt, weil kritische Pfade nicht von einem einzigen Dienst abhaengen. Gleichzeitig laesst sich **Kosten und Leistung** feiner steuern: teurere Modelle dort, wo der Mehrwert hoch ist; sparsamere Varianten bei einfachen Routinefragen.
**Governance bleibt obenauf:** Welche Modelle wer nutzen darf, bleibt **rollen- und mandantenbezogen** steuerbar Innovation ohne Kontrollverlust.
**Business-Nutzen:**
- Strategische Flexibilitaet in einem sich schnell veraendernden KI-Markt
- Bessere Ergebnisqualitaet, weil Werkzeug und Aufgabe zusammenpassen
- Hoehere Verfuegbarkeit und Resilienz im Tagesbetrieb
- Transparente Kostenlogik statt undurchsichtiger Flatrates ohne Steuerung
> **BILD-PROMPT (Nano Banana Pro):**
> Visualisiere mehrere abstrakte KI-Modelle als unterschiedliche Rechenkerne, die in einen zentralen Orchestrator laufen. Zeige Lastverteilung, Routing und Ausfallsicherheit. Stil: futuristisch, aber business-tauglich, clean und nicht verspielt. Farbschema: Blau #1976d2, Cyan, Violett, dunkler Hintergrund mit sanften Highlights. Kein Text im Bild. 16:9.
---
**13 von 16**
## Seite 13 - USP: Swiss Made, Governance und Compliance
*Innovation mit Leitplanken fuer Vorstand, Aufsicht und Pruefer nachvollziehbar.*
PowerOn Desktop verbindet **Innovationsgeschwindigkeit** mit **klaren Governance-Leitplanken**. Als Schweizer Anbieter adressieren wir den Erwartungsstandard vieler mittelstaendischer und grosser Organisationen: Qualitaet, Verlaesslichkeit und ein angemessener Umgang mit Datenschutz (DSG) und wo relevant DSGVO.
**Was „Governance“ hier konkret heisst:**
- **Rollen und Rechte:** Wer darf welche Features, Datenquellen und KI-Modelle nutzen?
- **Nachvollziehbarkeit:** Welche Schritte und Quellen haben zu einem Ergebnis beigetragen zumindest dort, wo es fuer interne Kontrolle noetig ist?
- **Mandanten- und Instanzlogik:** Klare Grenzen zwischen Organisationen, Projekten und persoenlichem Raum.
- **Betriebsreife:** Kein reines „Labor-Tool“, sondern eine Struktur, mit der sich KI **breit** ausrollen laesst.
**Fuer Vorstand und Aufsicht:** Sie erhalten eine erzaehlbare Geschichte: KI ist eingebettet in Regeln, Trennungen und Freigaben nicht eine anonyme Chat-Box aus dem Internet. Das erleichtert Freigaben, Versicherungs- und Partnerfragen sowie die Zusammenarbeit mit externen Pruefern.
**Business-Nutzen:**
- Verlaessliche Grundlage fuer strategische KI-Programme und Budgetentscheide
- Bessere Auditierbarkeit in sensiblen Bereichen (Finance, Legal, HR, Kundenprojekte)
- Weniger Schatten-KI, weil der offizielle Weg attraktiv *und* sicher ist
- Staerkere Positionierung gegenueber Kunden und Partnern, die Compliance explizit einfordern
> **BILD-PROMPT (Nano Banana Pro):**
> Erstelle eine hochwertige Compliance-Illustration mit Schweizer Referenz (abstrakte Alpenlinie oder dezente Swiss-Form), Security-Symbolen, Audit-Pfaden und Governance-Elementen. Stil: premium corporate, clean, vertrauensvoll, modern. Farben: Blau #1976d2, Weiss, dezentes Rot als kleiner Akzent. Kein Text im Bild. 16:9.
---
**14 von 16**
## Seite 14 - Business Impact
*Zeit, Qualitaet, Skalierung, Compliance messbar adressiert.*
PowerOn Desktop liefert Wirkung in vier strategischen Dimensionen:
**1) Zeitgewinn**
- Schnellere Informationssuche und Entscheidungsvorbereitung
- Weniger Tool-Wechsel und manuelle Zwischenschritte
**2) Qualitaet**
- Konsistentere Ergebnisse durch gemeinsamen Kontext
- Hoehere Nachvollziehbarkeit durch Quellen und Prozesssicht
**3) Skalierung**
- Wiederverwendbare Workflows statt Einzelfallarbeit
- Schnellere Uebertragung von Best Practices zwischen Teams
**4) Compliance**
- Strukturierte Datenkontrolle und klare Rollenlogik
- Bessere Grundlage fuer interne und externe Pruefungen
> **BILD-PROMPT (Nano Banana Pro):**
> Gestalte eine moderne Business-Impact-Illustration mit vier gleichwertigen Saeulen oder KPI-Kacheln: Geschwindigkeit, Qualitaet, Skalierung, Compliance. Zeige positive Dynamik, klare Struktur und Executive-Level-Aesthetik. Farben: Blau #1976d2 dominiert, Tuerkis und Violett als Sekundaerakzente, heller Hintergrund. Kein Text im Bild. 16:9.
---
**15 von 16**
## Seite 15 - So starten Sie
*Vom ersten Gespraech bis zum messbaren Ergebnis in vier Schritten.*
Ein erfolgreicher Einstieg folgt einem klaren, risikoarmen Vorgehen:
1. **Discovery Call (30 Min.)**
Ziele, Prioritaeten und kritische Use Cases abstimmen.
2. **Workspace Blueprint**
Datenquellen, Rollen und Governance-Rahmen definieren.
3. **MVP in kurzer Zeit**
Ein produktiver Kern-Use-Case mit messbarem Ergebnis.
4. **Scale-Up**
Weitere Teams, Prozesse und Automationen schrittweise ausrollen.
Dieser Ansatz schafft schnelle Erfolge ohne strategische Ueberdehnung.
> **BILD-PROMPT (Nano Banana Pro):**
> Erzeuge eine visuelle Roadmap mit vier klaren Etappen von links nach rechts: Discover, Blueprint, MVP, Scale. Nutze abstrakte Milestones, verbindende Linien und Fortschrittsdynamik. Stil: hochwertig, clean, enterprise consulting look. Farben: Blau #1976d2, Tuerkis-Akzente, viel Luft und Ordnung. Kein Text im Bild. 16:9.
---
**16 von 16**
## Seite 16 - Kontakt und Team
*Ihr Einstieg in PowerOn Desktop persoenlich und strukturiert.*
**PowerOn AG**
Birmensdorferstrasse 94, 8003 Zuerich (CH)
[www.poweron.swiss](https://www.poweron.swiss)
**Team**
- Patrick Motsch - CEO/CTO
- Ida Dittrich - Product Architect
- Stephan Schellworth - Business Integration
Wenn Sie KI im Tagesgeschaeft produktiv und kontrolliert verankern wollen, starten wir mit einem klaren ersten Schritt.
> **BILD-PROMPT (Nano Banana Pro):**
> Gestalte ein minimalistisches, professionelles Abschlussvisual fuer ein B2B-Pitchdeck: abstraktes Team-/Unternehmensmotiv mit einem zentralen Hub, verbundenen Punkten und vertrauensvoller Corporate-Atmosphaere. Stil clean, modern, hochwertig, nicht verspielt. Farbpalette: Blau #1976d2, Weiss, dezentes Grau, leichter Tuerkis-Akzent. Kein Text im Bild. 16:9.
---
## Keywords / Tags
PowerOn Desktop, AI Workspace, intelligente Wissenssuche, Datenschutz, Privacy Shield, Mandanten-Isolation, Datenkontrolle, Multi-Model AI, Workflow Automation, C-Level KI-Strategie, DSG, DSGVO, Swiss Made, Governance, Compliance, Datenquellen, Enterprise SaaS

View file

@ -1,217 +0,0 @@
# Case Study (Illustration / Template): PowerOn Launch48
**Wichtig:** Das **primaere Kundenangebot** zum Weitergeben ist **[poweron-launch48-offer.md](./poweron-launch48-offer.md)** verstaendlich fuer Management und Fachbereiche.
**Dieses Dokument** dient **nicht** als erstes Verkaufs-PDF: Es ist ein **Beispiel-Verlauf / Pilot-Template** zur Vertiefung, sobald Sie Referenzgeschichten brauchen. Alle **Kundendaten, Branche und Kennzahlen** sind **fiktiv oder anonymisiert**, bis ein reales Projekt mit schriftlicher Freigabe vorliegt.
**Referenz fuer Liefermethodik:** AI-augmented Engineering (vergleichbar der dokumentierten **Abraxas DATA Hub Migration** Kundennennung in oeffentlichen Materialien nur mit Freigabe).
---
## Executive Summary
**Ausgangslage (Beispiel):** Ein **mittelstaendisches Dienstleistungsunternehmen** (anonymisiert) hatte wiederkehrende **Kundenanfragen zu Vertrags- und Leistungsinhalten**, die heute aus **PDF-Handbuechern, E-Mail-Templates und internen Notizen** manuell beantwortet werden. Die Bearbeitungszeit pro Anfrage war hoch, die Qualitaet von der Erfahrung der jeweiligen Person abhaengig.
**PowerOn** fuehrte mit dem Paket **Launch48** einen **48-Stunden-Block** auf der **PowerOn-Plattform** durch. Ergebnis (Zielbild des Templates): **ein produktiv einsetzbarer KI-Assistent** mit angebundenen **internen Quellen**, definierter **Pilotgruppe** und **vereinbarten Erfolgszielen** fuer die zweite Zahlungsstufe.
**Kernresultat (Illustration):** Von **Kickoff** bis **Uebergabe** **2 Arbeitstage** intensiver Umsetzung; schneller **Mehrwert im Pilot** statt monatelanger Vorlauf; laufende **Pruefung durch Ihre Fachexpertinnen und Experten** fuer Qualitaet und Compliance.
---
## Projekteckdaten
| Aspekt | Detail (Template / anonymisiert) |
| --- | --- |
| **Kunde** | Anonymisiertes Dienstleistungsunternehmen, deutschsprachige Schweiz |
| **Plattform** | PowerOn (Power Desktop / AI Workspace, Datenquellen, Automation) |
| **Use-Case** | Erstbeantwortung und Strukturierung von wiederkehrenden Fachanfragen aus freigegebenen internen Unterlagen |
| **Sprint-Dauer** | 48 Stunden gebundene Umsetzung (plus Vorlauf fuer Gates) |
| **Umfang** | 1 Agent/Workflow, 3 Wissensquellen (Beispiel), 1 Integration (Beispiel: internes Ticket-Read) |
| **PowerOn Team** | Patrick Motsch (Technische Leitung), Ida Dittrich (Architektur), Stephan Schellworth (Projektsteuerung) |
| **Kunde Team** | Fach-Owner, IT-Ansprechpartner, 10 Pilotnutzer (Beispiel) |
---
## Die Herausforderung
### Fachliche und technische Ausgangslage
- **Wissen verteilt** in PDFs, geteilten Ablagen und persoenlichen Entwuerfen.
- **Kein einheitlicher Erstkontakt**: Mitarbeitende formulieren Antworten neu Inkonsistenz und laengere Durchlaufzeiten.
- **Datenschutz**: Kundenbezogene Details duerfen nicht in generische KI-Tools ohne Kontrolle.
### Business Impact der Ausgangslage
- Hoher **Zeitaufwand** pro Standardanfrage.
- **Skalierungsbremse** bei Wachstum (Onboarding neuer Mitarbeitender).
- **Risiko** unterschiedlicher Antwortqualitaet und laengerer Reaktionszeiten.
---
## Der PowerOn-Ansatz
### AI-Augmented Delivery auf der Plattform
PowerOn setzt auf **Human-in-the-Loop**: KI beschleunigt Aufbau und Iteration, **Architektur und Freigaben** bleiben beim erfahrenen Team und beim Kunden.
**Kernprinzip:** Schnelligkeit durch KI-gestuetzte Umsetzung, **Qualitaet** durch Reviews, **Governance** durch PowerOn-Faehigkeiten (Quellen, Rollen, nachvollziehbare Ablaeufe).
### Phase 1: Use-Case & Impact (Vorlauf + Sprint-Start)
**Aktivitaeten:**
- Priorisierung eines **einzigen** Kernprozesses.
- Definition von **3 KPIs** (z. B. Zeit pro Vorgang, Pilot-Zufriedenheit, Fehlerindikator).
- Scope-Freeze fuer den Fixpreis.
**Deliverables:**
- Schriftliche **Scope- und KPI-Spezifikation**.
- **Go/No-Go** nach Compliance-Freigabe.
### Phase 2: Wissensbasis
**Aktivitaeten:**
- Anbindung von **Handbuch-PDFs**, **FAQ-Dokument** und **freigegebenem SharePoint-Ordner** (Beispiel).
- Zuordnung zu **Instanz-/Mandantenlogik** gemaess Rollen.
**Deliverables:**
- Indexierte Quellen im PowerOn-Workspace.
- Kurz-Dokumentation, welche Inhalte **nicht** im Agent-Kontext liegen (Grenzen).
### Phase 3: Tools & Anbindung
**Aktivitaeten:**
- **Eine** Integration im vereinbarten Umfang: z. B. **Lesen** von Ticket-Metadaten fuer Kontext (kein Schreiben in Produktion im Template-Beispiel).
- Festlegung von **Freigaben** und Testfaellen.
**Deliverables:**
- Funktionsfaehiger Integrationspfad in der Pilotumgebung.
- Testprotokoll (Grundfaelle).
### Phase 4: 48h Build-Sprint
**Aktivitaeten:**
- Gemeinsame Umsetzung mit **Pairing** (Kunde + PowerOn).
- Iterative Tests mit **realistischen Anfragen**.
- **Runbook** und **Handover**.
**Deliverables:**
- **Einsatzbereiter Agent/Workflow** im Pilot.
- **Runbook** + Enablement-Session.
---
## Execution Das Herzstueck
1. **Strukturierte Zielvorgaben** aus Phase 13.
2. **Plattformnahe Umsetzung** (kein „einmaliger Skript-Hack“ ausserhalb des Betriebsmodells).
3. **Validierung** durch Fach-Owner und Architektur-Review.
4. **Test mit Pilotnutzern** vor KPI-Messfenster.
**Effizienzgewinn (Illustration):** Statt mehrwoechiger interner Experimentierphase entsteht in **48 Stunden** ein **abnahmefaehiger** Pilot mit klarer Messgroesse.
---
## Testing & Uebergabe
- **Pilotgruppe** (z. B. 10 Nutzer) fuer **10 Arbeitstage** nach Uebergabe.
- **Sammelfeedback** und kleine Nachjustierungen im vereinbarten Rahmen (optional als Zusatzleistung klaeren).
- **Auswertung der Erfolgsziele** zum vertraglichen Stichtag → Basis fuer die **CHF 7000**-Komponente von **Launch48**.
**Enablement:** Das Ziel ist **Autonomie**: Internes Team versteht Grenzen, Bedienung und Eskalationspfad analog zur **Enablement-Philosophie** bei groesseren PowerOn-Projekten (vgl. Wissenstransfer in der **Abraxas**-Methodendokumentation, sofern intern referenziert).
---
## Business Impact (Illustrationsbandbreiten)
*Hinweis: Zahlen erst mit echtem Projekt ersetzen.*
| Dimension | Illustrative Aussage |
| --- | --- |
| **Time-to-Value** | Produktiver Pilot in **Tagen** statt **Monaten** |
| **Zeit pro Vorgang** | Ziel z. B. **2540 %** Reduktion nach Baseline |
| **Qualitaet** | Weniger Streuung durch einheitliche Wissensbasis |
| **Risiko** | Weniger Shadow-AI durch **freigegebene** Plattform |
---
## Lessons Learned (generisch, aus Sprints dieser Art)
1. **Scope schlaegt Feature-Wunschliste** ein scharfer Use-Case traegt KPIs.
2. **Gates sparen Zeit** Compliance und Zugang vor dem Sprint klaeren.
3. **Human-in-the-Loop** verhindert Halluzinationen im produktiven Kontext.
4. **Runbook ist Produkt** ohne Dokumentation sinkt Adoption.
| Herausforderung | Loesung |
| --- | --- |
| Unklare Verantwortung Fach/IT | Zwei benannte Owner von Tag 1 |
| Zu grosse Wissensmenge | Priorisierte Quellen, spaetere Erweiterung |
| Integration komplexer als gedacht | Frueh Spike oder Scope auf „read-only“ reduzieren |
---
## Technische Details (Beispiel-Stack)
**PowerOn:**
- Power Desktop / AI Workspace
- Datenquellen (z. B. SharePoint, Uploads)
- Automation / Workflow (je nach Use-Case)
- Rollen und Sichtbarkeit gemaess Organisationsmodell
**Optional erwaehnt im echten Case:**
- Spezifische Modelle/Provider nur nach Kundenfreigabe dokumentieren.
---
## Projektorganisation
| Meilenstein | Zeit (Beispiel) |
| --- | --- |
| Kickoff & Gates | Woche -1 |
| Sprint Tag 1 | z. B. Do |
| Sprint Tag 2 | z. B. Fr |
| Handover | Ende Tag 2 |
| KPI-Messfenster | 10 Arbeitstage |
| Auswertung | Stichtag laut Vertrag |
---
## Verbindung zur Abraxas-Methodik (interner Verweis)
Die **Abraxas DATA Hub Migration** zeigte: **strukturierte Analyse**, **Architekturentscheide mit Review**, **KI-gestuetzte Execution** und **Enablement** liefern **hohe Geschwindigkeit bei produktionsreifer Qualitaet**. **Launch48** uebertraegt diese Prinzipien auf **kleinere, scharf umrissene KI-Piloten** auf der **PowerOn-Plattform** mit **Fixpreis** und **vereinbarten Erfolgszielen** fuer die zweite Zahlungsstufe.
*Oeffentliche Zitate oder Logos von Abraxas nur mit schriftlicher Freigabe.*
---
## Fazit
**Launch48** macht aus einem konkreten Alltags-Engpass einen **messbaren Piloten** auf **PowerOn** schnell, mit klaren Leitplanken und ohne Monatsprojekt-Pflicht. Nach dem ersten echten Kundenprojekt: dieses Template durch **verifizierte Kennzahlen**, **Zitate** und **freigegebenen Namen** ersetzen.
---
## Freigaben (Checkliste legal / kommerziell)
- [ ] Entscheid: Duerfen wir **Abraxas** als Referenz **namentlich** nennen?
- [ ] Entscheid: Duerfen wir **diesen** Pilot-Kunden nennen?
- [ ] Template-Kennzeichnung auf Website/PDF: **„Beispielszenario“** bis zur Finalversion.
- [ ] KPI-Formulierungen von Recht/Finance geprueft.
- [ ] Screenshots nur mit anonymisierten Daten.
---
## Referenzen
- **Kundenangebot (primaer):** [poweron-launch48-offer.md](./poweron-launch48-offer.md)
- Konzept (intern): [concept-poweron-48h-agent-offer.md](./concept-poweron-48h-agent-offer.md)
- Flyer: [flyer-poweron-48h-agent.md](./flyer-poweron-48h-agent.md)
- Plattform-Ueberblick: [product-teaser-poweron.md](./product-teaser-poweron.md)
- PowerOn Desktop Story (Marketingtiefe): [case-study-power-desktop.md](./case-study-power-desktop.md)

View file

@ -1,246 +0,0 @@
# PowerOn Launch48 Konzeptdokument (intern)
*Produktisiertes Angebot: KI auf PowerOn in 48 Stunden (Fixpreis, Erfolgsziele gestaffelt)*
**Kundenfaehiges Angebot zum Teilen:** [poweron-launch48-offer.md](./poweron-launch48-offer.md)
**Version:** 1.1 (Entwurf zur internen Freigabe)
**Bezug:** [product-teaser-poweron.md](./product-teaser-poweron.md), [case-study-power-desktop.md](./case-study-power-desktop.md)
---
## 1. Elevator Pitch
Viele Teams verlieren Kapazitaet an wiederkehrende Routine, waehrend KI-Piloten in Einzeltools haengen bleiben ohne klare Datenhoheit und ohne messbaren Betriebsmehrwert. **Launch48** ist ein **48-Stunden-Sprint** auf der **PowerOn Enterprise-KI-Orchestrierungsplattform**: Gemeinsam mit Ihren und unseren Entwickler\*innen entsteht **ein konkreter, produktiv nutzbarer KI-Agent** (inkl. definierter Wissensbasis und Systemanbindung im vereinbarten Umfang). **Fixpreis CHF 9000**, aufgeteilt in **CHF 2000** bei Vertragsstart und **CHF 7000** bei Erreichen **vorab definierter KPIs** Ergebnis und Scope sind schriftlich fixiert, nicht „Stunden ohne Ende“.
---
## 2. Category-Zeile (Marketing)
**„Launch48: In 48 Stunden von Use-Case zu produktivem KI-Agenten auf PowerOn mit messbarem Erfolg.“**
Alternativ techniknaeher (IT-Persona): **„Orchestrierter 48h-Sprint: Agent, Datenkontext und Integration auf Ihrer PowerOn-Instanz.“**
---
## 3. Ideal Customer Profile (ICP) und Ausschluss
### 3.1 ICP
- **Organisationen** in der Schweiz / DACH: KMU, Mittelstand, Software-Haeuser, Dienstleister mit wiederkehrenden Wissens- oder Verarbeitungsprozessen.
- **Ein klarer Kern-Use-Case** mit greifbarem Input/Output (z. B. Anfragebeantwortung aus internen Unterlagen, Vorbereitung standardisierter Antworten, erste Stufe Qualitaets-/Plausibilitaetschecks, strukturierte Extraktion aus definierten Dokumenten).
- **Bereitschaft**, technische Ansprechpartner, Testdaten und **Zugang zu den vereinbarten Quellen/Systemen** waehrend des Sprints bereitzustellen.
- **PowerOn** als Zielplattform akzeptiert (oder Pilot-Instanz wird fuer den Sprint bereitgestellt).
### 3.2 Ausschluss (kein Launch48 ohne Anpassung)
- Reine **Strategie-Workshops** ohne System- und Datenzugang.
- **„KI fuer alles“** ohne priorisierten Use-Case.
- Erwartung einer **vollstaendigen Unternehmens-Transformation** in 48 Stunden.
- Use-Cases mit **hochreguliertem Alleingang** ohne vorherige Compliance-/Datenschutz-Freigaben (Sprint verschieben bis Gate erfuellt).
---
## 4. Differenzierung (Orientierung am Markt)
| Aspekt | Typischer „AI-Hackathon / One-Day-Agent“-Stil | **Launch48 (PowerOn)** |
| --- | --- | --- |
| **Dauer** | Oft 1 Tag vor Ort / komprimiert | **48 Stunden** gebuegelter Sprint inkl. Vorbereitung und Uebergabe |
| **Traeger** | Oft generisch / tool-offen | **PowerOn-Plattform**: Workspace, Datenquellen, Automation, Governance |
| **Daten & Privacy** | hauefig implizit | **Explizit**: Mandantenlogik, Sichtbarkeitsstufen, Privacy-Shield-Ansatz (siehe Desktop-Story) |
| **Lieferobjekt** | „funktionale KI-Loesung“ (breit) | **Ein Agent/Workflow** im definierten Scope + Runbook + Enablement |
| **Preislogik** | variabel | **Fixpreis CHF 9000**, **CHF 7000** an **messbare KPIs** gekoppelt |
| **Beweis** | Referenzen variabel | **Methoden-Proof:** u. a. AI-augmented Delivery (z. B. Abraxas DATA Hub Migration siehe separate Case Study; Nennung nur mit Kundenfreigabe) |
---
## 5. Vier Phasen (PowerOn-spezifisch)
Die Phasen sind inhaltlich mit gaengigen „Ideation → Data → Integration → Build“-Modellen vergleichbar, aber **konkret auf PowerOn** gebaut:
1. **Use-Case & Impact** Priorisierung eines Szenarios mit hohem Business-Impact; Definition von **Erfolgskriterien und KPIs**; Abgrenzung In-/Out-of-Scope.
2. **Wissensbasis** Aufbau/Anbindung der vereinbarten **Dokumente und Datenquellen** im PowerOn-Kontext (persoenlich / Instanz / Mandat gemaess Rollenmodell).
3. **Tools & Anbindung** Auswahl und Umsetzung der **optimalen Integrationen** (z. B. APIs, konfigurierte Quellen, Automation-Trigger) im vereinbarten Rahmen; Freigaben und Berechtigungen.
4. **Build-Sprint (48h)** Gemeinsame Umsetzung mit **Pairing** zwischen Kunden- und PowerOn-Team; Reviews, Tests, Uebergabe.
---
## 6. Scope-Grenzen (Vorschlag zur internen Finalisierung)
*Die folgenden Groessen ermoeglichen einen verteidigbaren Fixpreis. Zahlen intern verbindlich festlegen und im Angebot/Vertrag ersetzen.*
### 6.1 Im Standard-Scope (empfohlen)
| Parameter | Vorschlag | Hinweis |
| --- | --- | --- |
| **Hauptlieferobjekt** | 1 **Agent bzw. 1 klar definierter Workflow/Automation** auf PowerOn | Erweiterung = Change Request |
| **Workspace** | 1 **PowerOn-Instanz** bzw. 1 Mandanten-Workspace | Mehr Instanzen = Zusatz |
| **Wissensquellen** | Bis **3 Quellen** (z. B. SharePoint-Bibliothek, definierter Ordner, CSV/FAQ-Dokumente) | „Quelle“ = fachlich abgegrenztes Bundle |
| **Dokumentenvolumen (indikativ)** | Bis ca. **500 MB** indexierbarer Inhalt **oder** bis ca. **2000** Seiten-Aequivalent | Grobmasstab; technische Validierung im Gate |
| **Integrationen** | **1** zusaetzliche Systemanbindung im vereinbarten Umfang (z. B. ein REST-Webhook, ein definierter Connector) | Komplexe ERP-Tiefenintegration oft ausserhalb |
| **Nutzer-Pilotgruppe** | Bis **15** aktive Testnutzer fuer KPI-Messung | Skalierung danach |
| **Enablement** | **1** Live-Handover (6090 Min.) **oder** Kurzvideo (30 Min.) + **Runbook** (Markdown/PDF) | |
### 6.2 Ausserhalb Standard-Scope (Zusatzangebot)
- Mehrere unabhaengige Use-Cases parallel.
- Umfangreiche Individualentwicklung ausserhalb PowerOn-Standardfeatures.
- Produktions-HA/DR, rechtliche Due-Diligence, vollstaendige Penetrationstests.
- Schulung der gesamten Belegschaft.
### 6.3 Vor-Sprint-Gates (Go/No-Go)
Vor Start **CHF 2000**-Phase muessen erfuellt sein:
- [ ] Geschaeftlicher **Use-Case Owner** benannt.
- [ ] **Technischer Ansprechpartner** mit Berechtigung fuer Testsystem oder Pilot.
- [ ] **Liste der Quellen** und Freigabe durch Datenschutz/Compliance (falls noetig).
- [ ] **KPI-Set** schriftlich unterschrieben (siehe Kapitel 8).
- [ ] Zugang zu PowerOn-Umgebung (Kunde oder PowerOn-Hosting laut Vereinbarung).
---
## 7. 48h-Ablauf (Kalender Beispiel)
**Vorlauf (remote, typisch 35 Arbeitstage vor Sprint):** Kickoff 60 Min., Scope-Freeze, Zugriffe, Testdaten.
| Zeit | Tag 1 | Tag 2 |
| --- | --- | --- |
| Vormittag | Phase 12 Abschluss: Use-Case frozen, Quellen angebunden/indexiert | Phase 4: Integration finalisieren, End-to-End-Tests |
| Nachmittag | Phase 34 Start: Tooling, erste Agent-/Workflow-Version | Pilotlauf mit Testnutzern, Runbook, Handover-Vorbereitung |
**Direkt nach Sprint:** Handover-Termin; **KPI-Messfenster** z. B. **10 Arbeitstage** nach Uebergabe (konfigurierbar).
---
## 8. KPI-Framework fuer CHF 7000
*Im Vertrag: **35 KPIs** waehlen, je eine klare **Messmethode**, **Zielwert**, **Messzeitpunkt**.*
### 8.1 KPI-Katalog (Auswahl)
| ID | KPI (Beispiel) | Messidee | Beispiel-Zielwert |
| --- | --- | --- | --- |
| K1 | **Zeit pro Vorgang** | Zeitstempel Start/Ende in Pilot (oder Ticket-Stichprobe) | ≥ **30 %** Reduktion vs. Baseline (4-Wochen-Durchschnitt) |
| K2 | **Anteil automatisierter Schritte** | Definierte Teilschritte ohne manuellen Eingriff | ≥ **70 %** der Schritte im definierten Prozess |
| K3 | **Fehlerquote / Nacharbeit** | Anzahl Eskalationen oder Korrekturloops pro 100 Vorgaenge | ≤ **X** (Baseline + Schwelle) |
| K4 | **Time-to-First-Answer** | Median bis erste brauchbare Agent-Antwort | ≤ **Y Minuten** |
| K5 | **Pilot-Akzeptanz** | SUS oder interne 15-Befragung nach 2 Wochen | Mittelwert ≥ **4.0** |
| K6 | **Verfuegbarkeit im Pilot** | Uptime der Agent-Instanz in Messfenster | ≥ **99 %** (ausser geplante Wartung) |
### 8.2 Regeln
- **Baseline** vor Sprint dokumentieren (Stichprobe oder Kennzahl aus Reporting).
- Bei **Teilerreichung** optional interne Policy definieren (z. B. gestaffelte Zahlung oder Nachsprint-Paket *nur wenn gewuenscht, rechtlich klaeren*).
- **CHF 7000** faellig bei **Erreichen aller vertraglich definierten KPI-Ziele** zum Messzeitpunkt.
---
## 9. Deliverables (Checkliste)
- [ ] Funktionsfaehiger **Agent/Workflow** im vereinbarten Scope auf PowerOn.
- [ ] Konfigurierte **Wissensquellen** (laut Vertrag).
- [ ] **Integration** (laut Vertrag) inkl. Testnachweis.
- [ ] **Runbook**: Bedienung, Grenzen, Eskalation, bekannte Einschraenkungen.
- [ ] **Enablement**: Session oder Video laut Vertrag.
- [ ] **Uebergabeprotokoll** mit Link auf Testfaelle / Abnahme-Checkliste.
---
## 10. Commercials
| Position | Betrag | Faelligkeit |
| --- | --- | --- |
| **Gesamt Fixpreis** | **CHF 9000** (exkl. MWSt. je nach Vereinbarung) | |
| **Anzahlung** | **CHF 2000** | Bei Vertragsunterzeichnung / Sprint-Freigabe |
| **Erfolgszahlung** | **CHF 7000** | Bei Nachweis der **vereinbarten KPIs** zum Messzeitpunkt |
Zusaetzliche Leistungen: nach **Stunden- oder Paketsatz** gemaess Preisliste.
---
## 11. Risiken und Mitigation
| Risiko | Mitigation |
| --- | --- |
| Schlechte Datenqualitaet | Gate vor Sprint: Stichprobe, Bereinigung, Scope reduzieren |
| Fehlende API-Dokumentation | Frueh Integrations-Spike; sonst manueller Uebergabe-Modus im Scope |
| Compliance verzoegert | Sprint startet erst nach Freigabe; keine parallele „Schatten-Produktion“ |
| Scope Creep | Aenderungen nur per Change Request; Product Owner auf Kundenseite |
| Erwartung „magische KI“ | KPIs und Grenzen im Runbook; Human-in-the-Loop explizit |
---
## 12. Sales Playbook
### 12.1 Discovery-Fragen (Auszug)
1. Welcher **konkrete Vorgang** kostet heute am meisten Zeit pro Woche?
2. Wo liegen die **Quellen** (Systeme, Ordner, Tickets)?
3. Wer ist **Owner** fuer Inhalt und fuer Technik?
4. Welche **Compliance**-Grenzen gelten (DSG, Kundenvertraege)?
5. Wie messen Sie heute **Qualitaet** (Fehlerquote, SLA)?
6. Gibt es eine **Baseline-Zahl** fuer die letzten 4 Wochen?
7. Wie viele **Pilotnutzer** sind realistisch in 2 Wochen?
8. Ist **PowerOn** bereits im Einsatz oder kommt eine Pilot-Instanz?
9. Was passiert bei **Erfolg** Rollout-Plan?
10. Was waere **nicht** im Scope (bewusst ausschliessen)?
### 12.2 Einwaende
- **„Zu schnell / zu guenstig.“** → Fixpreis gilt nur bei fixem Scope; Referenzmethodik AI-augmented Delivery; menschliche Validierung.
- **„Wir haben keine Daten.“** → Mindestens FAQs oder interne Vorlagen reichen oft; sonst kein Launch48.
- **„IT blockiert.“** → Gates und Pilot-Instanz-Option; kleinster sicherer Umfang.
### 12.3 Qualifikations-Scorecard (einfach)
| Kriterium | Punkte (02) |
| --- | --- |
| Klarer Use-Case | |
| Zugang zu Daten bis Sprint-Start | |
| Sponsor auf Fachseite | |
| Tech-Ansprechpartner | |
| KPI denkbar | |
| **Summe ≥ 8** → hohe Prioritaet fuer Angebot |
---
## 13. Marketing-Kit (Verweise)
| Artefakt | Datei |
| --- | --- |
| **Kundenangebot (primaer, teilbar)** | [poweron-launch48-offer.md](./poweron-launch48-offer.md) |
| **4-Folien-Deck (Praesentation)** | [launch48-deck-presentation.md](./launch48-deck-presentation.md) |
| Flyer (2 Seiten, Kurzfassung) | [flyer-poweron-48h-agent.md](./flyer-poweron-48h-agent.md) |
| Case Story (Illustration / Template, nicht Primaerverkauf) | [case-study-poweron-48h-agent.md](./case-study-poweron-48h-agent.md) |
### 13.1 LinkedIn-Posts (Kurzvarianten)
1. **Outcome:** „48 Stunden. Ein Agent. Messbarer Mehrwert. Launch48 auf PowerOn Fixpreis, KPI-gestaffelt.“
2. **IT/Governance:** „KI ohne Daten-Chaos: Launch48 verankert Ihren Agent auf PowerOn mit Quellen, Rollen und klarer Integration.“
3. **Social Proof (nur mit Freigabe):** „Wie bei komplexen Migrationen liefern wir **schnell und review-getrieben** jetzt als 48h-Paket fuer Ihren ersten produktiven Agent.“
---
## 14. Rechtliches und Freigaben (Checkliste)
Siehe [case-study-poweron-48h-agent.md](./case-study-poweron-48h-agent.md) Abschnitt „Freigaben“. Insbesondere:
- [ ] Abraxas- und andere Kundennennung in Marketing freigegeben.
- [ ] AGB/Vertrag fuer KPI-Zahl Klauseln geprueft.
- [ ] Angebotsname **Launch48** Markenpruefung (intern).
---
## 15. Team (Ansprechpartner)
- **Patrick Motsch** Technische Leitung, AI-Strategie
- **Ida Dittrich** Architektur, Plattform, Qualitaet
- **Stephan Schellworth** Projektsteuerung, Business Alignment
**PowerOn AG** [www.poweron.swiss](https://www.poweron.swiss)
---
## Keywords
Launch48, PowerOn, KI-Agent, 48h Sprint, Fixpreis, KPI, Enterprise AI, Orchestrierung, Datenhoheit, Governance, produktisierte Dienstleistung, Schweiz, DACH

Binary file not shown.

View file

@ -1,183 +0,0 @@
# PORTO AI Chat — Feature Slide Deck
Struktur analog zu «20260408 Local LLM.pdf» (6 Folien). Text für PowerPoint / Keynote / PDF-Export.
**Produktname:** PORTO (von PowerOn)
**Feature:** AI Chat (Unified AI Workspace)
---
## Folie 1 — Titelfolie
**Hauptzeile (gross):**
Ihr sicherer KI-Arbeitsplatz.
Chatten, analysieren, automatisieren — in einer Oberfläche.
**Fliesstext:**
PORTO gibt Ihrem Team einen KI-Agenten, der Dokumente versteht, Quellen verbindet und Ergebnisse liefert — ohne Datenabfluss ins Ausland.
**Kernnutzen (3 Bullets):**
- KI-Chat mit Dateizugriff und Dokumentenverständnis (RAG)
- Verbindung zu SharePoint, OneDrive, Google Drive und weiteren Quellen
- Schweizer Datenhaltung; Private LLM optional
**Fusszeile / Zielgruppe:**
Für Treuhand, Legal, Finance und weitere vertrauenssensible Bereiche.
**Logo:** PORTO / PowerOn
---
## Folie 2 — Problem
**Hauptzeile:**
Warum KI-Chat im Unternehmen heute oft an der Realität scheitert
**Fliesstext:**
Viele Organisationen wollen produktiv mit KI chatten — aber sensible Prozesse und fehlender Systemkontext bremsen die Umsetzung:
**Schmerzpunkte (Bullets):**
- Vertrauliche Inhalte landen in öffentlichen Chat-Tools (Copy/Paste-Risiko)
- Standard-Chats haben keinen sicheren Zugriff auf Unternehmensdokumente
- Ergebnisse müssen manuell in Dateien, Mails und Reports übertragen werden
- IT und Compliance verlangen Kontrolle über Modelle und Datenflüsse — Nutzer wollen Geschwindigkeit
**Zwischenüberschrift:**
Die Folgen im Alltag:
**Folgen (kurze Liste):**
- Compliance- und Reputationsrisiko
- Medienbrüche und Doppelarbeit
- Langsame Bearbeitung
- Verzögerte oder uneinheitliche KI-Nutzung
**Abschlusszeile:**
Das Problem ist nicht der Wille zur Innovation. Das Problem ist der fehlende sichere Rahmen für produktiven KI-Chat mit echtem Dokumentenkontext.
---
## Folie 3 — Lösung
**Hauptzeile:**
PORTO von PowerOn bringt sicheren KI-Chat in produktive Abläufe
**Fliesstext:**
PORTO AI Chat verbindet Konversation mit Datenhoheit und Agentenfähigkeit.
**Vier Säulen:**
| Säule | Inhalt |
|-------|--------|
| **Kontextbewusst** | Semantische Wissensabfrage (RAG) über Ihre Dokumente — nicht nur «blind» chatten |
| **Praktisch** | Eine Arbeitsfläche: Chats, Dateien, Datenquellen; Drag & Drop, Vorschau, klare Nachvollziehbarkeit |
| **Aktiv** | KI-Agent mit Tools: lesen, zusammenfassen, strukturierte Inhalte erstellen, Dateien vorschlagen — mit Ihrer Freigabe |
| **Kontrollierbar** | Modellwahl (z. B. OpenAI, Mistral, Private LLM), definierte Quellen, Daten in der Schweiz |
**Mit PORTO nutzen Teams KI so, wie sie gebraucht wird:**
- effizient
- nachvollziehbar
- geschützt
- geeignet für vertrauliche Informationen
**Abschlusszeile:**
So wird aus Zurückhaltung echte Umsetzung.
---
## Folie 4 — Typische Einsatzfelder
**Hauptzeile:**
Typische Einsatzfelder für PORTO AI Chat
**Untertitel:**
Wo PORTO im Alltag den grössten Nutzen bringt — schnell, verständlich, messbar.
**Vier Kacheln (je: Problem | Mit PORTO | Nutzen):**
### 1. Dokumentenanalyse & Prüfung
- **Problem:** PDFs, Verträge und Reports sind verteilt; manuelles Suchen und Quervergleiche kosten Zeit.
- **Mit PORTO:** Dateien hochladen oder aus dem Workspace wählen; gezielt fragen — die KI nutzt Dokumentenkontext und Struktur.
- **Nutzen:** Schnellere Einschätzung, weniger Suchaufwand, konsistente Antworten auf wiederkehrende Fragen.
### 2. Berichte & Ausarbeitungen
- **Problem:** Informationen aus mehreren Quellen zusammentragen und in saubere Dokumente bringen.
- **Mit PORTO:** Agent unterstützt bei Recherche, Strukturierung und Erstellung — verbunden mit Dateien und Datenquellen.
- **Nutzen:** Weniger manuelle Zusammenführung, höhere Durchsatzrate bei wiederkehrenden Deliverables.
### 3. Kommunikation & Übersetzung
- **Problem:** Entwürfe, Zusammenfassungen und Übersetzungen entstehen fragmentiert und ohne einheitlichen Leitfaden.
- **Mit PORTO:** KI formuliert, fasst zusammen und übersetzt — im geschützten Umfeld; Anbindung an E-Mail- und Cloud-Kontext wo vorgesehen.
- **Nutzen:** Schnellere, konsistentere Kommunikation bei gleichbleibender Governance.
### 4. Wissensabruf aus dem Unternehmen
- **Problem:** Wissen steckt in SharePoint, Drives, Ordnern und Alt-Dokumenten — Antworten dauern.
- **Mit PORTO:** Semantische Suche und Kontext über angebundene Quellen und indexierte Inhalte.
- **Nutzen:** Weniger «Wer hat das Dokument?» — mehr direkte, begründete Antworten.
**Fusszeile:**
PORTO von PowerOn macht Wissensarbeit einfacher — für Fachbereiche, Führung und Operations, nicht nur für Tech-Teams.
---
## Folie 5 — Warum PORTO? (CTA)
**Hauptzeile (gross):**
Warum PORTO?
**Kernbotschaften (3 Zeilen):**
- Ihre Daten bleiben in der Schweiz.
- Ihre Chats und Dokumente bleiben unter Kontrolle.
- Ihre Teams werden bei Wissensarbeit messbar schneller.
**Fliesstext:**
Wir zeigen Ihnen in einem kostenlosen Erstgespräch, wie PORTO AI Chat in Ihrem sensibelsten Prozess sinnvoll eingesetzt und wertschöpfend integriert werden kann.
**Claim:**
Ihr KI-Chat für sensible Daten und Dokumente — ohne Datenabfluss, ohne Kontrollverlust.
**Abschluss:**
Weil sensible KI Vertrauen braucht.
---
## Folie 6 — Team (unverändert zur Master-Präsentation)
**Überschrift:**
Wir kombinieren Strategie, Technologie und Umsetzungskraft.
**WER WIR SIND — Das PowerOn Team**
**Patrick Motsch** — Partner
Leitet erfolgreich komplexe IT-Implementierungsprojekte; langjährige Erfahrung in innovativer Softwareentwicklung.
*Mission: Nachhaltige KI-Integration für Schweizer KMUs.*
**Ida Dittrich** — Product Architect
Verbindet wissenschaftliches Know-how mit praktischer IT-Erfahrung und bringt innovative Ansätze in technische Projekte ein.
**Stephan Schellworth** — Business Integration
Verbindet strategisches Denken mit praxisnaher Projektsteuerung und gestaltet digitale Projekte erfolgreich.
**Rollen (kurz):**
Patrick Motsch: CEO/CTO · Ida Dittrich: Product Architect · Stephan Schellworth: Business Integration
**Kontakt:**
PowerOn AG
Birmensdorferstrasse 94, 8003 Zürich
www.poweron.swiss
---
## Hinweise für Design / PDF
- Typografie und Farben wie bei «Local LLM»-Deck übernehmen.
- Folie 4: Vier gleich breite Spalten oder 2×2-Raster; «Problem / Mit PORTO / Nutzen» visuell trennen (z. B. kleine Labels).
- Optional: Ein Screenshot der Workspace-Oberfläche (3-Spalten) als dezentes Hintergrund- oder Rand-Element auf Folie 3 — nur wenn markenkonform freigegeben.

View file

@ -1,22 +0,0 @@
# Automation — One-Pager
**Layout:** Links Screenshot (Flow-Editor oder Workflow-Uebersicht), rechts Text. PowerOn-Logo rechts oben.
---
## Automation
Wiederkehrende Aufgaben einmal einrichten — und nie wieder manuell erledigen.
Die Automation in PowerOn uebernimmt wiederkehrende Ablaeufe zuverlaessig fuer Sie. Stellen Sie Schritte visuell zusammen oder nutzen Sie fertige Vorlagen — die Plattform fuehrt sie auf Knopfdruck, nach Zeitplan oder ausgeloest durch eine E-Mail aus.
### Kernfunktionen
Ein Klick oder ein Zeitplan — der Ablauf erledigt den Rest.
- Ablaeufe visuell per Drag & Drop zusammenstellen und verbinden
- Fertige Vorlagen fuer gaengige Geschaeftsprozesse sofort einsetzbar
- Start per Zeitplan, Formular, E-Mail-Eingang oder manuell
- Freigaben, Formulare und Uploads als menschliche Zwischenschritte einbinden
> **Screenshot:** Flow-Editor mit verbundenen Schritten (z. B. Zeitplan → KI-Zusammenfassung → E-Mail) oder Workflow-Liste mit Status und naechster Ausfuehrung.

View file

@ -1,22 +0,0 @@
# Kommunikations-Coach — One-Pager
**Layout:** Links Screenshot (Dashboard oder Coaching-Session), rechts Text. PowerOn-Logo rechts oben.
---
## Kommunikations-Coach
Besser kommunizieren — mit KI als persoenlichem Sparringspartner.
Der Kommunikations-Coach in PowerOn trainiert gezielt Gespraechssituationen aus dem Berufsalltag. Waehlen Sie ein Thema, ueben Sie per Chat oder Sprache mit der KI — und verfolgen Sie Ihren Fortschritt ueber Sessions hinweg.
### Kernfunktionen
Von der ersten Uebung bis zum messbaren Fortschritt — alles in einem Dossier.
- Coaching-Themen fuer typische Fuehrungssituationen (Feedback, Konflikte, Verhandlung u. a.)
- Training per Chat oder Sprache — mit realistischen Rollenspielen
- Bewertung nach jeder Session mit konkreten Verbesserungshinweisen
- Fortschritt, Aufgaben und Erfolge (Streaks, Level, Auszeichnungen) auf einen Blick
> **Screenshot:** Dashboard mit Streak, Kompetenz-Score und aktiven Themen oder laufende Coaching-Session mit Chat-Verlauf und Sprachsteuerung.

Binary file not shown.

View file

@ -1,105 +0,0 @@
# Flyer: PowerOn Launch48
*Zweiseitige Kurzfassung zum Drucken verweist auf das Kundenangebot*
**Vollstaendiges, teilbares Angebot:** [poweron-launch48-offer.md](./poweron-launch48-offer.md)
**4-Folien-Deck (Praesentation):** [launch48-deck-presentation.md](./launch48-deck-presentation.md)
---
## Layout-Briefing (fuer Designer / Canva)
| Element | Vorgabe |
| --- | --- |
| **Format** | DIN A4 oder US Letter, **zweiseitig**; alternativ **A5 hoch** fuer Events |
| **Primaerfarbe** | Blau **#1976d2** |
| **Sekundaer** | Tuerkis-Akzente, Violett dezent (wie [case-study-power-desktop.md](./case-study-power-desktop.md)) |
| **Hintergrund** | Hell, viel Weissraum; Seite 1 „Hero“ |
| **Typo** | Serioes, gut lesbar |
| **Bilder** | Optional: abstrakte Workspace-Illustration |
| **Logo** | PowerOn oben links Seite 1 |
| **QR** | Seite 2: Link zum PDF/Web **Launch48** oder Kalender |
---
## Seite 1
### Headline
**PowerOn Launch48**
**Ihre erste produktive KI-Loesung in 48 Stunden.**
### Subline
**Ein klar abgegrenzter Anwendungsfall. Ihre Daten in geregeltem Rahmen. Ein Ergebnis, das sich im Pilot messen laesst.**
### Drei Kurz-Punkte (Problem)
- **Viel Routine** Teams haengen in wiederkehrenden Schritten.
- **Wissen verteilt** Antworten dauern, Qualitaet schwankt.
- **KI ohne Leitplanken** unsichere Tools statt freigegebener Plattform.
### Vier Phasen (grafisch 14, wie Praesentation)
1. **Discovery** Gemeinsame Analyse; Use-Case mit grossem Hebel, in 48h realistisch.
2. **Design und Architektur** Daten, Integration auf PowerOn; Erfolgsziele schriftlich vor Start.
3. **Build und Integration** Umsetzung, Tests; Fachseite prueft mit (parallel).
4. **Deploy und Handover** Go-Live in vereinbarter Umgebung, Doku, Einweisung.
*Volltext und Zeitrahmen:* [poweron-launch48-offer.md](./poweron-launch48-offer.md) Abschnitt „Der Ablauf“.
### Angebot (Box)
| | |
| --- | --- |
| **Paket** | **CHF 9000** Fixpreis |
| **Zu Beginn** | **CHF 2000** |
| **Bei Erfolg** | **CHF 7000** (wenn die vereinbarten Erfolgsziele im Pilot erreicht sind) |
*Der Preis steht fuer Transparenz und einen klaren Rahmen: kein offenes Beratungsprojekt, sondern ein fokussiertes Paket auf PowerOn.*
*Details und Grenzen: siehe Angebotsdokument Launch48.*
### Footer
**poweron.swiss** · PowerOn AG, Zuerich
---
## Seite 2
### Ueberschrift
**Was Sie bekommen · Was Sie mitbringen**
**Wir:**
- Funktionierende **KI-Loesung** auf PowerOn fuer **einen** definierten Fall
- **Datenquellen** und **eine** Anbindung im Standardrahmen (wie vereinbart)
- **Einweisung** und **kurze Dokumentation**
**Sie:**
- **Ansprechperson** Fach und IT
- **Zugriffe** und **Freigaben** rechtzeitig
- **kleine Pilotgruppe** fuer die Messung der Erfolgsziele
### Team
Patrick Motsch · Ida Dittrich · Stephan Schellworth
Birmensdorferstrasse 94, 8003 Zuerich
### Call to Action
**15-Minuten-Gespraech:** Passt Launch48 zu Ihnen?
→ QR / Link / E-Mail
---
## Druck-Hinweise
PDF **CMYK**, **3 mm Beschnitt**, Schriften einbetten.
---
## Keywords
Launch48, PowerOn, Flyer, KI, 48h, Fixpreis, Kundenangebot

View file

@ -1,138 +0,0 @@
# PowerOn Kosten auf einen Blick (Landingpage)
Dieses Dokument fasst die **tatsächlich im Gateway implementierte** Abrechnungslogik in verständlicher Sprache zusammen für transparente Darstellung auf der Website. Alle Zahlen und Regeln beziehen sich auf den Stand des Codes (siehe Abschnitt [Quellen im Repository](#quellen-im-repository)).
---
## Was kostet PowerOn? (Kurzfassung)
- **Abonnement (Standard):** Sie zahlen **pro Abrechnungszeitraum** für jeden **aktiven Benutzer** und jede **aktive Feature-Instanz** wahlweise **monatlich** oder **jährlich** (Preise in **CHF**).
- **Inkludiert im Abo:** Ein festes **KI-Budget in CHF pro Abrechnungsperiode** (z. B. monatlich 10 CHF bzw. jährlich 120 CHF beim Standard-Plan).
- **Darüber hinaus:** KI-Nutzung wird **verbrauchsbasiert** vom Guthaben abgebucht; der Endbetrag ergibt sich aus den **Provider-Einstandskosten** plus einem **definierten Aufschlag** im Backend.
- **Speicher:** Über dem im Plan enthaltenen Datenvolumen fällt **zusätzlicher Speicher** an (**CHF pro GB und Monat**).
- **Aufladen:** Zusätzliches Guthaben ist per **Stripe Checkout** in festen Stufen möglich (**10500 CHF**).
---
## So setzt sich der Preis zusammen
### 1) Fixe Abo-Komponente (Benutzer + Instanzen)
Die wählbaren Pläne sind im Backend als **fester Katalog** hinterlegt. Maßgeblich sind:
| Plan (Schlüssel) | Zeitraum | Preis pro aktivem User | Preis pro aktiver Feature-Instanz | Inkl. Datenvolumen (Plan) | Inkl. KI-Budget (pro Periode) |
|------------------|----------|------------------------|-----------------------------------|---------------------------|-------------------------------|
| **Standard (Monatlich)** `STANDARD_MONTHLY` | Monat | **90 CHF** | **150 CHF** | **1024 MB** (1 GB) | **10 CHF** |
| **Standard (Jährlich)** `STANDARD_YEARLY` | Jahr | **1080 CHF** | **1800 CHF** | **1024 MB** (1 GB) | **120 CHF** |
**Hinweis:** Die Jahrespreise entsprechen **12 ×** den Monatsbeträgen (gleiche effektive Monatsrate).
**Hinweis zu Limits:** Bei den Standard-Plänen sind im Katalog **keine** `maxUsers` / `maxFeatureInstances` gesetzt (`None` = im Modell **keine Plan-Obergrenze**; nur das **Datenvolumen** ist mit 1024 MB pro Mandat als Plan-Limit hinterlegt). Der Trial-Plan hat explizit **1** User und **3** Instanzen max.
**Wichtig für das Verständnis:** Die Abrechnung erfolgt **nutzungsorientiert in dem Sinne**, dass sich die **Gesamtsumme** aus der **Anzahl aktiver User** und **aktiver Feature-Instanzen** ergibt. Änderungen (z. B. mehr Instanzen) können über Stripe mit **Proration** abgebildet werden (technische Umsetzung im Gateway).
### 2) Testphase (Trial)
Der Plan **7-Tage-Test** (`TRIAL_7D`) ist **kein kostenpflichtiges Abo** im Katalog-Sinne, sondern eine begrenzte Phase:
- **Dauer:** 7 Tage
- **Limits:** max. **1** User, max. **3** Feature-Instanzen, **500 MB** Datenvolumen
- **Inkl. KI-Budget:** **5 CHF**
- Nach Ablauf ist laut Katalog ein Übergang zum **Standard (Monatlich)** vorgesehen (`successorPlanKey`).
---
## Variable Kosten (über das Abo hinaus)
### KI-Nutzung (Pay-per-Use aus dem Guthaben)
- Vor KI-Aufrufen prüft das System u. a., ob ein **aktives Abonnement** (oder Trial / begrenzt überfälliger Status) vorliegt und ob **ausreichend Guthaben** vorhanden ist.
- Die bei einem Aufruf verbuchte Summe basiert auf einem **Basispreis** (vom KI-Provider / AICore geliefert) und wird im Gateway mit einem **Aufschlag** multipliziert.
**Implementierter Aufschlag:** Konstante `BILLING_MARKUP_PERCENT = 400` → Faktor **\(1 + 400\% = 5{,}0\)** auf den übergebenen Basisbetrag.
> **Transparenz-Hinweis:** Im Code-Kommentar neben der Konstante steht eine andere Erläuterung („Faktor 2.0“). **Maßgeblich für die Verrechnung ist die Implementierung** (`400` → Faktor 5). Bei Änderungen der Konstante bitte Landingpage-Text anpassen.
### Speicher über dem Plan-Inklusivvolumen
- **Preis überschüssigen Speichers:** **0,50 CHF pro GB und Monat** (`STORAGE_PRICE_PER_GB_CHF`), soweit das Volumen über dem im Plan enthaltenen **Soft-Limit** liegt (Standard-Pläne: **1024 MB**).
### Guthaben aufladen (optional)
Erlaubte **Einmal-Beträge** für Stripe-Top-up (serverseitig fix): **10, 25, 50, 100, 250, 500 CHF**.
---
## Abrechnung & Zahlung (ohne Technik-Jargon)
1. **Abo:** Aktivierung läuft über **Stripe** (wiederkehrende Abrechnung). Mengen (User/Instanzen) werden mit Stripe synchronisiert; Rechnungsstellung erfolgt über Stripe entsprechend der gewählten Periode.
2. **Guthaben:** KI-Verbrauch und ggf. Speicher-Overage belasten das **Prepaid-Guthaben** des Mandats (bzw. die kontextabhängige Kontoführung im Billing-Modul).
3. **Top-up:** Mandats-Admins können per **Stripe Checkout** Guthaben kaufen; die Gutschrift erfolgt über **Webhooks** / Bestätigung serverseitig nur erlaubte Beträge.
4. **Nachvollziehbarkeit:** Transaktionen und Auswertungen (z. B. nach Zeitraum, Provider, Modell, Feature) sind über die **Billing-API** abrufbar (für eingeloggte Nutzer je nach Rolle).
---
## Währung, Steuern, Laufzeit
- **Währung:** Durchgängig **CHF** (Plan-Katalog, Speicher-Overage, Top-up-Stufen).
- **Intervalle:** **Monatlich** oder **jährlich** für die Standard-Pläne; Trial ohne Abo-Intervall.
- **MwSt. / Steuerlogik:** Im Gateway-Code ist **keine** automatische Umsatzsteuerberechnung für Stripe-Checkout der Abos erkennbar; die **Unternehmens-/MwSt.-Angaben** des Betreibers können aus der Konfiguration für Kommunikation genutzt werden **finanzrechtliche Texte** auf der Landingpage sollten mit Buchhaltung/Legal abgestimmt werden.
---
## FAQ (für die Website)
**Ist dies ein Abo?**
Ja — die reguläre Nutzung von PowerOn ist ein Abonnement. Sie zahlen in einem festen Rhythmus (**monatlich oder jährlich**) für Ihre aktiven Benutzer und aktiven Funktionsbereiche (Feature-Instanzen). Die Zahlung verlängert sich automatisch, bis Sie kündigen oder Ihren Tarif anpassen. Im Abo-Preis ist bereits ein **KI-Budget** enthalten, das Sie jeden Monat bzw. jedes Jahr nutzen können. Für intensive KI-Nutzung oder zusätzlichen Speicher über dem inkludierten Volumen kann separat Guthaben aufgeladen werden — so bleibt das Basis-Abo planbar, während Mehrverbrauch fair nach tatsächlicher Nutzung abgerechnet wird. Die **kostenlose Testphase** (7 Tage) ist kein bezahltes Abo und endet automatisch; über den Wechsel zu einem Standard-Tarif werden Sie rechtzeitig informiert.
<!-- LEGAL-REVIEW: Formulierungen "verlängert sich automatisch, bis Sie kündigen" und
"über den Wechsel zu einem Standard-Tarif werden Sie rechtzeitig informiert"
vor Veröffentlichung mit Legal/AGB abstimmen (autoRenew=true im Gateway-Katalog,
successorPlanKey=STANDARD_MONTHLY beim Trial). -->
**Zahle ich nur das Abo?**
Nein. Das Abo deckt **Lizenzen** (User + Instanzen) und ein **inkludiertes KI-Budget** pro Periode. Darüber hinaus zählen **zusätzliche KI-Kosten** (verbrauchsbasiert mit Aufschlag) und ggf. **Speicher über dem Planlimit**.
**Wie transparent sind die KI-Kosten?**
Jede belastbare Nutzung wird als **Transaktion** geführt (u. a. Provider, Modell, Feature-Kontext). Der Endbetrag enthält den im Code konfigurierten **Aufschlag** auf die Provider-Basis.
**Kann ich Guthaben nachladen?**
Ja, in festen Paketen (**10500 CHF**) über **Stripe**.
**Was passiert, wenn das Guthaben nicht reicht?**
Die Plattform blockiert entsprechende **KI-Aufrufe**, sobald die Prüfung (Abo + Guthaben) nicht mehr erfüllt ist Details siehe `BillingService.checkBalance` im Gateway.
**Gibt es eine Testphase?**
Ja: **7 Tage** mit klaren Grenzen (User, Instanzen, Volumen, **5 CHF** KI-Budget).
**Wechseln sich die Preise ohne Ankündigung?**
Die öffentlich kommunizierten Beträge sollten mit dem **Deploy-Stand** des Gateways übereinstimmen: Die Standardpreise liegen im **Python-Plan-Katalog** (`BUILTIN_PLANS`), nicht in einer Marketing-Datei.
---
## Quellen im Repository
| Thema | Datei (Gateway) |
|--------|------------------|
| Plan-Katalog, CHF-Preise, Limits, KI-Budget | `modules/datamodels/datamodelSubscription.py` (`BUILTIN_PLANS`) |
| Speicher-Overage CHF/GB/Monat | `modules/datamodels/datamodelBilling.py` (`STORAGE_PRICE_PER_GB_CHF`) |
| KI-Aufschlag / Verbrauchsbuchung | `modules/serviceCenter/services/serviceBilling/mainServiceBilling.py` (`BILLING_MARKUP_PERCENT`, `calculatePriceWithMarkup`, `recordUsage`, `checkBalance`) |
| Top-up-Beträge, Stripe Checkout | `modules/serviceCenter/services/serviceBilling/stripeCheckout.py` (`ALLOWED_AMOUNTS_CHF`) |
| Billing- & Abo-Routen (API) | `modules/routes/routeBilling.py`, `modules/routes/routeSubscription.py`, `modules/routes/routeStore.py` (`/api/store/subscription-info`) |
---
## Mini-Übersicht als Fluss (optional für Diagramm auf der Seite)
```mermaid
flowchart LR
visitor[Besucher] --> plans[Plan_User_und_Instanz_CHF]
plans --> included[Inkl_KI_Budget_pro_Periode]
included --> usage[Verbrauch_KI_und_Speicher]
usage --> topup[Optional_Stripe_TopUp]
topup --> insight[Transaktionen_und_Statistik_in_App]
```
---
*Letzte inhaltliche Abstimmung mit dem Gateway-Code: Dokument erzeugt für Landingpage-Transparenz; bei Code-Änderungen bitte Tabelle und FAQ aktualisieren.*

View file

@ -1,233 +0,0 @@
# PowerOn 48h AI Sprint Praesentationsdeck (4 Folien)
*Fertiger Copy-Stand fuer Canva / PowerPoint / PDF-Export. Ersetzt die fruehere Arbeitsversion `20260320_AI_Hackathon.pdf` inhaltlich.*
**Kundenangebot (Fliesstext):** [poweron-launch48-offer.md](./poweron-launch48-offer.md)
**Schreibweise:** durchgaengig **PowerOn** (nicht PowerON). Produktname im Kundenfacing: **48h AI Sprint** (nicht mehr „Launch48“ als Markenname).
**Optional Zusatzfolie Architektur (16:9, HTML):** [poweron-ki-betriebssystem-slide.html](./poweron-ki-betriebssystem-slide.html) im Browser oeffnen, Ansicht **1920×1080** (z. B. DevTools-Geraetemodus), **Screenshot** oder Druck als PDF fuer PowerPoint/Keynote. Prompts fuer Bild-KI: [poweron-ki-betriebssystem-prompts.md](./poweron-ki-betriebssystem-prompts.md).
---
## Folie 1 von 4 Einstieg (Hero)
### Haupttitel
**48h AI Sprint**
### Nutzenversprechen (ein Satz, sales-stark)
**Von der Idee zum pilotfaehigen MVP in 48 Stunden gebaut mit KI-gestuetztem Engineering durch PowerOn, mit Fixpreis und Erfolgsanteil.**
### Outcome (ein Satz, greifbar keine Wiederholung von „klar abgegrenzt“)
**Sie erhalten einen funktionierenden Software-Piloten im vereinbarten Umfang spezifiziert, umgesetzt, getestet und dokumentiert plus schriftlich fixierte Erfolgsziele fuer Abnahme und die zweite Zahlungsstufe.**
### Badge / Meta
**PowerOn · 48h AI Sprint · 2026**
### Drei Kurz-Pills (horizontal)
| Pill 1 | Pill 2 | Pill 3 |
| --- | --- | --- |
| **48 Stunden** Umsetzungsblock | **Fixpreis CHF 9'000** | **CHF 7'000** erfolgsgebunden |
*Hinweis fuer Layout (optional, klein unter Pills):* `CHF 7'000` = **78%** des Paketpreises faellig bei nachgewiesenen, vorab vereinbarten Erfolgszielen im Pilot.
### Zahlungslogik (eine Zeile, zentral)
**CHF 2'000 zu Projektstart · CHF 7'000 bei erfuellten, schriftlich definierten Pilot-Zielen.**
### Vertrauen / Governance (eine Zeile)
**Gemeinsame Entscheidungsbasis: fester Leistungsrahmen, keine offene Stundenhonorarspirale. Betrieb, Datenfluesse und Freigaben stimmen wir vor dem 48h-Block mit Ihrer IT und Compliance ab.**
### Micro-CTA
**15-Minuten-Check: Passt Ihr Software-Vorhaben zum 48h AI Sprint?**
### CTA-Kanaele (konkret eintragen / im PDF verlinken)
- **Web:** [www.poweron.swiss](https://www.poweron.swiss)
- **E-Mail:** info@poweron.swiss *(Betreff-Vorschlag: „48h AI Sprint 15-Minuten-Check“)*
- **Kalender:** *(Link zum Buchungstool hier einfuegen, sobald vorhanden)*
---
### Was auf Folie 1 weg soll (Redundanzen aus alter Version)
- Keine doppelte **FIXPREIS**-Box.
- Kein paralleler Marken-Mix: durchgaengig **48h AI Sprint** als Produktname auf der Folie.
- Kein Mix aus **CHF 9'000** und **CHF 9k** auf derselben Folie.
- Pill 3 nicht nur **„78% bei Erfolg“** ohne Kontext **CHF 7'000 erfolgsgebunden** plus optionaler Hinweis auf 78%.
- **„messbarer ROI“** nur, wenn ihr ihn operationalisiert; sonst: **„messbare Erfolgsziele im Pilot“**.
---
## Folie 2 von 4 Der Ablauf
### Titel
**DER ABLAUF**
### Untertitel (korrigiert)
**Vier Phasen · 48 Stunden · gemeinsam mit Ihrem Team**
### Subline
**KI-gestuetztes Engineering durch erfahrene Architektinnen und Architekten auf der PowerOn-Plattform, mit Ihren Daten und Systemen.**
### Phase 1 Discovery
Gemeinsame Analyse Ihres Vorhabens. Wir identifizieren den Scope mit dem groessten Nutzen **messbar** und in **48 Stunden** realistisch umsetzbar.
### Phase 2 Design und Architektur
Software-Architektur, Datenmodell und Integration auf der **PowerOn**-Plattform. Definition der **Erfolgsziele**, die **vor dem Start schriftlich fixiert** werden und ueber die **Erfolgszahlung** (CHF 7'000) entscheiden.
### Phase 3 Build und Integration
Umsetzung der Loesung, Anbindung an Ihre Systeme **im Vereinbarten**, Testing. **Mensch prueft mit:** Validierung durch Ihre Fachanwenderinnen und -anwender parallel zum Build.
### Phase 4 Deploy und Handover
**Go-Live in Ihrer vereinbarten PowerOn-Umgebung** (Pilot oder Produktion je nach Vereinbarung), Wissenstransfer, Dokumentation. Ihr Team kann die Loesung **im vereinbarten Rahmen** vom ersten Tag an **selbststaendig weiterbetreiben und ausbauen**.
### Optional: Zeit-Splits (Beispiel-Verteilung, nicht vertraglich)
*Hinweis intern: Anpassen, wenn euer echtes Modell anders ist.*
| Block | Dauer (Beispiel) |
| --- | --- |
| Discovery / Vorbereitung | 4 h |
| Design / Architektur | 8 h |
| Build / Integration | 28 h |
| Deploy / Handover | 8 h |
### Fussbereich Folie 2
**Gebaut auf PowerOn Ihrer AI-Augmented-Engineering-Plattform.**
PowerOn ist nicht nur ein Projektrahmen: Hier entsteht Ihre Loesung mit **KI als Produktivitaetshebel** des Teams mit Monitoring, Auditierbarkeit, Rollen- und Rechteverwaltung und Skalierbarkeit.
**Stichwoerter (Tags):** AI-augmented Engineering · Hosting nach Vorgabe · Software-Qualitaet · Nachvollziehbarkeit
---
## Folie 3 von 4 Warum PowerOn
### Kennzahl-Band (qualifiziert, nicht als Garantie)
**Deutlich schneller als klassische Monatsprojekte** je nach Ausgangslage und Vorhaben.
*(Die fruehere Formulierung „10x“ nur verwenden, wenn ihr sie pro Kunde belegen koennt.)*
### Titel
**WARUM POWERON**
### Untertitel
**Nicht nur schnell strukturell besser.**
### Kurzzeile
KI-gestuetzt bauen mit nachvollziehbaren Ergebnissen und klarem Scope.
### Block 1 AI-Augmented-Engineering-Plattform
PowerOn ist keine Ad-hoc-Einzelloesung: eine **Plattform fuer AI-augmented Engineering**. Ihre **Software-Loesung** laeuft in einer Umgebung, die fuer **Skalierung** und **Sicherheit** ausgelegt ist; die KI steigert die **Liefergeschwindigkeit** des Teams, nicht das Chat-Erlebnis allein.
### Block 2 Erfolgsgebundene Verguetung
**CHF 7'000** (78% von CHF 9'000) zahlen Sie bei **nachgewiesenen, vorab schriftlich vereinbarten Erfolgszielen** im Pilot. So teilen wir das Ergebnisrisiko transparent.
### Block 3 Sicherheit und Compliance fuer Ihr Projekt
**Datenhaltung nach Vorgabe:** Schweizer Hosting, Cloud nach Wahl oder andere Modelle wir stimmen das **im Erstgespraech** mit Ihrer IT und Compliance ab.
### Block 4 Enablement statt Abhaengigkeit
Wissenstransfer ist **fester Bestandteil**. Ihr Team versteht Bedienung, Grenzen und Weiterentwicklung **im vereinbarten Rahmen**.
### Messbare Ergebnisse (als Zielgroessen, nicht als Versprechen)
*Formulierung fuer Folie:*
**Im Pilot messen wir gemeinsam typische Zielgroessen (je nach Vorhaben):**
- Funktionalitaet und Stabilitaet der Loesung im Alltag
- Zeit bis zur **ersten pilotfaehigen** Nutzung: **48 Stunden** Umsetzungsblock (plus vereinbarter Pilot)
- Wirtschaftlichkeit: Break-even haengt von internem Aufwand und Volumen ab **kein fixer Monatswert ohne Daten**
*(Die frueheren harten Zahlen **-70%** und **<6 Monate** nur nutzen, wenn ihr sie durch Pilotdaten oder Rechnungsbeispiele stuetzt; sonst weglassen oder als Illustration kennzeichnen.)*
### Proof-Box Abraxas *(nur bei schriftlicher Kundenfreigabe nennen)*
**Variante A mit Namensnennung (bei Freigabe durch Abraxas):**
> **Referenz (Methodik):** PowerOn hat die **DATA-Hub-Backend-Migration** fuer die **Abraxas Informatik AG** in **11 Tagen** umgesetzt (Node.js/TypeScript zu .NET/C#) mit klaren Phasen, Reviews und Wissenstransfer.
> **Botschaft:** Dieselbe **Lieferdisziplin** nutzen wir, um Ihren **Software-Piloten** im Rahmen des **48h AI Sprint** schnell und kontrolliert zu bringen.
> **Details:** Case Study auf Anfrage.
**Variante B ohne Namen (wenn keine Freigabe), ausfuehrlich fuer Proof-Folie / HTML:**
**Referenzcase (anonymisiert) Backend-Migration unter Zeitdruck**
*Folie / Layout: optional zwei Spalten „Ausgangslage | Vorgehen“ oder vier Zeilen unten als Timeline.*
- **Ausgangslage**
- Fuehrendes **Schweizer Softwarehaus**; **geschaeftskritisches** Plattform-Backend (DATA-Hub-Umfeld)
- Jahre gewachsen, mehrere fruehere Partner, **hohe technische Schulden** und **Pentest-relevante Security-Themen**
- **Wissensluecken:** **10** Themenbereiche, **49** Klaerungsfragen vor Migration (strukturiert beantwortet)
- Klassische Groessenordnung: **36 Monate** statt Wochen
- **Vorgehen (4 Phasen, KI-gestuetzt, Human-in-the-Loop)**
1. **Analyse & Dokumentation** (ca. 23 Tage): **47** TypeScript-Dateien inventarisiert und dokumentiert
2. **Technische Spezifikation** (ca. 23 Tage): Ziel **.NET/C#**, Architekturentscheide, **Reviews** mit Kundenteam, **Acceptance Criteria**
3. **Execution** (ca. **1 Tag** Migration): **Node.js/TypeScript → .NET/C#**, Modul fuer Modul **architektonisch validiert**, Tests parallel
4. **Testing & Uebergabe** (ca. 23 Tage): Validierung, automatisierte Tests, **Wissenstransfer** (Training on the Job, Video)
- **Ergebnis**
- **11 Kalendertage** Gesamt (Kickoff bis uebergabefaehige Basis)
- **~1 Tag** fuer eigentliche Code-Migration; Tech Debt und Security-Punkte **adressiert**; Team **befaehigt**
- Relativ zu klassisch **mehrmonatiger** Migration: **ca. 10x schneller** in diesem Fall *(Indikator, keine Garantie fuer andere Vorhaben)*
- **Transfer zum 48h AI Sprint**
- Gleiche Logik: **fester Scope**, Phasen, messbare Abnahme, **Enablement** als Kern nicht als Zusatz
*Hinweis:* Kein Garantieversprechen pro Use Case. **Vollstaendige Case Study / namentliche Referenz** nur auf Anfrage und mit **schriftlicher Kundenfreigabe**.
---
### Geeignet fuer (Software-Vorhaben im Paketrahmen)
- **MVP-** und Pilotbau, Prototyping
- **Backend-Migration** und Stack-Wechsel
- **Systemintegration** (APIs, Daten, Identity)
- **Prozessautomatisierung** und interne Tools
- **Legacy-Modernisierung** in abgegrenztem Schnitt
- Dokumentenverarbeitung, Reporting, Freigabe-Workflows im vereinbarten Umfang
---
## Folie 4 von 4 Wer wir sind
### Titelzeile
**Wir kombinieren Strategie, Technologie und Umsetzungskraft.**
### Ueberschrift
**WER WIR SIND Das PowerOn-Team**
### Lieferfaehigkeit 48h AI Sprint (eine Zeile, fuer Erstleser)
**Dieses Team fuehrt Discovery, Architektur, Build und Handover im 48h AI Sprint End-to-End, mit klaren Meilensteinen.**
### Patrick Motsch
**CEO/CTO** Steuert technische Umsetzung und komplexe IT-Projekte; sorgt dafuer, dass Ihr Software-Pilot in PowerOn produktiv wird und betreibbar bleibt.
**Mission:** Schneller, nachvollziehbarer **Softwarebau** fuer Schweizer Unternehmen mit KI als Engineering-Hebel.
### Ida Dittrich
**Product Architect** Verantwortet Architektur, Qualitaet und Machbarkeit auf der PowerOn-Plattform damit Scope, Daten und Integration im 48h-Rahmen stimmig bleiben.
### Stephan Schellworth
**Business Integration** Verbindet Vorhaben, Stakeholder und Projektsteuerung damit Erfolgsziele vor dem Start klar sind und der Pilot messbar bleibt.
### Rollen (einzeilig, Fusszeile / Karten)
- **Patrick Motsch** CEO/CTO
- **Ida Dittrich** Product Architect
- **Stephan Schellworth** Business Integration
### Kontakt und naechster Schritt
**PowerOn AG**
Birmensdorferstrasse 94, 8003 Zuerich
**15-Minuten-Check buchen:** [www.poweron.swiss](https://www.poweron.swiss) · **E-Mail:** info@poweron.swiss
*(Betreff: „48h AI Sprint 15-Minuten-Check“; Kalenderlink ergaenzen, sobald verfuegbar.)*
---
## Checkliste vor PDF-Export
- [ ] Referenz: **Variante A (Abraxas namentlich)** oder **Variante B (anonym, ausfuehrlich)** gewaehlt; Freigabe fuer Namensnennung liegt vor?
- [ ] Alle **PowerOn**-Schreibweisen vereinheitlicht; Produktname **48h AI Sprint** konsistent
- [ ] Keine **doppelten** Preisboxen auf Folie 1
- [ ] **Kennzahlen** nur in der gewaehlten Strenge (hart vs. qualifiziert)
- [ ] **CTA** mit realem Kalenderlink oder zentraler E-Mail belegt
---
## Hinweis zur Datei in Downloads
Die bisherige Datei `20260320_AI_Hackathon.pdf` bitte **inhaltlich** an dieses Dokument anpassen (Design kann gleich bleiben). Diese Markdown-Datei ist die **autoritative Textfassung**.

View file

@ -1,494 +0,0 @@
<!DOCTYPE html>
<html lang="de-CH">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="PowerOn 48h AI Sprint: pilotfaehiger MVP oder Software-Pilot in 48 Stunden, KI-gestuetztes Engineering, Fixpreis CHF 9'000, CHF 7'000 erfolgsgebunden.">
<title>PowerOn 48h AI Sprint | MVP in 48 Stunden</title>
<style>
:root {
--po-blue: #1976d2;
--po-blue-dark: #12579b;
--po-teal: #00897b;
--text: #1a1a2e;
--text-muted: #5c5c6f;
--bg: #f8fafc;
--card: #ffffff;
--border: #e2e8f0;
--radius: 12px;
--shadow: 0 4px 24px rgba(25, 118, 210, 0.08);
--max: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 1.0625rem;
line-height: 1.55;
color: var(--text);
background: var(--bg);
}
a { color: var(--po-blue); text-decoration: none; }
a:hover { text-decoration: underline; }
.wrap { max-width: var(--max); margin: 0 auto; padding: 0 1.25rem; }
/* Header */
header.site {
background: linear-gradient(135deg, var(--po-blue-dark) 0%, var(--po-blue) 48%, #1565c0 100%);
color: #fff;
padding: 2.5rem 0 3.25rem;
}
.badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.92;
margin-bottom: 1rem;
}
header h1 {
margin: 0 0 0.75rem;
font-size: clamp(1.85rem, 4.5vw, 2.5rem);
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.02em;
}
.hero-lead {
font-size: 1.2rem;
max-width: 38rem;
opacity: 0.96;
margin: 0 0 0.5rem;
}
.hero-outcome {
font-size: 1.02rem;
font-weight: 600;
line-height: 1.45;
max-width: 40rem;
opacity: 0.98;
margin: 0 0 1.35rem;
}
.pills {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
margin-bottom: 1.25rem;
}
.pill {
background: rgba(255,255,255,0.14);
border: 1px solid rgba(255,255,255,0.28);
padding: 0.5rem 1rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 600;
}
.pill strong { font-weight: 700; }
.payment-line {
font-size: 0.95rem;
opacity: 0.95;
margin-bottom: 1rem;
max-width: 40rem;
}
.trust-line {
font-size: 0.88rem;
opacity: 0.85;
max-width: 42rem;
margin-bottom: 1.75rem;
}
header.site .pill-hint {
font-size: 0.8rem;
opacity: 0.82;
max-width: 42rem;
margin: 0.5rem 0 1rem;
}
.btn {
display: inline-block;
background: #fff;
color: var(--po-blue-dark);
font-weight: 600;
padding: 0.85rem 1.5rem;
border-radius: var(--radius);
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn:hover { text-decoration: none; opacity: 0.95; }
.btn-secondary {
background: transparent;
color: #fff;
border: 2px solid rgba(255,255,255,0.55);
box-shadow: none;
margin-left: 0.5rem;
}
@media (max-width: 560px) {
.btn-secondary { margin-left: 0; margin-top: 0.65rem; display: inline-block; }
}
/* Sections */
section {
padding: 3rem 0;
}
section.alt { background: #fff; }
h2 {
margin: 0 0 0.35rem;
font-size: clamp(1.35rem, 3vw, 1.65rem);
font-weight: 700;
color: var(--text);
}
.section-intro {
color: var(--text-muted);
margin: 0 0 1.75rem;
max-width: 38rem;
}
/* Pain grid */
.grid-3 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.25rem;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.35rem 1.5rem;
box-shadow: var(--shadow);
}
.card h3 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: var(--po-blue);
}
.card p { margin: 0; color: var(--text-muted); font-size: 0.98rem; }
/* Phases */
.phases-head { text-align: center; margin-bottom: 2rem; }
.phases-head h2 { margin-bottom: 0.35rem; }
.phases-head .sub { color: var(--text-muted); margin: 0; font-size: 1rem; }
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
counter-reset: step;
}
.step {
position: relative;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.35rem 1.25rem 1.25rem 1.35rem;
border-top: 4px solid var(--po-blue);
}
.step:nth-child(2) { border-top-color: #e65100; }
.step:nth-child(3) { border-top-color: #7b1fa2; }
.step:nth-child(4) { border-top-color: #c2185b; }
.step-num {
font-size: 0.75rem;
font-weight: 700;
color: var(--po-blue);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.35rem;
}
.step:nth-child(2) .step-num { color: #e65100; }
.step:nth-child(3) .step-num { color: #7b1fa2; }
.step:nth-child(4) .step-num { color: #c2185b; }
.step h3 { margin: 0 0 0.5rem; font-size: 1.05rem; }
.step p { margin: 0; font-size: 0.92rem; color: var(--text-muted); line-height: 1.5; }
/* Why */
.why-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.25rem;
}
.why-card h3 { margin: 0 0 0.5rem; font-size: 1.05rem; }
.why-card p { margin: 0; color: var(--text-muted); font-size: 0.95rem; }
/* Proof */
.proof {
background: linear-gradient(180deg, #f0f7fc 0%, #fff 100%);
border: 1px solid #cfe8fc;
border-radius: var(--radius);
padding: 1.5rem 1.75rem;
margin-top: 2rem;
}
.proof h3 { margin: 0 0 0.5rem; font-size: 1.05rem; color: var(--po-blue-dark); }
.proof h4 {
margin: 1.1rem 0 0.4rem;
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--po-blue);
}
.proof h4:first-of-type { margin-top: 0; }
.proof p { margin: 0 0 0.55rem; font-size: 0.95rem; color: var(--text); line-height: 1.55; }
.proof .fine { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.85rem; margin-bottom: 0; }
.deliverables-strip {
margin-top: 1.75rem;
padding: 1.25rem 1.5rem;
background: #f1f5f9;
border-radius: var(--radius);
border: 1px solid var(--border);
}
.deliverables-strip h3 {
margin: 0 0 0.65rem;
font-size: 0.95rem;
color: var(--text);
}
.deliverables-strip ul {
margin: 0;
padding-left: 1.2rem;
color: var(--text-muted);
font-size: 0.92rem;
line-height: 1.5;
}
.deliverables-strip li { margin-bottom: 0.35rem; }
/* Use cases */
ul.check {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 0.5rem 1.5rem;
}
ul.check li {
padding-left: 1.35rem;
position: relative;
font-size: 0.95rem;
color: var(--text-muted);
}
ul.check li::before {
content: "";
position: absolute;
left: 0;
top: 0.45rem;
width: 0.5rem;
height: 0.5rem;
background: var(--po-teal);
border-radius: 2px;
}
/* CTA footer */
.cta-band {
background: var(--po-blue);
color: #fff;
text-align: center;
padding: 2.75rem 1.25rem;
}
.cta-band h2 { color: #fff; margin-bottom: 0.5rem; }
.cta-band p { opacity: 0.92; margin: 0 0 1.25rem; }
.cta-band .btn { color: var(--po-blue); }
.cta-band .btn.btn-secondary {
color: #fff;
border-color: rgba(255, 255, 255, 0.55);
}
footer.legal {
padding: 1.5rem 1.25rem;
text-align: center;
font-size: 0.85rem;
color: var(--text-muted);
}
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus { left: 1rem; top: 1rem; z-index: 100; background: #fff; padding: 0.5rem; }
</style>
</head>
<body>
<a class="skip-link" href="#main">Zum Inhalt</a>
<header class="site">
<div class="wrap">
<p class="badge">PowerOn &middot; 48h AI Sprint &middot; 2026</p>
<h1>48h AI Sprint</h1>
<p class="hero-lead">Von der Idee zum <strong>pilotfaehigen MVP</strong> in <strong>48 Stunden</strong> &ndash; gebaut mit <strong>KI-gestuetztem Engineering</strong> durch das PowerOn-Team, zum <strong>Fixpreis</strong> und mit Erfolgsanteil. Die KI ist unser Produktivitaetshebel &ndash; Ihr Lieferobjekt ist <strong>funktionierende Software</strong>.</p>
<p class="hero-outcome">Sie erhalten einen <strong>funktionierenden Software-Piloten</strong> im vereinbarten Umfang: spezifiziert, umgesetzt, getestet, dokumentiert &ndash; plus <strong>schriftlich fixierte Erfolgsziele</strong> fuer Abnahme und die zweite Zahlungsstufe.</p>
<div class="pills" role="list">
<span class="pill" role="listitem"><strong>48 Stunden</strong> Umsetzungsblock</span>
<span class="pill" role="listitem"><strong>Fixpreis CHF 9&rsquo;000</strong></span>
<span class="pill" role="listitem"><strong>CHF 7&rsquo;000</strong> erfolgsgebunden</span>
</div>
<p class="pill-hint">CHF 7&rsquo;000 entspricht 78% des Pakets &ndash; wird faellig, sobald die <strong>vorab vereinbarten Erfolgsziele</strong> im Pilot nachgewiesen sind.</p>
<p class="payment-line"><strong>Zahlungslogik:</strong> CHF 2&rsquo;000 zu Projektstart &middot; CHF 7&rsquo;000 bei erfuellten, schriftlich definierten Pilot-Zielen.</p>
<p class="trust-line"><strong>Gemeinsame Entscheidungsbasis:</strong> fester Leistungsrahmen, keine offene Stundenhonorarspirale. Betrieb, Datenfluesse und Freigaben stimmen wir <strong>vor</strong> dem 48h-Block mit Ihrer IT und Compliance ab.</p>
<p>
<a class="btn" href="https://www.poweron.swiss" target="_blank" rel="noopener">15-Minuten-Check &ndash; passt Ihr Vorhaben?</a>
<a class="btn btn-secondary" href="mailto:info@poweron.swiss?subject=48h%20AI%20Sprint%20%E2%80%93%2015-Minuten-Check">E-Mail</a>
</p>
</div>
</header>
<main id="main">
<section class="alt" aria-labelledby="pain-title">
<div class="wrap">
<h2 id="pain-title">Warum viele bei Software-Vorhaben zoegern</h2>
<p class="section-intro">MVPs und Piloten rutschen oft in lange Vorlaufphasen &ndash; Scope wabert, Budget bleibt offen, der erste echte Nutzen kommt zu spaet. Der <strong>48h AI Sprint</strong> verbindet <strong>Tempo im Umsetzungsblock</strong>, einen <strong>festen Leistungsrahmen</strong> und eine <strong>messbare Pilot-Abnahme</strong>.</p>
<div class="grid-3">
<div class="card">
<h3>Scope ohne Schärfe</h3>
<p>Ohne klare Grenzen wächst das Vorhaben ständig &ndash; und Ende offen statt Lieferdatum. Sie brauchen einen <strong>abgeschlossenen Pilot-Schnitt</strong>, der sich bewerten lässt.</p>
</div>
<div class="card">
<h3>Budget ohne Plan</h3>
<p>Stundensätze und offene Schätzungen machen Einkauf und Führung nervös. <strong>Fixpreis plus erfolgsgebundener Anteil</strong> schafft eine gemeinsame Entscheidungsbasis.</p>
</div>
<div class="card">
<h3>Zu lange bis zum MVP</h3>
<p>Klassische Monatsprojekte versanden leicht zwischen Workshops und Spezifikationen. Sie wollen <strong>schnell sehen</strong>, ob Architektur, Integration und Nutzen im Alltag tragen.</p>
</div>
</div>
</div>
</section>
<section aria-labelledby="outcome-title">
<div class="wrap">
<h2 id="outcome-title">Was Sie am Ende haben</h2>
<p class="section-intro">Konkrete Software-Lieferobjekte statt Folienstapel: alles, was Sie brauchen, um im Alltag zu testen, zu messen und intern zu entscheiden &ndash; ob und wie Sie skalieren.</p>
<div class="grid-3">
<div class="card">
<h3>Funktionierender Software-Pilot</h3>
<p>Eine <strong>pilotfaehige Loesung</strong> im vereinbarten Umfang &ndash; z. B. MVP, Integrations-Schnittstelle, Automatisierung oder Migrationsschritt &ndash; mit realistischen Testfaellen aus Ihrem Alltag, nicht als reines Konzeptpapier.</p>
</div>
<div class="card">
<h3>Daten, Integration, Abnahme</h3>
<p><strong>Freigegebene</strong> Datenquellen und <strong>eine</strong> Systemanbindung wie vereinbart. <strong>Erfolgsziele und Abnahmekriterien</strong> sind vor dem 48h-Block schriftlich fixiert &ndash; dieselbe Sprache fuer Fachbereich, IT und Einkauf.</p>
</div>
<div class="card">
<h3>Betrieb &amp; Wissenstransfer</h3>
<p>Kurze Dokumentation, Einweisung und Uebergabe, damit Ihr Team die Loesung im Paketrahmen <strong>selbststaendig weiterbetreiben oder ausbauen</strong> kann &ndash; inklusive Grenzen, Rollen und naechster sinnvoller Schritte.</p>
</div>
</div>
<div class="deliverables-strip">
<h3>Zeitlicher Rahmen (Orientierung)</h3>
<ul>
<li><strong>Vorbereitung</strong> vor dem Block: Discovery, Architektur, Freigaben &ndash; typischerweise einige Arbeitstage.</li>
<li><strong>48 Stunden</strong> intensiver Umsetzungsblock gemeinsam mit Ihrem Team.</li>
<li><strong>Pilotphase</strong> danach: Messfenster fuer die vereinbarten Erfolgsziele (Dauer wie im Angebot festgelegt).</li>
</ul>
</div>
</div>
</section>
<section class="alt" aria-labelledby="flow-title">
<div class="wrap">
<div class="phases-head">
<h2 id="flow-title">Der Ablauf</h2>
<p class="sub"><strong>Vier Phasen</strong> &middot; <strong>48 Stunden</strong> Umsetzung &middot; <strong>gemeinsam</strong> mit Ihrem Team</p>
<p class="sub" style="margin-top:0.5rem">Ihre freigegebenen Systeme und Daten &ndash; umgesetzt durch <strong>KI-gestuetztes Engineering</strong> erfahrener Architektinnen und Architekten auf der PowerOn-Plattform, mit nachvollziehbaren Abl&auml;ufen und wachsender Skalierbarkeit.</p>
</div>
<div class="steps">
<article class="step">
<div class="step-num">Phase 1</div>
<h3>Discovery</h3>
<p>Gemeinsame Analyse: Wir waehlen den Use-Case mit dem groessten Hebel &ndash; messbar und in 48 Stunden realistisch umsetzbar.</p>
</article>
<article class="step">
<div class="step-num">Phase 2</div>
<h3>Design &amp; Architektur</h3>
<p>Software-Architektur, Datenmodell und Integration auf PowerOn. <strong>Erfolgsziele</strong> werden vor dem Start schriftlich fixiert &ndash; sie steuern die Erfolgszahlung.</p>
</article>
<article class="step">
<div class="step-num">Phase 3</div>
<h3>Build &amp; Integration</h3>
<p>Build der Loesung, Anbindung im Vereinbarten, Tests. Ihre Fachseite prueft mit &ndash; parallel zum Build, mit Alltags-Beispielen.</p>
</article>
<article class="step">
<div class="step-num">Phase 4</div>
<h3>Deploy &amp; Handover</h3>
<p>Go-Live in Ihrer <strong>vereinbarten PowerOn-Umgebung</strong>, Wissenstransfer und Dokumentation f&uuml;r den laufenden Betrieb.</p>
</article>
</div>
</div>
</section>
<section aria-labelledby="why-title">
<div class="wrap">
<h2 id="why-title">Warum PowerOn</h2>
<p class="section-intro">Nicht nur schnell &ndash; strukturell besser: <strong>AI-augmented Engineering</strong> auf einer Plattform mit klaren Rollen und nachvollziehbaren Ergebnissen.</p>
<div class="why-grid">
<div class="card why-card">
<h3>AI-Augmented-Engineering-Plattform</h3>
<p>PowerOn ist keine Ad-hoc-Einzelloesung. Wir bauen Ihre Loesung auf einer Umgebung, auf der <strong>KI-gestuetzte Produktivitaet</strong> und klassische Softwarequalitaet zusammenkommen &ndash; Skalierung, Rechte, Nachvollziehbarkeit inbegriffen.</p>
</div>
<div class="card why-card">
<h3>Erfolg teilen</h3>
<p><strong>CHF 7&rsquo;000</strong> (78% von CHF 9&rsquo;000) werden erst faellig, wenn die <strong>vorab schriftlich vereinbarten Erfolgsziele</strong> im Pilot nachgewiesen sind.</p>
</div>
<div class="card why-card">
<h3>Daten nach Vorgabe</h3>
<p>Sicherheit und Compliance fuer Ihr Softwareprojekt: Hosting- und Verarbeitungsmodell stimmen wir mit Ihrer IT ab &ndash; vom Schweizer Rechenzentrum bis zu definierten Cloud-Szenarien.</p>
</div>
<div class="card why-card">
<h3>Enablement</h3>
<p>Wissenstransfer ist fester Bestandteil. Ihr Team versteht Bedienung, Grenzen und naechste Schritte im vereinbarten Rahmen.</p>
</div>
</div>
<aside class="proof" aria-labelledby="proof-title">
<h3 id="proof-title">Referenzcase (anonymisiert) &ndash; Backend-Migration unter Zeitdruck</h3>
<!-- Anonym: keine Kundennamen. Variante mit Namensnennung: siehe launch48-deck-presentation.md Variante A. -->
<h4>Ausgangslage</h4>
<p>Ein fuehrendes <strong>Schweizer Softwarehaus</strong> modernisierte ein <strong>geschaeftskritisches Plattform-Backend</strong> (DATA-Hub-Umfeld): ueber Jahre gewachsen, mehrere fruehere Partner, <strong>erhebliche technische Schulden</strong> und <strong>Sicherheitsbefunde</strong> (u.a. aus Pentests). Zusaetzlich <strong>Wissensluecken</strong> im Bestand &ndash; strukturiert geklaert in <strong>10 Themenbereichen</strong> mit <strong>49 Detailfragen</strong> vor der Umsetzung. Ein vergleichbares Vorhaben waere klassisch oft mit <strong>3&ndash;6 Monaten</strong> und hohem internen Aufwand geplant worden.</p>
<h4>Vorgehen (4 Phasen, KI-gestuetzt, Human-in-the-Loop)</h4>
<p><strong>1. Analyse &amp; Dokumentation</strong> (ca. 2&ndash;3 Tage): Inventur und strukturierte Analyse von <strong>47 TypeScript-Dateien</strong>, Geschaeftslogik und Abhaengigkeiten; Ergebnis in dokumentierter Form fuer Entscheid und Migration.</p>
<p><strong>2. Technische Spezifikation</strong> (ca. 2&ndash;3 Tage): Zielarchitektur <strong>.NET / C#</strong> (u.a. ORM, APIs, Anbindungen an Messaging, Object Storage, Identity); <strong>Review-Sessions</strong> mit dem Kundenteam; feste <strong>Acceptance Criteria</strong>.</p>
<p><strong>3. Execution</strong> (ca. <strong>1 Tag</strong> fuer die eigentliche Code-Migration): KI unterstuetzt die Uebersetzung <strong>Node.js/TypeScript &rarr; .NET/C#</strong>; erfahrene Architektinnen und Architekten <strong>validieren jedes Modul</strong>; Tests und Integration laufen parallel.</p>
<p><strong>4. Testing &amp; Uebergabe</strong> (ca. 2&ndash;3 Tage): Mock- und End-to-End-Validierung, Testdaten, automatisierte Tests; <strong>Wissenstransfer</strong> (Training on the Job, aufgezeichnete Session) &ndash; damit das interne Team den Ansatz <strong>eigenstaendig fortsetzen</strong> kann.</p>
<h4>Ergebnis</h4>
<p>Gesamtprojekt <strong>11 Kalendertage</strong> vom Kickoff bis zur uebergabefaehigen .NET/C#-Basis &ndash; bei gleichzeitiger <strong>Bereinigung von Tech Debt</strong>, Adressierung relevanter <strong>Security-Punkte</strong> und <strong>vollstaendigem Enablement</strong> des Kundenteams. Relativ zur typischen Planungsgroessenordnung <strong>mehrmonatiger</strong> klassischer Migration: <strong>ca. 10x schnellere</strong> Time-to-Result in diesem Fall (kein Uebertragungsversprechen fuer jedes Projekt).</p>
<h4>Transfer zum 48h AI Sprint</h4>
<p>Dieselbe Lieferdisziplin nutzen wir fuer Ihren <strong>Software-Piloten</strong>: <strong>klarer Scope</strong>, feste Phasen, nachvollziehbare Zwischenresultate, messbare Abnahme &ndash; plus <strong>Wissenstransfer</strong> als fester Bestandteil, nicht als Zusatz.</p>
<p class="fine">Kein Garantieversprechen pro Use Case; Dauer und Aufwand haengen von Ausgangslage und Freigaben ab. <strong>Vollstaendige Case Study und namentliche Referenz</strong> auf Anfrage und nur mit Kundenfreigabe.</p>
</aside>
<h2 style="margin-top:2.5rem;">Geeignet fuer</h2>
<p class="section-intro">Typische Einstiege &ndash; immer im vereinbarten Paketrahmen:</p>
<ul class="check">
<li><strong>MVP-</strong> und Pilotbau (neue Produkte, Schnittstellen, Prozesse)</li>
<li><strong>Backend-Migration</strong> und Stack-Wechsel (wie im Referenzcase)</li>
<li><strong>Systemintegration</strong> (APIs, Messaging, Identity, Datenpipelines)</li>
<li><strong>Prozessautomatisierung</strong> und interne Tools</li>
<li><strong>Prototyping</strong> und Machbarkeitsnachweis vor groesserem Budget</li>
<li><strong>Legacy-Modernisierung</strong> in abgegrenztem Schnitt</li>
<li>Dokumentenverarbeitung, Reporting, Freigabe-Workflows &ndash; im vereinbarten Umfang</li>
</ul>
</div>
</section>
<section class="alt" aria-labelledby="team-title">
<div class="wrap">
<h2 id="team-title">Das PowerOn-Team</h2>
<p class="section-intro">Wir kombinieren Strategie, Technologie und Umsetzungskraft. Dieses Team fuehrt Discovery, Architektur, Build und Handover im <strong>48h AI Sprint</strong> &ndash; End-to-End, mit klaren Meilensteinen.</p>
<div class="grid-3">
<div class="card">
<h3>Patrick Motsch</h3>
<p><strong>CEO/CTO</strong> &ndash; Steuert technische Umsetzung und komplexe IT-Projekte; sorgt dafuer, dass Ihr Software-Pilot in PowerOn produktiv wird und betreibbar bleibt.</p>
</div>
<div class="card">
<h3>Ida Dittrich</h3>
<p><strong>Product Architect</strong> &ndash; Verantwortet Architektur, Qualitaet und Machbarkeit auf der PowerOn-Plattform &ndash; damit Scope, Daten und Integration im 48h-Rahmen stimmig bleiben.</p>
</div>
<div class="card">
<h3>Stephan Schellworth</h3>
<p><strong>Business Integration</strong> &ndash; Verbindet Use Case, Stakeholder und Projektsteuerung &ndash; damit Erfolgsziele vor dem Start klar sind und der Pilot messbar bleibt.</p>
</div>
</div>
</div>
</section>
</main>
<div class="cta-band">
<div class="wrap">
<h2>Bereit fuer den 15-Minuten-Check?</h2>
<p>Wir sagen ehrlich, ob Ihr Vorhaben zum <strong>48h AI Sprint</strong> passt &ndash; ohne Druck.</p>
<a class="btn" href="https://www.poweron.swiss" target="_blank" rel="noopener">15-Minuten-Check &ndash; poweron.swiss</a>
<a class="btn btn-secondary" href="mailto:info@poweron.swiss?subject=48h%20AI%20Sprint%20%E2%80%93%2015-Minuten-Check">E-Mail</a>
</div>
</div>
<footer class="legal">
<div class="wrap">
<p><strong>PowerOn AG</strong> &middot; Birmensdorferstrasse 94, 8003 Zuerich &middot; <a href="https://www.poweron.swiss">www.poweron.swiss</a></p>
<p>Fliesstext und Vertragsdetails: siehe <a href="./poweron-launch48-offer.md">poweron-launch48-offer.md</a> (Markdown).</p>
</div>
</footer>
</body>
</html>

View file

@ -1,52 +0,0 @@
# Prompts: KI-Betriebssystem-Infografik (Google Bild-KI / Gemini)
Für die Generierung einer alternativen oder verfeinerten Visualisierung in **Google AI Studio**, der **Gemini-App** (Bildfunktion) oder einem vergleichbaren Angebot (z. B. Imagen). Modell im UI wählen (intern manchmal anders benannt).
**Zugehörige Code-Folie (HTML, 1920×1080):** [poweron-ki-betriebssystem-slide.html](./poweron-ki-betriebssystem-slide.html)
---
## Master-Prompt (ein Bild, gesamte Folie)
```
Professional German B2B infographic slide, 16:9 landscape, 1920x1080. Title at top: "Das moderne KI-Betriebssystem" with subtitle "PowerOn". Light grey background (#f8fafc).
Left: flat-design rocket pointing up, five horizontal colored segments from top to bottom: dark blue, medium blue, light blue-grey, cream, orange flame at bottom; small dark fins on sides of cream section. White line icons centered in each segment: dashboard gauge; server with nodes; hand holding gear; crossed wrench and screwdriver; document with magnifying glass. Labels on rocket segments in German: Interface, Orchestrierung, Skills, Modelle, Daten.
Center: five stacked white rounded cards with left color tabs matching rocket segments. Each card: bold German title, subtitle, two bullet lines with arrow symbols. Content exactly:
(1) Interface Layer (Interaktion) — Einstiegspunkt für User & Systeme — Chat, Spracheingabe, Oberflächen; API & Webhooks
(2) Orchestrierung & Agenten — Entscheiden & Abläufe planen — Aufgaben delegieren; Skills & Tools koordinieren
(3) Skill- & Tool-Layer — Ausführung konkreter Aufgaben — Prozesse, Aktionen, Integration; API-Aufrufe, Funktionen & Automationen
(4) KI-Modelle — Spezialisierte Modelle — Generierung, Analyse & Klassifikation; Ausführung einzelner Denkschritte
(5) Daten- & Kontextschicht — Dokumente & Wissen — Vektordatenbanken; Retrieval & Historien
Ribbon connectors between rocket segments and cards with subtle folded ribbon 3D effect.
Far left vertical bar: vertical text "Regeln & Steuerung", subtext horizontal small: Zugriffsrechte & Rollen, Entscheidungsgrenzen, Validierung & Freigaben. Far right vertical bar: vertical text "Transparenz & Kontrolle", subtext: Nachvollziehbarkeit, Qualitätssicherung, Kosten- und Nutzungsübersicht. Dark blue caps on bars.
Typography: clean geometric sans-serif, high legibility, no watermark, no stock photo people, no clutter.
```
---
## Kurz-Prompt (Iteration / Stil-Fix)
```
Same layout as a McKinsey-style architecture infographic: rocket left, five layered segments, five matching explanation cards center, two slim governance columns with vertical German labels. Colors: corporate blues #12579b and #1976d2, light grey background, orange accent only for data layer flame. Flat vector, crisp edges, presentation-ready.
```
---
## Negativ / Vermeiden (an den Bild-Prompt anhängen)
```
Avoid: 3D photorealistic rocket, cartoon style, low resolution, illegible micro-text, English-only text, logos of OpenAI/Google/Microsoft, busy backgrounds, isometric clutter, more than five main layers.
```
---
## Text-Prompt für Gemini (Copy-Feinschliff, kein Bild)
```
Du bist Redakteur für ein deutschsprachiges Enterprise-Pitchdeck. Überprüfe die fünf Schichten eines "KI-Betriebssystems" (Interface, Orchestrierung/Agenten, Skills/Tools, Modelle, Daten/Kontext) plus die beiden Querschnittsthemen Regeln & Steuerung und Transparenz & Kontrolle. Schlage je Schicht maximal zwei prägnante Bulletpoints vor (jeweils unter 90 Zeichen), konsistent mit PowerOn-Messaging (Plattform, Governance, schnelle Lieferung, Auditierbarkeit). Gib nur die optimierte Liste aus, keine Einleitung.
```

View file

@ -1,467 +0,0 @@
<!DOCTYPE html>
<html lang="de-CH">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1920">
<title>PowerOn Das moderne KI-Betriebssystem (16:9)</title>
<style>
:root {
--po-blue: #1976d2;
--po-blue-dark: #12579b;
--po-seg-light: #b8d4f0;
--po-seg-cream: #f0ebe3;
--po-flame: #f57c00;
--po-flame-light: #ff9800;
--text: #1a1a2e;
--text-muted: #5c5c6f;
--bg: #f8fafc;
--card: #ffffff;
--icon: rgba(255, 255, 255, 0.95);
--icon-dark: #12579b;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #e2e8f0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--text);
}
.stage {
width: 1920px;
height: 1080px;
background: var(--bg);
overflow: hidden;
flex-shrink: 0;
display: flex;
flex-direction: column;
padding: 28px 36px 32px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
}
.slide-header {
text-align: center;
margin-bottom: 18px;
}
.slide-header h1 {
margin: 0;
font-size: 2.05rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--po-blue-dark);
}
.slide-header p {
margin: 6px 0 0;
font-size: 1.15rem;
font-weight: 600;
color: var(--po-blue);
}
.slide-grid {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 78px 210px 46px minmax(0, 1fr) 78px;
grid-template-rows: repeat(5, minmax(0, 1fr));
gap: 0 0;
column-gap: 0;
}
/* Governance columns */
.gov {
grid-row: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--card);
border-radius: 10px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 12px rgba(18, 87, 155, 0.06);
padding: 12px 8px;
position: relative;
}
.gov::before,
.gov::after {
content: "";
position: absolute;
left: 4px;
right: 4px;
height: 10px;
background: var(--po-blue-dark);
border-radius: 3px;
}
.gov::before { top: 8px; }
.gov::after { bottom: 8px; }
.gov-left { grid-column: 1; }
.gov-right { grid-column: 5; }
.gov-title {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 0.95rem;
font-weight: 700;
color: var(--po-blue-dark);
letter-spacing: 0.04em;
text-align: center;
flex: 0 0 auto;
max-height: 62%;
}
.gov-sub {
font-size: 0.62rem;
line-height: 1.35;
color: var(--text-muted);
text-align: center;
padding: 10px 2px 0;
writing-mode: horizontal-tb;
max-width: 100%;
}
/* Per-row cells: col 2 = rocket tier, col 3 = ribbon, col 4 = card */
.rocket-tier {
grid-column: 2;
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin: 0 8px;
min-height: 0;
}
.rocket-tier .tier-body {
width: 100%;
height: 100%;
min-height: 72px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.rocket-tier.t-interface .tier-body {
background: var(--po-blue-dark);
border-radius: 14px 14px 0 0;
margin-top: 38px;
}
.rocket-nose {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 52px solid transparent;
border-right: 52px solid transparent;
border-bottom: 42px solid var(--po-blue-dark);
z-index: 1;
}
.rocket-tier.t-interface { align-items: flex-end; padding-top: 0; }
.rocket-tier.t-interface .wrap { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; }
.rocket-tier.t-orch .tier-body { background: var(--po-blue); }
.rocket-tier.t-skills .tier-body { background: var(--po-seg-light); }
.rocket-tier.t-skills .tier-label { color: var(--po-blue-dark); text-shadow: none; }
.rocket-tier.t-models .tier-body {
background: var(--po-seg-cream);
border-radius: 0 0 6px 6px;
}
.rocket-tier.t-models .fin {
position: absolute;
bottom: 8px;
width: 0;
height: 0;
border-style: solid;
z-index: 0;
}
.rocket-tier.t-models .fin-l {
left: -20px;
border-width: 0 22px 56px 0;
border-color: transparent var(--po-blue-dark) transparent transparent;
}
.rocket-tier.t-models .fin-r {
right: -20px;
border-width: 0 0 56px 22px;
border-color: transparent transparent transparent var(--po-blue-dark);
}
.rocket-tier.t-data .tier-body {
background: linear-gradient(180deg, var(--po-flame-light) 0%, var(--po-flame) 100%);
clip-path: polygon(15% 0%, 85% 0%, 100% 100%, 50% 85%, 0% 100%);
min-height: 64px;
margin-top: 2px;
}
.tier-label {
position: absolute;
bottom: 6px;
left: 0;
right: 0;
text-align: center;
font-size: 0.65rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
pointer-events: none;
}
.rocket-tier.t-models .tier-label,
.rocket-tier.t-data .tier-label { color: var(--po-blue-dark); text-shadow: none; }
.rocket-tier.t-data .tier-label { color: #fff; bottom: 10px; }
/* Ribbons */
.ribbon {
grid-column: 3;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 2px;
}
.ribbon-inner {
width: 100%;
height: 72%;
min-height: 48px;
position: relative;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(0, 0, 0, 0.04) 100%);
transform: skewY(-2deg);
border-radius: 0 4px 4px 0;
box-shadow: inset -2px 0 4px rgba(0, 0, 0, 0.06), 2px 2px 6px rgba(18, 87, 155, 0.08);
}
.ribbon-inner::after {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
border-radius: 2px;
}
.ribbon.row-1 .ribbon-inner::after { background: var(--po-blue-dark); }
.ribbon.row-2 .ribbon-inner::after { background: var(--po-blue); }
.ribbon.row-3 .ribbon-inner::after { background: var(--po-seg-light); }
.ribbon.row-4 .ribbon-inner::after { background: #c4b8a8; }
.ribbon.row-5 .ribbon-inner::after { background: var(--po-flame); }
/* Cards */
.layer-card {
display: flex;
align-items: stretch;
margin: 4px 0 4px 10px;
min-height: 0;
}
.layer-card .card-shell {
flex: 1;
display: flex;
background: var(--card);
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 14px rgba(25, 118, 210, 0.07);
overflow: hidden;
min-height: 0;
}
.layer-card .tab {
width: 14px;
flex-shrink: 0;
}
.layer-card.row-1 .tab { background: var(--po-blue-dark); }
.layer-card.row-2 .tab { background: var(--po-blue); }
.layer-card.row-3 .tab { background: var(--po-seg-light); }
.layer-card.row-4 .tab { background: #c4b8a8; }
.layer-card.row-5 .tab { background: linear-gradient(180deg, var(--po-flame-light), var(--po-flame)); }
.layer-card .card-body {
padding: 10px 16px 10px 14px;
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.layer-card h3 {
margin: 0 0 2px;
font-size: 0.98rem;
font-weight: 700;
color: var(--po-blue-dark);
line-height: 1.2;
}
.layer-card .sub {
margin: 0 0 6px;
font-size: 0.78rem;
font-weight: 600;
color: var(--text-muted);
}
.layer-card ul {
margin: 0;
padding: 0;
list-style: none;
}
.layer-card li {
font-size: 0.76rem;
line-height: 1.4;
color: var(--text);
padding-left: 0.85em;
text-indent: -0.85em;
}
.layer-card li + li { margin-top: 2px; }
/* Row placement (row-N on same element as component) */
.rocket-tier.row-1, .ribbon.row-1, .layer-card.row-1 { grid-row: 1; }
.rocket-tier.row-2, .ribbon.row-2, .layer-card.row-2 { grid-row: 2; }
.rocket-tier.row-3, .ribbon.row-3, .layer-card.row-3 { grid-row: 3; }
.rocket-tier.row-4, .ribbon.row-4, .layer-card.row-4 { grid-row: 4; }
.rocket-tier.row-5, .ribbon.row-5, .layer-card.row-5 { grid-row: 5; }
.rocket-tier { grid-column: 2; }
.ribbon { grid-column: 3; }
.layer-card { grid-column: 4; }
/* SVG icons */
.tier-icon { width: 44px; height: 44px; color: var(--icon); }
.rocket-tier.t-models .tier-icon,
.rocket-tier.t-skills .tier-icon { color: var(--icon-dark); }
.rocket-tier.t-data .tier-icon { color: #fff; }
</style>
</head>
<body>
<div class="stage" role="img" aria-label="Infografik: Das moderne KI-Betriebssystem PowerOn mit fünf Schichten und Governance-Säulen">
<header class="slide-header">
<h1>Das moderne KI-Betriebssystem</h1>
<p>PowerOn</p>
</header>
<div class="slide-grid">
<aside class="gov gov-left">
<div class="gov-title">Regeln &amp; Steuerung</div>
<div class="gov-sub">Zugriffsrechte &amp; Rollen, Entscheidungsgrenzen, Validierung &amp; Freigaben</div>
</aside>
<!-- Row 1: Interface -->
<div class="row-1 rocket-tier t-interface">
<div class="wrap" style="width:100%;height:100%;">
<div class="rocket-nose" aria-hidden="true"></div>
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<circle cx="24" cy="24" r="16" stroke-opacity="0.35"/>
<path d="M24 12 v8 M24 28 v8 M12 24 h8 M28 24 h8"/>
<circle cx="24" cy="24" r="6"/>
</svg>
<span class="tier-label">Interface</span>
</div>
</div>
</div>
<div class="row-1 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-1 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Interface Layer (Interaktion)</h3>
<p class="sub">Einstiegspunkt für User &amp; Systeme</p>
<ul>
<li>➔ Chat, Spracheingabe, Oberflächen</li>
<li>➔ API &amp; Webhooks</li>
</ul>
</div>
</div>
</article>
<!-- Row 2: Orchestrierung -->
<div class="row-2 rocket-tier t-orch">
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<rect x="8" y="22" width="18" height="16" rx="2"/>
<path d="M26 26 h10 M26 30 h10 M26 34 h10"/>
<circle cx="38" cy="18" r="5"/>
<path d="M33 22 L36 20 M26 22 L22 18"/>
</svg>
<span class="tier-label">Orchestrierung</span>
</div>
</div>
<div class="row-2 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-2 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Orchestrierung &amp; Agenten</h3>
<p class="sub">Entscheiden &amp; Abläufe planen</p>
<ul>
<li>➔ Aufgaben delegieren</li>
<li>➔ Skills &amp; Tools koordinieren</li>
</ul>
</div>
</div>
</article>
<!-- Row 3: Skills -->
<div class="row-3 rocket-tier t-skills">
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M14 32 c8 -4 12 -12 12 -20 c0 -4 -2 -6 -5 -6 c-4 0 -7 4 -7 10 c0 6 4 10 10 10 z"/>
<circle cx="30" cy="22" r="9"/>
<path d="M30 16 v12 M24 22 h12"/>
</svg>
<span class="tier-label">Skills</span>
</div>
</div>
<div class="row-3 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-3 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Skill- &amp; Tool-Layer</h3>
<p class="sub">Ausführung konkreter Aufgaben</p>
<ul>
<li>➔ Prozesse, Aktionen, Integration</li>
<li>➔ API-Aufrufe, Funktionen &amp; Automationen</li>
</ul>
</div>
</div>
</article>
<!-- Row 4: Modelle -->
<div class="row-4 rocket-tier t-models">
<span class="fin fin-l" aria-hidden="true"></span>
<span class="fin fin-r" aria-hidden="true"></span>
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M12 38 L22 10 L38 38 Z M18 28 h14"/>
<line x1="26" y1="18" x2="32" y2="32"/>
</svg>
<span class="tier-label">Modelle</span>
</div>
</div>
<div class="row-4 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-4 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>KI-Modelle</h3>
<p class="sub">Spezialisierte Modelle</p>
<ul>
<li>➔ Generierung, Analyse &amp; Klassifikation</li>
<li>➔ Ausführung einzelner „Denkschritte“</li>
</ul>
</div>
</div>
</article>
<!-- Row 5: Daten -->
<div class="row-5 rocket-tier t-data">
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<rect x="10" y="8" width="22" height="28" rx="2"/>
<line x1="14" y1="16" x2="28" y2="16"/>
<line x1="14" y1="22" x2="26" y2="22"/>
<circle cx="34" cy="30" r="9"/>
<line x1="40" y1="36" x2="44" y2="40" stroke-linecap="round"/>
</svg>
<span class="tier-label">Daten</span>
</div>
</div>
<div class="row-5 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-5 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Daten- &amp; Kontextschicht</h3>
<p class="sub">Dokumente &amp; Wissen</p>
<ul>
<li>➔ Vektordatenbanken</li>
<li>➔ Retrieval &amp; Historien</li>
</ul>
</div>
</div>
</article>
<aside class="gov gov-right">
<div class="gov-title">Transparenz &amp; Kontrolle</div>
<div class="gov-sub">Nachvollziehbarkeit, Qualitätssicherung, Kosten- und Nutzungsübersicht</div>
</aside>
</div>
</div>
</body>
</html>

View file

@ -1,176 +0,0 @@
# PowerOn Launch48
**Ihre erste produktive KI-Loesung auf der PowerOn-Plattform in 48 Stunden.**
*Zum Weitergeben an Kundinnen und Kunden. Verstaendlich fuer Geschaeftsfuehrung, Fachbereiche und IT.*
---
## Warum viele Unternehmen bei KI noch zoegern
Daten liegen heute oft **verteilt**: in Ordnern, Mails, Ticketsystemen und Fachapplikationen. Gleichzeitig wuenschen sich Teams **schnellere Antworten** und **weniger manuelle Routine**.
Viele erste KI-Versuche scheitern nicht an der Technik allein, sondern daran, dass
- **kein klarer Anwendungsfall** im Fokus steht,
- **wichtige Unterlagen** nicht sicher und gezielt genutzt werden,
- **generische Chat-Tools** ohne Freigaben genutzt werden mit Risiko fuer Datenschutz und Qualitaet,
- **lange Vorprojekte** geplant werden, bevor ueberhaupt etwas Greifbares entsteht.
**PowerOn Launch48** ist das Gegenteil davon: ein **fokussiertes Paket** mit klarem Ablauf, **Fixpreis** und einem **konkreten Ergebnis** auf **Ihrer** PowerOn-Umgebung.
---
## Was PowerOn ist in einem Satz
**PowerOn** ist eine **Unternehmensplattform fuer kuenstliche Intelligenz**: Teams arbeiten mit KI **dort, wo Ihre Informationen und Prozesse ohnehin sind** mit klaren Rollen, nachvollziehbaren Ablaeufen und ohne dass Sie die Kontrolle ueber sensible Inhalte verlieren.
Mehr zur Plattform: [product-teaser-poweron.md](./product-teaser-poweron.md) (interne Vertiefung).
---
## Was Sie mit Launch48 bekommen
**Am Ende steht keine Theorie, sondern etwas Greifbares:** eine **einsatznahe KI-Loesung** auf der **PowerOn-Plattform** typischerweise Ihr erster **KI-Assistent** fuer **einen** klar abgegrenzten Prozess, mit **Ihren freigegebenen Daten** und im vereinbarten Rahmen einer **Systemanbindung**. Details zum Ablauf folgen im naechsten Abschnitt.
Damit koennen Sie **realistisch einschaetzen**, welchen Nutzen KI in **Ihrem** Unternehmen bringt und darauf aufbauen.
---
## Der Ablauf: vier Phasen, 48 Stunden, ein gemeinsames Team
**Kopfzeile (z. B. fuer Praesentation / Folie):** *Vier Phasen · 48 Stunden · gemeinsam mit Ihrem Team*
**Subline:** *Ein Workspace. Ihre Daten. Die passenden KI-Faehigkeiten. Gebündelt auf PowerOn.*
Die **48 Stunden** sind der **konzentrierte Umsetzungsblock**. Davor liegt eine **kurze Vorbereitung** (Discovery, Architektur, Freigaben); danach ein **Pilot** mit ausgewaehlten Nutzerinnen und Nutzern, in dem wir die **vereinbarten Erfolgsziele** messen.
### Phase 1: Discovery
**Gemeinsame Analyse Ihrer Prozesse.** Wir identifizieren den Anwendungsfall mit dem groessten **Automatisierungs- bzw. Entlastungspotenzial** so, dass er **messbar** und **in 48 Stunden realistisch** umsetzbar ist (z. B. wiederkehrende Anfragen, Erstbearbeitungen, standardisierte Pruefschritte).
**Ergebnis:** Ein **scharf umrissener Use-Case** und klare Erwartungen.
### Phase 2: Design und Architektur
**KI-Architektur, Datenmodell und Integration auf der PowerOn-Plattform.** Wir legen fest, **welche Datenquellen** und **welche Anbindung** im Paketrahmen vorgesehen sind. Zentral: die **messbaren Erfolgsziele**, die **vor dem Start schriftlich fixiert** werden und ueber die **zweite Zahlungsstufe** (CHF 7000) entscheiden damit Einkauf, Fachbereich und IT dieselbe Sprache sprechen.
**Ergebnis:** **Fester Plan** fuer Umsetzung und Abnahme.
### Phase 3: Build und Integration
**Entwicklung des KI-Assistenten, Anbindung an Ihre Systeme (im Vereinbarten), Testing.** Ihre **Fachanwenderinnen und -anwender** pruefen parallel zum Build mit (**„Mensch prueft mit“** statt reiner Black-Box) mit realistischen **Testfaellen aus dem Alltag**.
**Ergebnis:** Stabile, einsatznahe Loesung vor dem Go-Live.
### Phase 4: Deploy und Handover
**Go-Live in Ihrer vereinbarten PowerOn-Umgebung** (Pilot- oder Produktions-Instanz je nach Vereinbarung kein generisches „Internet-KI-Experiment“), **Wissenstransfer** und **Dokumentation**, damit Ihr Team den Assistenten **vom ersten Tag an** im vereinbarten Rahmen **selbst betreiben** kann.
**Ergebnis:** Uebergabe mit kurzer **Einweisung** und **Nachschlagewerk** fuer den Betrieb.
### Zeitlicher Ablauf auf einen Blick
| Phase | Inhalt |
| --- | --- |
| **Vorbereitung** | Discovery, Design/Architektur, Freigaben in der Regel **einige Arbeitstage** vor dem 48h-Block |
| **Umsetzung** | **48 Stunden** intensiv gemeinsam |
| **Pilot** | z. B. **ca. 10 Arbeitstage** Messfenster fuer die vereinbarten Erfolgsziele (wie im Angebot festgelegt) |
### Am Ende liegen fuer Sie vor (Kernlieferobjekte)
- eine **funktionierende KI-Loesung** auf PowerOn fuer den **definierten** Anwendungsfall,
- **konfigurierte Datenquellen** und **eine** Systemanbindung **im vereinbarten Umfang**,
- **kurze Dokumentation** und **Einweisung** fuer Ihr Team.
### Rollen: Sie und wir
| | **Sie** | **Wir** |
| --- | --- | --- |
| **Verantwortung** | Fach-Owner, IT-Zugang, Freigaben (Datenschutz/Compliance nach Bedarf), **Pilotgruppe** | Architektur, Umsetzung, Qualitaet, Begleitung im 48h-Block |
**Vertrauen in einem Satz:** Kein undurchsichtiges Einzel-Tool im Browser sondern **PowerOn** mit **Ihren freigegebenen Daten** und **klaren Grenzen**.
---
## Fuer wen ist Launch48 gedacht?
Launch48 richtet sich an Organisationen, die **wissen, dass KI relevant ist**, aber **noch keinen einfachen Weg** gefunden haben, **schnell und kontrolliert** zu starten oder die **bereits eine Idee** haben und diese **in Wochen, nicht Monaten** greifbar machen wollen.
**Typische Situationen:**
- Viele **wiederkehrende Anfragen** (Kundenservice, interne Support-Themen, Fachfragen).
- **Wissen in Dokumenten**, das immer wieder neu gesucht und zusammengefasst wird.
- **Bedarf an Geschwindigkeit** ohne monatelange Evaluationsprojekte.
- **Wuensche nach Kontrolle** ueber Daten und Rollen statt „KI irgendwo im Browser“.
---
## Investition einfach und planbar
| | Betrag | Wann |
| --- | --- | --- |
| **Gesamtpaket** | **CHF 9000** (zzgl. MWSt. falls anwendbar) | Fixpreis fuer das vereinbarte Paket |
| **Zu Beginn** | **CHF 2000** | Wenn Sie starten und wir die Umsetzung freigeben |
| **Nach dem Pilot** | **CHF 7000** | Wenn die **gemeinsam festgelegten Erfolgsziele** im vereinbarten Messzeitraum erreicht sind |
**Was bedeutet das fuer Sie?** Der Preis ist bewusst **frueh transparent**, weil Launch48 kein offenes Beratungsprojekt ist, sondern ein **klar abgegrenztes Paket**. Sie investieren zu Beginn einen **kleineren Teil**. Der groessere Teil ist an **messbare, vorab beschriebene Ziele** geknuepft z. B. Zeitersparnis pro typischem Vorgang, Zufriedenheit der Pilotgruppe oder Fehlerquote. **Genau diese Ziele** legen wir **vor dem Start** schriftlich fest, damit alle dasselbe verstehen.
So wird aus der Zahl kein Risikozeichen, sondern ein **Vertrauenssignal**: klarer Rahmen, klares Ergebnis, klare Abnahme. Details und Grenzen des Pakets besprechen wir **transparent** im Erstgespraech (Umfang der Datenquellen, eine Systemanbindung im Standardrahmen, Groesse der Pilotgruppe).
---
## Was Sie von uns erwarten koennen
- **Erfahrene Begleitung** von Anfang bis Pilotende
- **Klare Kommunikation** wenig Buzzwords, viel Nutzen
- **PowerOn als Plattform** skalierbar, wenn Sie verlaengern moechten
- **Respekt vor Ihren Freigaben** Datenschutz und IT-Security ernst nehmen
---
## Ihr naechster Schritt
**Kurzes Erstgespraech (ca. 1530 Minuten):** Passt Ihr Thema zu Launch48? Wir sagen Ihnen ehrlich **ja, nein oder noch nicht**.
**Kontakt**
- **Web:** [www.poweron.swiss](https://www.poweron.swiss)
- **Adresse:** PowerOn AG, Birmensdorferstrasse 94, 8003 Zuerich, Schweiz
**Ansprechpartner**
- Patrick Motsch
- Ida Dittrich
- Stephan Schellworth
*Bitte ersetzen Sie bei Bedarf durch eine zentrale E-Mail-Adresse oder Buchungslink fuer Ihr Vertriebsteam.*
---
## Hauefige Fragen (kurz)
**Brauchen wir schon PowerOn?**
Wir klaeren mit Ihnen, ob eine **Pilot-Umgebung** oder Ihre bestehende Instanz passt.
**Ist das nur ein Prototyp?**
Nein Ziel ist eine **einsatznahe Loesung** fuer einen **definierten** Anwendungsfall. Was **nicht** im Paket liegt (z. B. Rollout auf die ganze Firma), sagen wir klar dazu.
**Was, wenn unsere IT Zeit braucht?**
Dann verschieben wir den Start **Zugang und Freigaben** muessen passen, sonst wird niemand gluecklich.
**Duerfen wir das Dokument weitergeben?**
Ja. Es ist dafuer gedacht, intern weiterzureichen (Geschaeftsfuehrung, Fachbereich, IT).
---
## Weitere Unterlagen (optional)
- **Onepager im Browser (HTML, teilbar):** [launch48-offer-page.html](./launch48-offer-page.html)
- **4-Folien-Deck (Copy fuer PDF/Canva/PPT):** [launch48-deck-presentation.md](./launch48-deck-presentation.md)
- **Kurzfassung zum Drucken:** [flyer-poweron-48h-agent.md](./flyer-poweron-48h-agent.md)
- **Technisches Vertiefungs- und Lieferkonzept (intern):** [concept-poweron-48h-agent-offer.md](./concept-poweron-48h-agent-offer.md)
- **Beispiel-Verlauf (Illustration, kein Echt-Kunde):** [case-study-poweron-48h-agent.md](./case-study-poweron-48h-agent.md)
---
*PowerOn Launch48 strukturiert vorbereitet, in 48 Stunden umgesetzt, messbar abgeschlossen.*

View file

@ -1,529 +0,0 @@
<!DOCTYPE html>
<html lang="de-CH">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1920">
<title>PowerOn Die KI-Plattform (Layer-Schaubild, 16:9)</title>
<style>
:root {
--po-blue: #1976d2;
--po-blue-dark: #12579b;
--po-industry: #4a8ec8;
--po-seg-light: #b8d4f0;
--po-seg-cream: #f0ebe3;
--po-flame: #f57c00;
--po-flame-light: #ff9800;
--text: #1a1a2e;
--text-muted: #5c5c6f;
--bg: #f8fafc;
--card: #ffffff;
--icon: rgba(255, 255, 255, 0.95);
--icon-dark: #12579b;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #e2e8f0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--text);
}
.stage {
width: 1920px;
height: 1080px;
background: var(--bg);
overflow: hidden;
flex-shrink: 0;
display: flex;
flex-direction: column;
padding: 20px 32px 24px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
}
.slide-header {
text-align: center;
margin-bottom: 10px;
}
.brand-row {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 4px;
}
.brand-mark {
font-size: 0.95rem;
font-weight: 800;
letter-spacing: 0.22em;
color: var(--po-blue-dark);
text-transform: uppercase;
border: 2px solid var(--po-blue-dark);
padding: 6px 14px 6px 18px;
border-radius: 4px;
line-height: 1;
}
.slide-header h1 {
margin: 0;
font-size: 1.85rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--po-blue-dark);
}
.slide-header .tagline {
margin: 4px 0 0;
font-size: 0.98rem;
font-weight: 600;
color: var(--po-blue);
max-width: 920px;
margin-left: auto;
margin-right: auto;
line-height: 1.35;
}
.slide-grid {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 82px 200px 42px minmax(0, 1fr) 82px;
grid-template-rows: repeat(6, minmax(0, 1fr));
gap: 0;
}
.gov {
grid-row: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--card);
border-radius: 10px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 12px rgba(18, 87, 155, 0.06);
padding: 10px 6px;
position: relative;
}
.gov::before,
.gov::after {
content: "";
position: absolute;
left: 4px;
right: 4px;
height: 10px;
background: var(--po-blue-dark);
border-radius: 3px;
}
.gov::before { top: 8px; }
.gov::after { bottom: 8px; }
.gov-left { grid-column: 1; }
.gov-right { grid-column: 5; }
.gov-title {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 0.88rem;
font-weight: 700;
color: var(--po-blue-dark);
letter-spacing: 0.04em;
text-align: center;
flex: 0 0 auto;
max-height: 58%;
}
.gov-sub {
font-size: 0.6rem;
line-height: 1.34;
color: var(--text-muted);
text-align: center;
padding: 8px 2px 0;
writing-mode: horizontal-tb;
max-width: 100%;
}
.rocket-tier {
grid-column: 2;
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin: 0 6px;
min-height: 0;
}
.rocket-tier .tier-body {
width: 100%;
height: 100%;
min-height: 52px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.rocket-tier.t-interface .tier-body {
background: var(--po-blue-dark);
border-radius: 12px 12px 0 0;
margin-top: 32px;
}
.rocket-nose {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 48px solid transparent;
border-right: 48px solid transparent;
border-bottom: 36px solid var(--po-blue-dark);
z-index: 1;
}
.rocket-tier.t-interface { align-items: flex-end; padding-top: 0; }
.rocket-tier.t-interface .wrap {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.rocket-tier.t-orch .tier-body { background: var(--po-blue); }
.rocket-tier.t-industry .tier-body { background: var(--po-industry); }
.rocket-tier.t-skills .tier-body { background: var(--po-seg-light); }
.rocket-tier.t-skills .tier-label { color: var(--po-blue-dark); text-shadow: none; }
.rocket-tier.t-models .tier-body {
background: var(--po-seg-cream);
border-radius: 0 0 6px 6px;
}
.rocket-tier.t-models .fin {
position: absolute;
bottom: 6px;
width: 0;
height: 0;
border-style: solid;
z-index: 0;
}
.rocket-tier.t-models .fin-l {
left: -18px;
border-width: 0 20px 48px 0;
border-color: transparent var(--po-blue-dark) transparent transparent;
}
.rocket-tier.t-models .fin-r {
right: -18px;
border-width: 0 0 48px 20px;
border-color: transparent transparent transparent var(--po-blue-dark);
}
.rocket-tier.t-data .tier-body {
background: linear-gradient(180deg, var(--po-flame-light) 0%, var(--po-flame) 100%);
clip-path: polygon(15% 0%, 85% 0%, 100% 100%, 50% 85%, 0% 100%);
min-height: 52px;
margin-top: 2px;
}
.tier-label {
position: absolute;
bottom: 4px;
left: 0;
right: 0;
text-align: center;
font-size: 0.58rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
pointer-events: none;
line-height: 1.1;
padding: 0 2px;
}
.rocket-tier.t-skills .tier-label,
.rocket-tier.t-models .tier-label { color: var(--po-blue-dark); text-shadow: none; }
.rocket-tier.t-data .tier-label { color: #fff; bottom: 8px; }
.ribbon {
grid-column: 3;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 2px;
}
.ribbon-inner {
width: 100%;
height: 70%;
min-height: 40px;
position: relative;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(0, 0, 0, 0.04) 100%);
transform: skewY(-2deg);
border-radius: 0 4px 4px 0;
box-shadow: inset -2px 0 4px rgba(0, 0, 0, 0.06), 2px 2px 6px rgba(18, 87, 155, 0.08);
}
.ribbon-inner::after {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
border-radius: 2px;
}
.ribbon.row-1 .ribbon-inner::after { background: var(--po-blue-dark); }
.ribbon.row-2 .ribbon-inner::after { background: var(--po-blue); }
.ribbon.row-3 .ribbon-inner::after { background: var(--po-industry); }
.ribbon.row-4 .ribbon-inner::after { background: var(--po-seg-light); }
.ribbon.row-5 .ribbon-inner::after { background: #c4b8a8; }
.ribbon.row-6 .ribbon-inner::after { background: var(--po-flame); }
.layer-card {
display: flex;
align-items: stretch;
margin: 2px 0 2px 8px;
min-height: 0;
}
.layer-card .card-shell {
flex: 1;
display: flex;
background: var(--card);
border-radius: 10px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 12px rgba(25, 118, 210, 0.07);
overflow: hidden;
min-height: 0;
}
.layer-card .tab {
width: 12px;
flex-shrink: 0;
}
.layer-card.row-1 .tab { background: var(--po-blue-dark); }
.layer-card.row-2 .tab { background: var(--po-blue); }
.layer-card.row-3 .tab { background: var(--po-industry); }
.layer-card.row-4 .tab { background: var(--po-seg-light); }
.layer-card.row-5 .tab { background: #c4b8a8; }
.layer-card.row-6 .tab { background: linear-gradient(180deg, var(--po-flame-light), var(--po-flame)); }
.layer-card .card-body {
padding: 6px 12px 6px 10px;
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.layer-card h3 {
margin: 0 0 1px;
font-size: 0.92rem;
font-weight: 700;
color: var(--po-blue-dark);
line-height: 1.2;
}
.layer-card .sub {
margin: 0 0 4px;
font-size: 0.72rem;
font-weight: 600;
color: var(--text-muted);
line-height: 1.25;
}
.layer-card ul {
margin: 0;
padding: 0;
list-style: none;
}
.layer-card li {
font-size: 0.71rem;
line-height: 1.35;
color: var(--text);
padding-left: 0.85em;
text-indent: -0.85em;
}
.layer-card li + li { margin-top: 1px; }
.rocket-tier.row-1, .ribbon.row-1, .layer-card.row-1 { grid-row: 1; }
.rocket-tier.row-2, .ribbon.row-2, .layer-card.row-2 { grid-row: 2; }
.rocket-tier.row-3, .ribbon.row-3, .layer-card.row-3 { grid-row: 3; }
.rocket-tier.row-4, .ribbon.row-4, .layer-card.row-4 { grid-row: 4; }
.rocket-tier.row-5, .ribbon.row-5, .layer-card.row-5 { grid-row: 5; }
.rocket-tier.row-6, .ribbon.row-6, .layer-card.row-6 { grid-row: 6; }
.rocket-tier { grid-column: 2; }
.ribbon { grid-column: 3; }
.layer-card { grid-column: 4; }
.tier-icon { width: 36px; height: 36px; color: var(--icon); }
.rocket-tier.t-industry .tier-icon { color: var(--icon); }
.rocket-tier.t-skills .tier-icon,
.rocket-tier.t-models .tier-icon { color: var(--icon-dark); }
.rocket-tier.t-data .tier-icon { color: #fff; }
</style>
</head>
<body>
<div class="stage" role="img" aria-label="Infografik: PowerOn KI-Plattform in sechs verständlichen Schichten für Entscheider">
<header class="slide-header">
<div class="brand-row">
<span class="brand-mark" aria-hidden="true">PowerOn</span>
</div>
<h1>Die PowerOn KI-Plattform</h1>
<p class="tagline">Eine Plattform für KI im Unternehmen mit Kontrolle, klaren Kosten und Lösungen für echte Fachfragen.</p>
</header>
<div class="slide-grid">
<aside class="gov gov-left">
<div class="gov-title">Sicherheit &amp; Regeln</div>
<div class="gov-sub">Wer darf was?<br>Getrennt pro Kunde / Mandant<br>Sensible Daten schützen<br>DSGVO: Auskunft &amp; Löschen</div>
</aside>
<!-- Row 1: Interface -->
<div class="row-1 rocket-tier t-interface">
<div class="wrap" style="width:100%;height:100%;">
<div class="rocket-nose" aria-hidden="true"></div>
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<circle cx="24" cy="24" r="16" stroke-opacity="0.35"/>
<path d="M24 12 v8 M24 28 v8 M12 24 h8 M28 24 h8"/>
<circle cx="24" cy="24" r="6"/>
</svg>
<span class="tier-label">Zugang</span>
</div>
</div>
</div>
<div class="row-1 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-1 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Zugang &amp; Bedienung</h3>
<p class="sub">So arbeiten Menschen und Systeme mit PowerOn</p>
<ul>
<li>➔ Chat, Arbeitsfläche, Sprache</li>
<li>➔ Im Browser, als App, Anbindung an Ihre IT</li>
</ul>
</div>
</div>
</article>
<!-- Row 2: Orchestrierung -->
<div class="row-2 rocket-tier t-orch">
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<rect x="8" y="22" width="18" height="16" rx="2"/>
<path d="M26 26 h10 M26 30 h10 M26 34 h10"/>
<circle cx="38" cy="18" r="5"/>
<path d="M33 22 L36 20 M26 22 L22 18"/>
</svg>
<span class="tier-label">Steuerung</span>
</div>
</div>
<div class="row-2 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-2 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Steuerung &amp; KI-Helfer</h3>
<p class="sub">Die KI plant Schritte und koordiniert das Weitere</p>
<ul>
<li>➔ Gespräche, Aufgaben und Abläufe im Griff</li>
<li>➔ Übergibt Arbeit an Programme und Schnittstellen</li>
</ul>
</div>
</div>
</article>
<!-- Row 3: Branchen -->
<div class="row-3 rocket-tier t-industry">
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<rect x="10" y="12" width="28" height="26" rx="2"/>
<path d="M10 20 h28 M18 12 v8 M30 12 v8"/>
<circle cx="18" cy="30" r="3"/>
<circle cx="30" cy="30" r="3"/>
</svg>
<span class="tier-label">Branchen</span>
</div>
</div>
<div class="row-3 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-3 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Fachlösungen</h3>
<p class="sub">Vorgefertigt für konkrete Berufsfelder</p>
<ul>
<li>➔ Treuhand &amp; Buchhaltung, Immobilien &amp; Grundstücke</li>
<li>➔ Coaching, Schulung, Unterstützung in Microsoft Teams</li>
</ul>
</div>
</div>
</article>
<!-- Row 4: Skills & Automation -->
<div class="row-4 rocket-tier t-skills">
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M14 32 c8 -4 12 -12 12 -20 c0 -4 -2 -6 -5 -6 c-4 0 -7 4 -7 10 c0 6 4 10 10 10 z"/>
<circle cx="30" cy="22" r="9"/>
<path d="M30 16 v12 M24 22 h12"/>
</svg>
<span class="tier-label">Aktionen</span>
</div>
</div>
<div class="row-4 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-4 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Automatisierung &amp; Aktionen</h3>
<p class="sub">Routine läuft, ohne dass alles manuell geklickt wird</p>
<ul>
<li>➔ Abläufe starten nach Zeitplan oder Ereignis (z.&nbsp;B. E-Mail)</li>
<li>➔ Verbindet Microsoft, Google und weitere Tools</li>
</ul>
</div>
</div>
</article>
<!-- Row 5: KI-Modelle -->
<div class="row-5 rocket-tier t-models">
<span class="fin fin-l" aria-hidden="true"></span>
<span class="fin fin-r" aria-hidden="true"></span>
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M12 38 L22 10 L38 38 Z M18 28 h14"/>
<line x1="26" y1="18" x2="32" y2="32"/>
</svg>
<span class="tier-label">Modelle</span>
</div>
</div>
<div class="row-5 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-5 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>KI-Modelle</h3>
<p class="sub">Sie wählen nicht an einen einzigen Anbieter gebunden</p>
<ul>
<li>➔ Einsatz führender KI-Anbieter nach Bedarf</li>
<li>➔ Eigene KI im eigenen Rechenzentrum möglich</li>
</ul>
</div>
</div>
</article>
<!-- Row 6: Unified Data Bar -->
<div class="row-6 rocket-tier t-data">
<div class="tier-body">
<svg class="tier-icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<rect x="8" y="14" width="32" height="22" rx="2"/>
<line x1="12" y1="20" x2="36" y2="20"/>
<line x1="12" y1="25" x2="28" y2="25"/>
<line x1="12" y1="30" x2="32" y2="30"/>
<circle cx="38" cy="10" r="6"/>
<path d="M40 12 l4 4" stroke-linecap="round"/>
</svg>
<span class="tier-label">Daten</span>
</div>
</div>
<div class="row-6 ribbon"><div class="ribbon-inner"></div></div>
<article class="row-6 layer-card">
<div class="card-shell">
<div class="tab" aria-hidden="true"></div>
<div class="card-body">
<h3>Datenleiste &amp; Wissen</h3>
<p class="sub">Alle wichtigen Quellen an einem Ort für die KI</p>
<ul>
<li>➔ Dateien und Ablagen sichtbar wie eine gemeinsame Leiste</li>
<li>➔ Antworten mit Bezug zu Ihren Unterlagen &amp; Gesprächen</li>
</ul>
</div>
</div>
</article>
<aside class="gov gov-right">
<div class="gov-title">Kosten &amp; Nachvollziehbarkeit</div>
<div class="gov-sub">Zahlen nach tatsächlicher Nutzung<br>Wer hat was gemacht?<br>Kosten pro Kunde / Mandant<br>Nachvollziehbare Entscheidungen</div>
</aside>
</div>
</div>
</body>
</html>

View file

@ -1,174 +0,0 @@
# PowerOn Billing Product Teaser - Recherche & Analyse
## Zusammenfassung der Recherche-Ergebnisse
### Abrechnungsmodelle
PowerOn bietet **4 flexible Abrechnungsmodelle**, die auf unterschiedliche Unternehmensanforderungen zugeschnitten sind:
1. **PREPAY_MANDATE** - Gemeinsames Prepaid-Guthaben fuer das gesamte Mandat
2. **PREPAY_USER** - Individuelles Prepaid-Guthaben pro Benutzer (Standard-Startguthaben: 10 CHF)
3. **CREDIT_POSTPAY** - Kreditrahmen mit monatlicher Abrechnung (erfordert Rechnungsadresse)
4. **UNLIMITED** - Unbegrenzt (nur fuer interne Mandate)
### Preisstruktur
- **Pay-per-Use**: Abrechnung nach tatsaechlicher KI-Nutzung
- **Transparente Aufschlaege**: 100% Markup auf Provider-Kosten (Faktor 2.0)
- 50% fuer Infrastruktur und Platform Service
- 50% fuer Waehrungsrisiko
- **Waehrung**: Schweizer Franken (CHF)
- **Aufladungsbetraege**: 10, 25, 50, 100, 250, 500 CHF
- **Zahlungsmethode**: Stripe Checkout (Kreditkarte)
### Kernmerkmale
- **Mandanten-basierte Abrechnung**: Isolierte Konten pro Mandat
- **Echtzeit-Transparenz**: Sofortige Kostenzuordnung nach jedem KI-Aufruf
- **Detaillierte Statistiken**: Nach Provider, Modell, Feature, Zeitraum
- **Warnungen**: Konfigurierbare Schwellenwerte (Standard: 10%)
- **Flexible Kontrolle**: Blockierung bei Nullsaldo optional
- **RBAC-Integration**: Feingranulare Zugriffskontrolle auf AI-Provider
### Technische Details
- **Keine Abonnements**: One-time Payments, keine wiederkehrenden Gebuehren
- **Webhook-Integration**: Automatische Gutschrift nach Zahlung
- **API-First**: Vollstaendige REST-API fuer Billing-Operationen
- **Audit-Trail**: Vollstaendige Transaktionshistorie
## Product Teaser fuer Homepage
Der folgende Text ist **Copy & Paste ready** und fuer die Homepage optimiert.
---
# Transparente Abrechnung. Volle Kostenkontrolle.
## Bezahlen Sie nur, was Sie nutzen - fair, transparent und flexibel
PowerOn bietet ein modernes, nutzungsbasiertes Abrechnungssystem, das sich Ihren Geschaeftsanforderungen anpasst. Keine versteckten Kosten, keine ueberraschenden Rechnungen - nur klare, nachvollziehbare Preise fuer die KI-Leistungen, die Sie tatsaechlich nutzen.
---
## Unsere Abrechnungsmodelle
### Prepaid fuer volle Kontrolle
**Prepaid Mandant** - Gemeinsames Guthaben fuer Ihr gesamtes Team. Ideal fuer Organisationen, die zentrale Budgetkontrolle bevorzugen.
**Prepaid Benutzer** - Individuelles Guthaben pro Mitarbeiter. Perfekt fuer dezentrale Teams mit eigenstaendiger Kostenverwaltung.
- Startguthaben von 10 CHF fuer neue Benutzer
- Flexible Aufladung: 10, 25, 50, 100, 250 oder 500 CHF
- Einfache Zahlung per Kreditkarte
- Sofortige Gutschrift nach Zahlung
### Kreditrahmen fuer etablierte Kunden
**Credit Postpay** - Arbeiten Sie mit einem Kreditrahmen und erhalten Sie monatliche Rechnungen. Ideal fuer Unternehmen mit etablierten Prozessen und hoeherem Nutzungsvolumen.
- Individuell vereinbarter Kreditrahmen
- Monatliche Abrechnung
- Rechnungsstellung an Ihre Firmenadresse
- Keine Vorauszahlung erforderlich
---
## So funktioniert die Preisgestaltung
### Pay-per-Use - Fair und transparent
Sie bezahlen ausschliesslich fuer die tatsaechlich genutzten KI-Leistungen. Jeder Aufruf wird praezise erfasst und Ihrem Konto zugeordnet.
### Klare Preisstruktur
Unsere Preise basieren auf den Kosten der fuehrenden KI-Provider (OpenAI, Anthropic, etc.) mit einem transparenten Aufschlag fuer:
- Infrastruktur und Platform Services
- Waehrungsabsicherung und Stabilitaet
- Support und Betrieb
**Alle Preise in Schweizer Franken (CHF)** - keine Waehrungsrisiken fuer Sie.
---
## Ihre Vorteile auf einen Blick
### Volle Transparenz
- **Echtzeit-Uebersicht**: Sehen Sie Ihr aktuelles Guthaben jederzeit ein
- **Detaillierte Statistiken**: Kosten nach Provider, Modell, Feature und Zeitraum
- **Vollstaendige Historie**: Jede Transaktion nachvollziehbar dokumentiert
### Intelligente Kontrolle
- **Warnungen**: Automatische Benachrichtigung bei niedrigem Guthaben
- **Flexible Limits**: Optionale Blockierung bei Nullsaldo
- **Budget-Management**: Individuelle Schwellenwerte pro Mandat
### Sicherheit und Compliance
- **Mandanten-Isolation**: Strikte Trennung zwischen Organisationen
- **Audit-Trail**: Vollstaendige Nachverfolgbarkeit aller Transaktionen
- **DSGVO-konform**: Schweizer Datenschutzstandards
### Einfache Verwaltung
- **Self-Service**: Guthaben jederzeit selbst aufladen
- **Keine Vertraege**: Keine Mindestlaufzeiten oder Kuendigungsfristen
- **Sofortige Aktivierung**: Nach Zahlung direkt einsatzbereit
---
## Fuer wen ist welches Modell geeignet?
| Ihr Bedarf | Empfohlenes Modell | Vorteil |
|------------|-------------------|---------|
| Kleine Teams, erste Schritte mit KI | **Prepaid Benutzer** | Jeder verwaltet sein eigenes Budget |
| Zentrale Kostenkontrolle | **Prepaid Mandant** | Ein gemeinsames Budget fuer alle |
| Etablierte Prozesse, hoeheres Volumen | **Credit Postpay** | Arbeiten ohne Vorauszahlung, monatliche Rechnung |
| Pilotprojekte, flexible Nutzung | **Prepaid Mandant** | Schneller Start, volle Flexibilitaet |
---
## Haeufig gestellte Fragen
**Gibt es versteckte Kosten?**
Nein. Sie bezahlen ausschliesslich fuer die tatsaechlich genutzten KI-Leistungen. Keine Setup-Gebuehren, keine Grundgebuehren, keine versteckten Zuschlaege.
**Wie schnell wird mein Guthaben gutgeschrieben?**
Sofort nach erfolgreicher Zahlung. Sie koennen direkt weiterarbeiten.
**Kann ich zwischen Modellen wechseln?**
Ja, Ihr Administrator kann das Abrechnungsmodell jederzeit anpassen - je nach Entwicklung Ihrer Anforderungen.
**Welche Zahlungsmethoden werden akzeptiert?**
Aktuell: Kreditkarte ueber Stripe Checkout. Fuer Credit Postpay: Rechnung per E-Mail.
**Wie detailliert ist die Kostenaufschluesselung?**
Sehr detailliert. Sie sehen fuer jede Transaktion: Provider, Modell, Feature, Benutzer, Zeitpunkt und Kosten.
**Was passiert, wenn mein Guthaben aufgebraucht ist?**
Je nach Konfiguration erhalten Sie eine Warnung oder KI-Funktionen werden blockiert. Sie koennen jederzeit selbst Guthaben aufladen.
---
## Jetzt starten
Beginnen Sie mit einem Prepaid-Modell und 10 CHF Startguthaben pro Benutzer. Keine Kreditkarte erforderlich fuer den ersten Test.
**Bereit fuer den naechsten Schritt?**
Kontaktieren Sie uns fuer eine persoenliche Demo oder starten Sie direkt mit Ihrem Team.
---
## Technische Details fuer IT-Verantwortliche
- **API-First**: Vollstaendige REST-API fuer Billing-Operationen
- **Webhook-Integration**: Automatische Verarbeitung von Zahlungsereignissen
- **RBAC-Integration**: Feingranulare Zugriffskontrolle auf AI-Provider
- **Stripe-Integration**: Sichere Zahlungsabwicklung nach PCI-DSS
- **Echtzeit-Abrechnung**: Sofortige Kostenzuordnung nach jedem AI-Call
- **Statistik-Aggregation**: Nach Tag, Monat, Jahr mit Breakdown nach Provider/Feature
---
## Kontakt
**PowerOn AG**
Zuerich, Schweiz
Haben Sie Fragen zu unseren Abrechnungsmodellen?
Unser Team beraet Sie gerne persoenlich.
---
*Stand: Maerz 2026*

View file

@ -1,245 +0,0 @@
# PowerOn Product Teaser
*Ihre KI-Plattform. Ein Arbeitsplatz. Alle Moeglichkeiten.*
## Die KI-Plattform fuer produktivere Teams
*Weniger Aufwand, bessere Ergebnisse -- ab dem ersten Tag.*
PowerOn ist die zentrale Arbeitsplattform fuer Unternehmen, die Prozesse vereinfachen, Wissen skalieren und wiederkehrende Aufgaben intelligent automatisieren wollen.
Auch ohne technisches Vorwissen starten Teams schnell: klare Oberflaechen, gefuehrte Workflows und direkt nutzbare KI-Funktionen helfen ab dem ersten Tag.
> **Screenshot-Platzhalter:** `HERO_SCREENSHOT`
> **Empfohlener Inhalt:** Startseite oder Dashboard mit PowerOn Branding und klarer Hauptnavigation
---
## Was ist PowerOn?
*KI, Zusammenarbeit und Automatisierung -- vereint in einer Plattform.*
PowerOn verbindet KI-Assistenten, teamweite Zusammenarbeit und Automatisierung in einer Plattform. Unternehmen erhalten damit einen digitalen Arbeitsplatz, in dem Beratung, Meetings, Prozesse und Fachdaten in einem einheitlichen Erlebnis zusammenkommen.
### Ihr Nutzen auf einen Blick
*Fuenf Gruende, warum Unternehmen auf PowerOn setzen.*
- Schnellere Entscheidungen durch kontextbezogene KI-Unterstuetzung
- Weniger manuelle Arbeit durch wiederverwendbare Automationen
- Hoehere Qualitaet durch standardisierte Ablaufe und transparente Ergebnisse
- Bessere Zusammenarbeit, weil Teams in vertrauten Umgebungen arbeiten koennen
- Skalierbarkeit fuer wachsende Organisationen und unterschiedliche Mandate
> **Screenshot-Platzhalter:** `SCREENSHOT_DASHBOARD`
> **Empfohlener Inhalt:** Uebersichtsseite mit zentralen Kacheln, Kennzahlen oder Einstiegen in Features
> **Screenshot-Platzhalter:** `SCREENSHOT_NAVIGATION`
> **Empfohlener Inhalt:** Linke Navigation mit den Bereichen Power Desktop, Test Coach, Teams Bot, Automation und Machbarkeitsstudie
> **Screenshot-Platzhalter:** `SCREENSHOT_FEATURE_STORE`
> **Empfohlener Inhalt:** Feature-Store mit aktivierbaren Modulen
---
## Feature 1: Power Desktop (AI Workspace)
*Ihr digitaler Schreibtisch -- alles an einem Ort.*
Power Desktop ist der zentrale Arbeitsbereich fuer produktives, KI-gestuetztes Arbeiten. Teams finden dort die wichtigsten Werkzeuge in einer durchgaengigen Umgebung: Chat, Editor und experimentelle KI-Arbeitsflaechen.
### Was neue Kunden daran schaetzen
*Der Mehrwert, der sofort spuerbar ist.*
- Ein Ort fuer Ideen, Inhalte und Umsetzung
- Weniger Tool-Wechsel, mehr Fokus im Tagesgeschaeft
- Schneller Einstieg auch fuer Nicht-Techniker durch klare Bedienlogik
### Kernfunktionen
*Chat, Editor und Playground in einer Umgebung.*
- KI-Chat fuer Fragen, Entwuerfe und iterative Verbesserungen
- Editor-Arbeitsbereich fuer strukturierte Inhalte und Dokumentation
- Playground-Bereich zum Testen und Verfeinern von KI-gestuetzten Loesungen
> **Screenshot-Platzhalter:** `SCREENSHOT_WORKSPACE_OVERVIEW`
> **Empfohlener Inhalt:** Gesamtansicht des Workspaces mit mehreren Bereichen
> **Screenshot-Platzhalter:** `SCREENSHOT_WORKSPACE_CHAT`
> **Empfohlener Inhalt:** Konkrete Chat-Interaktion mit verwertbarer Antwort
> **Screenshot-Platzhalter:** `SCREENSHOT_WORKSPACE_CODE`
> **Empfohlener Inhalt:** Editor-Ansicht mit klaren Arbeitsflaechen
---
## Feature 2: Test Coach (Kommunikations-Coach)
*Besser kommunizieren -- mit KI als persoenlichem Sparringspartner.*
Der Test Coach unterstuetzt Mitarbeitende und Fuehrungskraefte dabei, Kommunikationssituationen gezielt zu trainieren. Statt abstrakter Theorie liefert die KI konkrete, direkt anwendbare Impulse fuer den Berufsalltag.
### Was neue Kunden daran schaetzen
*Persoenliches Wachstum, messbar und alltagsnah.*
- Sichereres Auftreten in schwierigen Gespraechen
- Kontinuierliche Weiterentwicklung mit messbarem Fortschritt
- Individuelle Unterstuetzung passend zum persoenlichen Kommunikationsstil
### Kernfunktionen
*Von Themenauswahl bis Gamification -- alles in einem Dossier.*
- Coaching-Kontexte fuer Themen, Ziele und Herausforderungen
- Session-basiertes Training mit KI-Dialogen
- Aufgaben, Fortschritt und Verlauf in einem Dossier gebuendelt
- Sprachunterstuetzung fuer natuerlichere Lernsituationen
- Motivierende Elemente wie Streaks, Scores und Badges
> **Screenshot-Platzhalter:** `SCREENSHOT_COACH_DASHBOARD`
> **Empfohlener Inhalt:** Dashboard mit KPIs (z. B. Streak, Score, Badges)
> **Screenshot-Platzhalter:** `SCREENSHOT_COACH_SESSION`
> **Empfohlener Inhalt:** Laufende Coaching-Session mit Chat-Verlauf
> **Screenshot-Platzhalter:** `SCREENSHOT_COACH_DOSSIER`
> **Empfohlener Inhalt:** Dossier mit Tabs fuer Aufgaben, Sessions und Dokumente
---
## Feature 3: Teams Bot
*KI-Unterstuetzung dort, wo Ihr Team bereits arbeitet -- in Microsoft Teams.*
Der Teams Bot bringt KI-Unterstuetzung direkt in Microsoft Teams Meetings. Er kann Sitzungen begleiten, Inhalte erfassen und kontextbezogene Antworten bereitstellen.
### Was neue Kunden daran schaetzen
*Meetings produktiver machen, ohne Gewohnheiten zu aendern.*
- Sofortiger Mehrwert in bereits etablierten Meeting-Prozessen
- Besseres Informationsmanagement durch strukturierte Protokollierung
- Schnellere Nachbereitung durch KI-gestuetzte Unterstuetzung
### Kernfunktionen
*Ein Link genuegt -- der Bot uebernimmt den Rest.*
- Start einer Session ueber Meeting-Link
- Unterstuetzung verschiedener Join-Modi (z. B. Bot oder Benutzerkonto)
- Laufende Verarbeitung von Meeting-Inhalten
- KI-Antworten als Chat, Audio oder kombiniert
> **Screenshot-Platzhalter:** `SCREENSHOT_TEAMSBOT_START`
> **Empfohlener Inhalt:** Formular/Ansicht zum Start einer Teams-Bot-Session
> **Screenshot-Platzhalter:** `SCREENSHOT_TEAMSBOT_LIVE`
> **Empfohlener Inhalt:** Aktive Session mit Status und Live-Interaktion
---
## Feature 4: Automation
*Einmal definieren, immer wieder zuverlaessig ausfuehren.*
Automation in PowerOn macht wiederkehrende Aufgaben planbar und zuverlaessig. Unternehmen definieren Vorlagen einmal und fuehren Prozesse danach manuell oder zeitgesteuert aus.
### Was neue Kunden daran schaetzen
*Weniger Routine, mehr Raum fuer das Wesentliche.*
- Spuerbare Entlastung bei repetitiven Aufgaben
- Konstante Prozessqualitaet ueber Teams hinweg
- Mehr Zeit fuer wertschaffende Arbeit
### Kernfunktionen
*Templates, Zeitplanung und Echtzeit-Transparenz.*
- Verwaltung von Automations-Definitionen
- Wiederverwendbare Templates fuer typische Geschaeftsprozesse
- Geplante oder sofortige Ausfuehrung
- Transparente Rueckmeldungen ueber Live-Logs
> **Screenshot-Platzhalter:** `SCREENSHOT_AUTOMATION_LIST`
> **Empfohlener Inhalt:** Uebersicht der vorhandenen Automationen
> **Screenshot-Platzhalter:** `SCREENSHOT_AUTOMATION_EDIT`
> **Empfohlener Inhalt:** Erstellungs- oder Bearbeitungsmaske mit Template-Auswahl
> **Screenshot-Platzhalter:** `SCREENSHOT_AUTOMATION_LOGS`
> **Empfohlener Inhalt:** Laufende oder abgeschlossene Ausfuehrung mit Log-Anzeige
---
## Feature 5: Machbarkeitsstudie (Real Estate)
*Immobilienpotenziale in Minuten statt Tagen bewerten.*
Die Machbarkeitsstudie unterstuetzt bei der schnellen Erstbewertung von Immobilienpotenzialen. Relevante Informationen aus Regelwerken werden strukturiert extrahiert und als verwertbare Entscheidungsgrundlage aufbereitet.
### Was neue Kunden daran schaetzen
*Fundierte Entscheidungen frueher im Projektverlauf.*
- Schnellere Vorpruefung von Immobilienprojekten
- Bessere Entscheidungsgrundlagen in fruehen Projektphasen
- Klar strukturierte Ergebnisse statt unuebersichtlicher Rohdaten
### Kernfunktionen
*Automatische Analyse von Regelwerken und Parzellendaten.*
- KI-gestuetzte Extraktion von BZO-Inhalten
- Aufbereitung zentraler Fakten
- Konkrete Vorschlaege zur Einschaetzung von Potenzialen
- Zusatzinformationen fuer vertiefte Pruefungen
> **Screenshot-Platzhalter:** `SCREENSHOT_REALESTATE_MAP`
> **Empfohlener Inhalt:** Karten-/Parzellenansicht im Real-Estate-Bereich
> **Screenshot-Platzhalter:** `SCREENSHOT_REALESTATE_MACHBARKEIT`
> **Empfohlener Inhalt:** Ergebnisbereich mit Fakten und Vorschlaegen
> **Screenshot-Platzhalter:** `SCREENSHOT_REALESTATE_BZO`
> **Empfohlener Inhalt:** Detailansicht der BZO-Extraktion
---
## Warum PowerOn fuer neue Kunden?
*Eine Plattform, die mit Ihren Anforderungen waechst.*
PowerOn ist darauf ausgelegt, den Einstieg in KI-gestuetztes Arbeiten einfach zu machen und zugleich professionellen Mehrwert zu liefern. Statt isolierter Einzelloesungen erhalten Unternehmen eine skalierbare Plattform, die Menschen, Prozesse und KI wirkungsvoll verbindet.
### Besonders relevant fuer Nicht-Techies
*Kein Vorwissen noetig -- einfach loslegen.*
- Intuitive Bedienung statt technischer Komplexitaet
- Klare, gefuehrte Workflows
- Sofort sichtbarer Nutzen in Alltagsszenarien
- Schrittweise Erweiterung je nach Bedarf
### Call to Action
*Jetzt den naechsten Schritt machen.*
Starten Sie mit den wichtigsten Anwendungsfaellen in Ihrem Team und bauen Sie Ihre KI-gestuetzten Prozesse mit PowerOn systematisch aus.
---
## Benoetigte Screenshots (Uebersicht)
*Alle visuellen Platzhalter auf einen Blick.*
| Platzhalter | Benoetigter Screenshot | Empfohlene Perspektive |
| --- | --- | --- |
| `HERO_SCREENSHOT` | PowerOn Startseite mit Branding | Vollansicht mit Logo, Claim, Einstieg |
| `SCREENSHOT_DASHBOARD` | Hauptdashboard nach Login | Uebersicht mit wichtigsten Einstiegen |
| `SCREENSHOT_NAVIGATION` | Seitennavigation mit Feature-Liste | Fokus auf Feature-Namen und Struktur |
| `SCREENSHOT_FEATURE_STORE` | Feature-Store | Sichtbare Feature-Kacheln/Module |
| `SCREENSHOT_WORKSPACE_OVERVIEW` | Power Desktop Gesamtansicht | Mehrere Arbeitsbereiche gleichzeitig |
| `SCREENSHOT_WORKSPACE_CHAT` | Chat-Bereich | Konkrete Konversation mit KI-Antwort |
| `SCREENSHOT_WORKSPACE_CODE` | Editor-Bereich | Klar lesbare Arbeitsumgebung |
| `SCREENSHOT_COACH_DASHBOARD` | Coach Dashboard | KPIs wie Streak, Score, Badges |
| `SCREENSHOT_COACH_SESSION` | Aktive Coach Session | Laufender Dialog und Session-Kontext |
| `SCREENSHOT_COACH_DOSSIER` | Coach Dossier | Tabs/Abschnitte mit Aufgaben und Verlauf |
| `SCREENSHOT_TEAMSBOT_START` | Teams Bot Start | Meeting-Link und Session-Einstellungen |
| `SCREENSHOT_TEAMSBOT_LIVE` | Teams Bot Live Session | Session-Status und Interaktion |
| `SCREENSHOT_AUTOMATION_LIST` | Automation-Liste | Definitions-Uebersicht |
| `SCREENSHOT_AUTOMATION_EDIT` | Automation bearbeiten | Template + Parameter sichtbar |
| `SCREENSHOT_AUTOMATION_LOGS` | Automation-Logs | Live- oder Abschlussprotokolle |
| `SCREENSHOT_REALESTATE_MAP` | Real-Estate Kartenansicht | Parzellen und Kontext sichtbar |
| `SCREENSHOT_REALESTATE_MACHBARKEIT` | Machbarkeitsstudie Ergebnis | Fakten, Vorschlaege, strukturierte Ausgabe |
| `SCREENSHOT_REALESTATE_BZO` | BZO-Extraktionsdetails | Ausgelesene Regel-/Detailinformationen |
---
## Hinweis zur Verwendung
*So werden aus Platzhaltern fertige Bilder.*
Alle Platzhalter koennen spaeter im selben Dokument durch finale Bilder ersetzt werden, z. B. als direkte Markdown-Bilder:
```md
![PowerOn Dashboard](./screenshots/dashboard.png)
```

Binary file not shown.

View file

@ -1,168 +0,0 @@
# PORTO AI Chat — Screen-Recording-Skript (Werbeclip)
**Zielgruppe:** Entscheider, C-Level, Investoren
**Ton:** sachlich, vertrauensbildend, ohne Tech-Slang
**Gesamtlänge:** ca. 90120 Sekunden
**Auflösung:** mindestens 1920×1080; UI zoomed / Browser auf 125150 % falls nötig für Lesbarkeit
---
## Vorbereitung (nicht aufnehmen)
1. **Testdaten:** Eine anonymisierte PDF (z. B. «Muster-Vertrag») und ein kurzes internes Memo — keine echten Kundendaten.
2. **Workspace:** Bereits eingeloggt; eine leere oder neue Konversation wählen.
3. **Datenquelle (optional):** SharePoint- oder OneDrive-Testsite verbunden *oder* vorbereiteten Screen mit bereits verbundener Quelle (ohne sensible Namen).
4. **Provider:** Für eine Szene «Private LLM» sichtbar wählen — nur wenn in Ihrer Umgebung freigeschaltet; sonst Szene 7 weglassen oder durch «Mistral» ersetzen.
5. **Browser:** Tabs schliessen; keine persönlichen Lesezeichenleisten im Bild.
---
## Struktur Overview
| Block | Dauer | Inhalt |
|-------------|---------|----------------------------------|
| Hook | ~5 s | Eine Zeile, die Aufmerksamkeit holt |
| Problem | ~15 s | Risiko + Lücke öffentlicher Chats |
| Demo | ~60 s | Walkthrough mit konkreten Prompts |
| CTA | ~15 s | Schweiz, Kontrolle, Erstgespräch |
---
## Szene A — Hook (0:000:05)
**Bild:** Start auf PORTO Workspace (zentrale Chat-Ansicht, ruhig, keine Bewegung).
**Voice-over:**
«Was wäre, wenn Ihre Teams mit KI chatten könnten — mit echtem Zugriff auf Ihre Dokumente, aber ohne dass sensible Daten das Unternehmen verlassen?»
**Action:** Keine Klicks; 12 Sekunden Pause, dann sanft zur linken Sidebar zoomen oder leicht scrollen (optional).
---
## Szene B — Problem (0:050:20)
**Bild:** Kurz Browser-Tab oder Grafik weglassen — bleiben Sie in PORTO oder wechseln Sie zu einer neutralen Titelfolie «Das Problem» (optional).
**Voice-over:**
«Öffentliche Chat-Tools sind schnell — aber sie kennen Ihre Verträge, Ihre SharePoint-Ordner und Ihre Compliance-Regeln nicht. Das Ergebnis: Copy-Paste, Medienbrüche und ein Risiko, das Audit und Vorstand nicht tragen wollen. PORTO AI Chat schliesst diese Lücke.»
**Action:** Zurück zum Workspace wechseln.
---
## Szene C — Demo Teil 1: Dokument hochladen (0:200:30)
**Bild:** Workspace mit leerem oder neuem Chat; linke Leiste mit Dateien sichtbar.
**Action:** PDF per **Drag & Drop** in den Chat-Bereich ziehen *oder* über Datei-Upload anhängen. Warten, bis die Datei in der Konversation / Anhänge erscheint.
**Voice-over:**
«Hier arbeiten Ihre Mitarbeitenden in einer geschützten Oberfläche. Sie ziehen ein Dokument hinein — und der KI-Agent kann es im Kontext nutzen.»
---
## Szene D — Demo Teil 2: Analyse-Prompt (0:300:45)
**Bild:** Cursor im Eingabefeld; dann Senden.
**Eingabetext (exakt oder leicht angepasst):**
```text
Fasse die wichtigsten Klauseln dieses Dokuments in maximal fünf Bulletpoints zusammen.
Hebe Haftung, Kündigung und Vertraulichkeit hervor. Antworte auf Deutsch.
```
**Action:** Nach dem Senden **nicht** unterbrechen: kurz die **Streaming-Antwort** laufen lassen; wenn sichtbar, **Tool-Aktivität** oder Fortschritt in der rechten Spalte («Activity» / Tool-Log) mitfilmen.
**Voice-over:**
«Statt manuell zu lesen und zu kopieren, stellt man eine präzise Frage. Die KI arbeitet mit dem Dokument — und Sie sehen, was im Hintergrund passiert. Transparenz statt Blackbox.»
---
## Szene E — Demo Teil 3: Datenquelle (0:451:00)
**Bild:** Linke Sidebar → Tab **Quellen / Datenquellen** (SharePoint, OneDrive o. Ä., je nach UI-Label).
**Action:** Bereits verbundene Quelle anzeigen *oder* kurz durch Ordner browsen (nur Testinhalte). Dann zurück in den Chat.
**Eingabetext (Prompt):**
```text
Suche in meiner verbundenen Datenquelle nach dem neuesten Dokument zum Thema "Onboarding"
und gib mir eine einzeilige Inhaltszusammenfassung. Wenn nichts passt, sag es klar.
```
**Voice-over:**
«PORTO verbindet sich mit Ihren Systemen — SharePoint, OneDrive, Google Drive und mehr. Die KI beantwortet Fragen über Ihr Unternehmenswissen, nicht nur über das offene Internet.»
*Hinweis:* Wenn die Suche in der Demo leer zurückkommt, Voice-over anpassen: «Auch dann liefert das System eine klare Antwort — ohne zu halluzinieren.»
---
## Szene F — Demo Teil 4: Ergebnis als Datei (1:001:15)
**Bild:** Neuer Follow-up im selben Chat.
**Eingabetext (Prompt):**
```text
Erstelle auf Basis deiner letzten Antwort eine strukturierte Markdown-Datei
"Executive_Summary.md" mit Überschriften: Kontext, Kernpunkte, offene Fragen.
Schlage die Datei zur Freigabe vor, falls dein Workflow das vorsieht.
```
**Action:** Wenn **Datei-Änderungsvorschlag** / Vorschau erscheint: kurz **Akzeptieren** oder Vorschau zeigen (je nach Produktverhalten). Optional rechte Spalte **Vorschau** einblenden.
**Voice-over:**
«Der Agent liefert nicht nur Text im Chat — er kann Arbeitsergebnisse vorbereiten und zur Freigabe einreichen. Kontrolle bleibt beim Menschen.»
---
## Szene G — Demo Teil 5: Modellwahl (1:151:25)
**Bild:** Eingabebereich unten; **Provider-Auswahl** / Modell-Multiselect sichtbar machen.
**Action:** Dropdown öffnen; **Private LLM** (oder «Mistral» / konfigurierte Option) auswählen — ohne erneut zu senden, es sei denn Sie wollen eine kurze «Ping»-Antwort zeigen.
**Voice-over:**
«Entscheider interessiert: Welches Modell läuft? Hier wählen Sie es — bis hin zu Private LLM auf Schweizer Infrastruktur. Governance wird zur Einstellung, nicht zur Ausrede.»
---
## Szene H — CTA (1:251:40)
**Bild:** Ruhiger Vollbild-Workspace oder Ihre PORTO-/PowerOn-Schlussfolie mit Kontakt.
**Voice-over:**
«PORTO AI Chat macht produktive KI-Nutzung vereinbar mit Datenschutz und Kontrolle. PowerOn zeigt Ihnen im Erstgespräch, wie das in Ihrem konkreten Prozess funktioniert — von Treuhand bis Legal. Kontakt: poweron.swiss.»
**On-Screen-Text (optional, 34 Sekunden):**
`poweron.swiss` · `Erstgespräch vereinbaren`
---
## Prompts — Schnellkopie (alle Demo-Eingaben)
```
1) Analyse:
Fasse die wichtigsten Klauseln dieses Dokuments in maximal fünf Bulletpoints zusammen.
Hebe Haftung, Kündigung und Vertraulichkeit hervor. Antworte auf Deutsch.
2) Datenquelle:
Suche in meiner verbundenen Datenquelle nach dem neuesten Dokument zum Thema "Onboarding"
und gib mir eine einzeilige Inhaltszusammenfassung. Wenn nichts passt, sag es klar.
3) Datei:
Erstelle auf Basis deiner letzten Antwort eine strukturierte Markdown-Datei
"Executive_Summary.md" mit Überschriften: Kontext, Kernpunkte, offene Fragen.
Schlage die Datei zur Freigabe vor, falls dein Workflow das vorsieht.
```
---
## Technische Checkliste nach der Aufnahme
- [ ] Keine echten Kundennamen, E-Mails oder Vertragsnummern sichtbar
- [ ] Töne: optional dezente Hintergrundmusik (royalty-free), Voice-over dominant
- [ ] Untertitel (DE) für LinkedIn / stummes Abspielen empfohlen
- [ ] Endcard: Logo + URL + «Schweizer Datenhaltung»

Binary file not shown.

View file

@ -1,137 +0,0 @@
# Erfolgreicher Einsatz von KI
*PowerPoint-Vorlage. Ersetzt die Ring-Grafik durch eine klar lesbare 5-Säulen-Darstellung. Jede Säule: Titel, ein Leitsatz (aus Originalgrafik), 23 konkrete Belege aus der PowerOn-Doku.*
**Quellenbasis:** Originalbild „Erfolgreicher Einsatz von KI" sowie `wiki/a-strategy/product-vision.md`, `wiki/b-reference/platform/rbac.md`, `wiki/b-reference/platform/neutralization.md`, `wiki/b-reference/gateway/ai-agent.md`, `wiki/e-compliance/security-overview.md`.
---
## Folie 0 Intro (Titelfolie)
### Von der KI-Idee zur Roadmap, die trägt.
**Ihre Daten bleiben in der Schweiz. Ergebnisse in Wochen, nicht Monaten.**
Was Sie in der Hand halten: Handlungsfelder · Kausalnetz · Umsetzungsroadmap · Steckbriefe.
**Kicker (oben, klein, grau, uppercase):** PRÄSENTATION · ERFOLGREICHER EINSATZ VON KI
**Footer (unten, dezent):** PowerOn · www.poweron.swiss
**Textbausteine in Reihenfolge:**
| Ebene | Inhalt | Stil |
|---|---|---|
| Kicker | `PRÄSENTATION · ERFOLGREICHER EINSATZ VON KI` | klein, uppercase, grau |
| Headline | `Von der KI-Idee zur Roadmap, die trägt.` | XXL, schwarz, 2 Zeilen (Umbruch nach „KI-Idee") |
| Subline | `Ihre Daten bleiben in der Schweiz. Ergebnisse in Wochen, nicht Monaten.` | mittel, dunkelgrau |
| Deck-Teaser | `Was Sie in der Hand halten: Handlungsfelder · Kausalnetz · Umsetzungsroadmap · Steckbriefe.` | klein, grau, eine Zeile |
| Footer | `PowerOn · www.poweron.swiss` | klein, grau |
**Begründung der Wortwahl:**
- „Von der KI-Idee zur Roadmap, die trägt" — verknüpft explizit mit Folie 4 („Das halten Sie am Ende in der Hand") und schafft eine Deck-Klammer. „die trägt" setzt den selbstbewussten Ton, ohne marktschreierisch zu wirken.
- „Ihre Daten bleiben in der Schweiz" — deckt Compliance (CISO), Differenzierung (CEO) und Risiko-Argument (CFO) in einem Satz ab.
- „Ergebnisse in Wochen, nicht Monaten" — bewusst ohne harte Zahl (Time-to-Value steht noch nicht verbindlich fest), aber mit klarem Erwartungsmanagement.
- „Was Sie in der Hand halten" — identischer Wording-Anker wie Folie 4. Konsistenz schafft Vertrauen.
> **Visual-Hinweis:** 16:9, weißer Hintergrund, keine Hintergrund-Textur. Links 55 %: Kicker → Headline → Subline → Teaser, alles linksbündig. Rechts 45 %: die 5-Säulen-Grafik auf dezentem hellgrauen Panel mit abgerundeten Ecken, **mit Labels unter jedem Balken** (Use-Cases / Datenschutz / Berechtigungen / Verbindungen / Regeln / Ethik) in kleiner grauer Schrift. Dünne Trennlinie über dem Footer. Nur zwei Schriftgrößen-Stufen: Headline XXL + alles andere. Keine dekorativen Icons, keine Badges, keine Fülltextur.
> **C-Level-Logik:** EIN visueller Anker (Headline) statt fünf konkurrierender Hierarchieebenen. Ergebnis-Framing („Roadmap") spricht CEO, Subline fängt CFO („Wochen") und Compliance („Schweiz") gleichzeitig mit. Teaser baut inhaltliche Brücke zu Folie 4 Leser versteht sofort, worauf das Deck hinausläuft. Generisches Wording hält die Zielgruppe offen; Branchen-Personalisierung bleibt dem Cover-Letter vorbehalten.
---
## Folie 1 Übersicht: Die 5 Erfolgsfaktoren
### Erfolgreicher Einsatz von KI
**Fünf Säulen, die KI im Unternehmen sicher und wirksam machen.**
| # | Säule | Leitsatz (aus Originalgrafik) |
|---|---|---|
| 1 | **Use-Cases** | Der Einsatz basiert auf klar definierten Use-Cases. |
| 2 | **Datenschutz** | Keine sensitiven Daten gelangen nach aussen. |
| 3 | **Berechtigungen** | Die KI hat nur Zugriff auf klar definierte Daten. |
| 4 | **Verbindungen** | Einfache Anbindung von Informationsquellen und Agentensystemen. |
| 5 | **Regeln / Ethik** | Vertrauensvoller und fairer Einsatz der KI. |
> **Visual-Hinweis:** 5 gleich grosse Karten nebeneinander (Icons oben, Titel fett, Leitsatz darunter). Kein Kreis, keine schräg gestellten Labels. Reihenfolge links → rechts von „Voraussetzung" (Use-Case) über „Schutz" (Datenschutz, Berechtigungen) und „Integration" (Verbindungen) bis „Leitplanken" (Regeln / Ethik).
---
## Folie 2 Use-Cases
### Der Einsatz basiert auf klar definierten Use-Cases
- **Schrittweise Einführung** statt Big-Bang: „Schrittweise Integration, beginnend mit einfachen Use Cases." (product-vision.md)
- **Feature-Store-Architektur:** Mandanten aktivieren modular nur die Features, die sie brauchen (Workspace, Automation, CommCoach, Trustee …). Skaliert von Einzelanwendung bis Full-Suite. (product-vision.md)
- **Spezialisierte Agenten pro Aufgabe:** Chat, Workflow, Voice, RAG, Automation jeder Agent auf seinen Use-Case zugeschnitten, koordiniert durch eine zentrale Engine. (product-vision.md)
> **Visual-Hinweis:** Drei Stufen-Icons (klein → mittel → gross), um die schrittweise Einführung zu zeigen.
---
## Folie 3 Datenschutz
### Keine sensitiven Daten gelangen nach aussen
- **Datenschutz-Neutralisierer:** Sensitive Inhalte werden durch stabile Platzhalter ersetzt, bevor Text an externe KI-Modelle geht oder dauerhaft im RAG landet. Ein zentrales AI-Gate prüft jeden Modell-Call. (neutralization.md)
- **Hard-Mode:** Ist Neutralisierung erforderlich und scheitert, wird der Call blockiert Inhalte gelangen nie im Klartext zum Modell. (neutralization.md)
- **Private-LLM-Option:** Für höchste Anforderungen kann ein lokal betriebenes Sprachmodell genutzt werden. In diesem Fall verlassen keine Daten die eigene Infrastruktur. (security-overview.md § 7.5)
- **Kein Training mit Kundendaten:** Über Enterprise-APIs der Anbieter vertraglich ausgeschlossen. (security-overview.md § 7.4)
> **Visual-Hinweis:** Symbol „Schild" mit einem ausgehenden Pfeil, der auf einen Filter trifft.
---
## Folie 4 Berechtigungen
### Die KI hat nur Zugriff auf klar definierte Daten
- **Rollenbasierte Zugriffskontrolle (RBAC)** auf vier Stufen: System, Mandant, Feature und Feature-Instanz. (rbac.md)
- **Feingliedrige Zugriffsstufen** pro Aktion (Lesen, Erstellen, Bearbeiten, Löschen):
| Stufe | Zugriff |
|---|---|
| Kein Zugriff | Funktion nicht verfügbar |
| Eigene Daten | Nur selbst erstellte Einträge |
| Mandantendaten | Alle Daten des eigenen Mandanten |
| Alle Daten | Vollzugriff (Administratoren) |
*(security-overview.md § 4.1 / rbac.md)*
- **Vollständige Mandantentrennung:** Zugehörigkeitsprüfung bei jedem Zugriff serverseitig keine mandantenübergreifenden Datenflüsse. (security-overview.md § 3)
> **Visual-Hinweis:** Schlüssel-Icon + vier abgestufte Balken (kein / eigene / Mandant / alle).
---
## Folie 5 Verbindungen
### Einfache Anbindung von Informationsquellen und Agentensystemen
- **Toolbox-Registry:** Der Agent verfügt über thematische Tool-Gruppen (`core`, `ai`, `datasources`, `email`, `sharepoint`, `clickup`, `jira`, `workflow`, `trustee`) und kann bei Bedarf weitere zur Laufzeit nachfordern. (ai-agent.md)
- **Connection-abhängige Aktivierung:** External-Toolboxes werden nur freigeschaltet, wenn der Nutzer eine passende Connection hat (z. B. Microsoft, ClickUp, Jira). (ai-agent.md)
- **Modellunabhängigkeit:** Integration mit Anthropic, OpenAI, Mistral, Perplexity, Tavily und Private LLM kein Vendor-Lock-in, das jeweils beste Modell pro Aufgabe. (product-vision.md)
> **Visual-Hinweis:** Hub-and-Spoke-Grafik: PowerOn in der Mitte, Connectoren nach aussen (SharePoint, ClickUp, Jira, Mail, Private LLM, externe Modelle).
---
## Folie 6 Regeln / Ethik
### Vertrauensvoller und fairer Einsatz der KI
- **Transparente KI-Datenverarbeitung:** Die Plattform legt offen, welche Daten an welche KI-Dienste übermittelt werden. (security-overview.md § 7)
- **Lückenloser Audit-Trail:** Alle sicherheitsrelevanten Aktionen (Zugriffe, Administratoraktionen, Berechtigungsänderungen, KI-Nutzung) werden automatisch protokolliert und sind für Compliance-Nachweise verfügbar. (security-overview.md § 8)
- **DSGVO-Betroffenenrechte als Self-Service:** Auskunft, Löschung, Datenübertragbarkeit und Berichtigung sind direkt in der Plattform implementiert. (security-overview.md § 2)
- **Kein unkontrolliertes Superuser-Konto:** Auch Administratoren unterliegen dem RBAC-System; jede Aktion ist nachvollziehbar. (security-overview.md § 4.3)
> **Visual-Hinweis:** Waage-Icon (Balance) plus ein Logbuch-Symbol für den Audit-Trail.
---
## Übergeordneter Visual-Hinweis für PowerPoint
- **Statt Kreis → Zeile:** 5 gleich grosse Karten nebeneinander auf der Übersichts-Folie.
- **Farblogik konsistent halten:** pro Säule eine Farbe (z. B. wie im Original: Use-Cases grün, Datenschutz rosa, Berechtigungen blau, Verbindungen grau, Regeln/Ethik orange) und diese Farbe auch auf der jeweiligen Detail-Folie als Akzent verwenden.
- **Lesbarkeit:** Keine schräg gestellten Labels. Titel horizontal, Leitsatz in 12 Zeilen, Belege als Bullet-Liste mit max. 34 Einträgen pro Folie.
- **Quellen-Footer (optional):** klein am Folienrand: „Quelle: PowerOn Wiki a-strategy / b-reference / e-compliance".

View file

@ -1,266 +0,0 @@
# Social-Media-Werbeclip: PowerOn Desktop (AI Workspace)
Handbuch fuer einen **Stufen-Clip**: Funktionen und Vorteile **der Reihe nach** — aehnlich wie bei eurem **Treuhand- / Trustee-Beispiel** (Canva-Folie: grosse Schrittnummer, klare Headline, kurzer Erklaertext, zentrale Screenshot-Flaeche, optionaler Footer-Hinweis mit Pfeil).
**Medien:** Mockups, **selbst aufgezeichnete Screen Recordings**, optional Motion-Transitions zwischen den Stufen.
**Laenge:** ca. **3060 s**, je nach Anzahl Stufen (pro Stufe typisch **35 s**).
**Plattformen:** Reels, Shorts, TikTok (**9:16**), LinkedIn (**1:1** oder **4:5**).
---
## Dieses Format vs. „ProblemLoesungMontage“
| Ansatz | Eignung |
|--------|---------|
| **Stufen-Clip (dieses Dokument)** | Zuschauer sollen **einzelne Staerken** nacheinander **merken** — wie eine Kurz-Praesentation. Ideal, wenn ihr **mehrere Funktionen** fair abhaengen wollt. |
| Reiner HookPainSolution-Clip | Ein emotionaler Bogen in 2030 s; weniger Platz fuer **5+ konkrete Features**. |
Beides laesst sich kombinieren: **Stufe 0** = 2 s Hook, dann **Stufe 1 ff.** = Features.
---
## Slide-/Card-Vorlage (orientiert am Trustee-Beispiel)
Pro **Stufe** eine visuelle Einheit (Canva-Slide, After-Effects-Comp oder Schnitt-Szene mit festem Layout):
| Bereich | Inhalt | Hinweis |
|---------|--------|---------|
| **Badge** (oben links, optional) | z. B. `Neu`, `PowerOn Desktop`, Kampagnen-Tag | Kurz; nicht jede Stufe muss ein Badge haben |
| **Schrittnummer** | Grosse Zahl `1`, `2`, `3` … | Sofort klar: „wir sind bei Schritt X“ |
| **Headline** | **Ein Nutzenversprechen** (nicht Techniklabel) | z. B. „Einfache Bedienung“, „Alles im Blick“ |
| **Fliesstext** (12 Saetze) | **Was die Funktion tut** + **warum es dem Nutzer hilft** | Verstaendlich fuer Nicht-Techies |
| **Akzentzeile** (optional, unten mit Pfeil) | Micro-CTA oder Feature-Kern | z. B. „Einfacher Drag and Drop“ — analog zu eurem Trustee-Slide |
| **Mittelband / Label** ueber dem Screenshot | **Name der gezeigten Funktion** | z. B. „Dokumenten-Upload“ bei Trustee; bei Desktop z. B. „KI-Chat im Workspace“ |
| **Hauptbild** | **Screen Recording** oder Mockup | Echte UI bevorzugt; Demo-Mandat, anonymisiert |
| **Seitenleiste** (optional) | `www.poweron.swiss` vertikal | Wiedererkennung wie auf eurer Referenzfolie |
**Social-Best-Practice dazu:** Pro Stufe **nur eine Kernaussage** lesbar halten; bei **Sound off** muessen **Nummer + Headline** allein schon den Nutzen transportieren.
---
## Namensfuehrung
| Kontext | Bezeichnung |
|---------|-------------|
| Stufen-Headlines / Voiceover (Kunde) | **PowerOn Desktop**, „**Ihr KI-Arbeitsplatz**“ |
| Screenshot (Navigation in der App) | **AI Workspace** (ggf. Voice: „PowerOn Desktop der AI Workspace“) |
| Marke | **PowerOn** |
---
## Empfohlene Stufenfolge (PowerOn Desktop)
Die Reihenfolge ist fuer **Verstaendnis** optimiert: erst **Gesamtbild**, dann **Arbeiten mit KI**, dann **Daten**, dann **Kontextsteuerung**, dann **Quellen**, dann **Kontrolle**, dann **Transparenz**, zuletzt **CTA**.
---
### Stufe 0 — Einstieg (optional, 23 s)
| Feld | Vorschlag |
|------|-----------|
| Badge | `PowerOn` oder `Neu` |
| Nummer | — oder kleines `Start` |
| Headline | **Ihr KI-Arbeitsplatz in einem Workspace** |
| Text | Chat, Dateien und Quellen zusammen — statt staendig zwischen Tools zu wechseln. |
| Screenshot | Sehr kurze **Gesamtansicht** AI Workspace (drei Spalten andeuten) oder nur Logo + Farbflaeche |
| Footer (optional) | **Mehr Ueberblick. Weniger Medienbruch.** |
**Voiceover (optional):**
> „PowerOn Desktop: alles, was Sie fuer produktives Arbeiten mit KI brauchen — an einem Ort.“
---
### Stufe 1 — Ein Workspace, alles verbunden
| Feld | Vorschlag |
|------|-----------|
| Nummer | **1** |
| Headline | **Alles im Blick** |
| Text | Chats, Dateien und Datenquellen in **einer** Oberflaeche. Sie behalten den Faden vom ersten Satz bis zur fertigen Ausarbeitung. |
| Mittelband | `AI Workspace` |
| Screenshot | **Workspace Gesamtansicht:** links Tabs **Chats / Files / Sources**, Mitte Chat, rechts **Activity** oder **Preview** |
| Footer (optional) | **Ein Ort statt fuenf Fenster** |
**Was im Recording zeigen:** 23 s ruhig auf Layout verweilen; Cursor einmal links ueber die drei Tabs fuehren (ohne Hektik).
---
### Stufe 2 — Mit KI arbeiten, mit Kontext
| Feld | Vorschlag |
|------|-----------|
| Nummer | **2** |
| Headline | **Fragen. Antworten. Nachvollziehbar.** |
| Text | Der **KI-Chat** nutzt Ihre gebundenen Inhalte — Antworten lassen sich an **Quellen** und Schritten nachvollziehen, nicht nur „aus dem Bauch“ der KI. |
| Mittelband | `KI-Chat` |
| Screenshot | Aktive Konversation mit **sichtbarer** Antwort; wenn moeglich **Quellen** oder Anhaenge andeuten |
| Footer (optional) | **Weniger Raetselraten** |
**Hinweis:** Demo-Frage so waehlen, dass die Antwort in 23 s lesbar ist (kurzer Absatz).
---
### Stufe 3 — Dateien ablegen und mitnehmen
| Feld | Vorschlag |
|------|-----------|
| Nummer | **3** |
| Headline | **Einfach ablegen** |
| Text | **Dateien** im Workspace ablegen, sortieren und direkt als Kontext fuer die KI nutzen — analog zu eurem Trustee-Beispiel mit klarer **Drag-and-Drop**-Botschaft. |
| Mittelband | `Dateien` / `Files` |
| Screenshot | Tab **Files**: Ordnerliste oder **Drag-and-Drop**-Zone kurz zeigen (Datei markieren oder in Zone ziehen) |
| Footer (optional) | **Einfacher Drag and Drop** |
---
### Stufe 4 — Wer sieht was? Kontextsteuerung pro Datei
| Feld | Vorschlag |
|------|-----------|
| Nummer | **4** |
| Headline | **Wer sieht was?** |
| Text | Fuer jede Datei bestimmen Sie mit **einem Klick**, ob sie **persoenlich** bleibt, im **Team** (Instanz) sichtbar wird oder dem ganzen **Mandanten** zur Verfuegung steht. Die KI nutzt genau diesen Kontext. |
| Mittelband | `Kontextsteuerung` / `Scope` |
| Screenshot | Tab **Files** mit sichtbaren **Scope-Icons** neben den Dateinamen: 👤 Persoenlich, 👥 Instanz, 🏢 Mandant; Cursor klickt ein Icon — es wechselt zur naechsten Stufe |
| Footer (optional) | **Ein Klick. Drei Stufen.** |
**Was im Recording zeigen:** Files-Tab mit mind. 3 Dateien, jede mit sichtbarem Scope-Icon. Klick auf ein Icon — Wechsel von 👤 (Persoenlich) zu 👥 (Instanz). Kurz verweilen, damit die **Legende** unten sichtbar ist (Persoenlich / Instanz / Mandant).
---
### Stufe 5 — Eigene Systeme einbinden
| Feld | Vorschlag |
|------|-----------|
| Nummer | **5** |
| Headline | **Ihre Datenquellen, Ihr Kontext** |
| Text | Cloud und Fachsysteme als **Quellen** anbinden — die KI arbeitet mit dem, was Sie **freigeben**, nicht mit beliebigem Internetwissen. |
| Mittelband | `Datenquellen` / `Sources` |
| Screenshot | Tab **Sources**: mind. eine verbundene Quelle sichtbar (farbcodierte Eintraege); keine echten Mandantendaten |
| Footer (optional) | **Gebunden statt raten** |
---
### Stufe 6 — Aenderungen pruefen, dann freigeben
| Feld | Vorschlag |
|------|-----------|
| Nummer | **6** |
| Headline | **Sie entscheiden** |
| Text | Im **Editor** sehen Sie Aenderungen im **Vergleich****annehmen** oder **ablehnen**. Tempo von KI, Kontrolle bei Ihnen. |
| Mittelband | `Aenderungen pruefen` / `File Edit Review` |
| Screenshot | **Editor** mit Diff / VorherNachher oder Aktionsleiste **Accept / Reject** |
| Footer (optional) | **Freigabe bleibt bei Ihnen** |
---
### Stufe 7 — Transparenz bei der Arbeit
| Feld | Vorschlag |
|------|-----------|
| Nummer | **7** |
| Headline | **Nachvollziehbar fuer Teams** |
| Text | Die **Aktivitaets**-Ansicht zeigt, was im Hintergrund laeuft — gut fuer Vertrauen im Team und fuer Fuehrungskraefte, die Steuerung wollen. |
| Mittelband | `Aktivitaet` / `Activity` |
| Screenshot | Rechte Spalte **Activity** mit Eintraegen / Status |
| Footer (optional) | **Keine Blackbox** |
---
### Stufe 8 — Abschluss + CTA (35 s)
| Feld | Vorschlag |
|------|-----------|
| Nummer | **8** oder weglassen |
| Headline | **PowerOn Desktop** |
| Text | Produktiv mit KI arbeiten — mit Struktur, Daten und Kontrolle. |
| Screenshot | Wieder **Gesamtansicht** oder nur Markenflaeche |
| Footer | **Demo auf poweron.swiss** — URL nach Freigabe |
| Element | Wert |
|---------|------|
| Primaer-Link | `https://____________` |
| UTM (optional) | `?utm_source=___&utm_medium=paid_social&utm_campaign=ai_desktop` |
---
## Kurzvariante (ca. 30 s)
Nur **Stufen 0 → 1 → 2 → 6 → 8** (Ueberblick, Chat, Editor-Kontrolle, CTA).
Pro Stufe **~4 s**.
Optional **Stufe 4** (Kontextsteuerung) ergaenzen, wenn die Datei-Sichtbarkeit betont werden soll (+4 s).
---
## Schnitt-Timeline (Referenz, alle 8 inkl. 0)
| Stufe | ca. Sekunden |
|-------|----------------|
| 0 Einstieg | 23 |
| 1 Workspace | 45 |
| 2 KI-Chat | 45 |
| 3 Dateien | 34 |
| 4 Kontextsteuerung | 34 |
| 5 Quellen | 34 |
| 6 Editor | 45 |
| 7 Aktivitaet | 34 |
| 8 CTA | 35 |
**Summe:** etwa **4865 s** — kuerzbar durch Weglassen von 3, 4, 5 oder 7.
---
## Mockups vs. Screen Recordings
| Stufe | Empfehlung |
|-------|------------|
| 0, 8 | Mockup oder reduzierte UI + starke Typo erlaubt |
| 17 | **Echtes Screen Recording** aus Demo-Instanz (Zoom 100125 %, ruhiger Cursor) |
Uebergaenge: kurzer **Push** oder **Match-Cut** auf die naechste Schrittnummer — konsistent mit eurem Trustee-Stil (Farbe, Schrift, www.poweron.swiss).
---
## Social-Media-Best Practices (kompakt)
- **Erste Sekunde:** Bewegung oder klare Schritt-`1`-Flaeche — sonst Swipe weg.
- **Ein Gedanke pro Stufe:** Headline + ein Satz Text reichen.
- **Ohne Ton:** alles Wichtige als **grossen Text** im Bild; Untertitel zusaetzlich.
- **9:16:** Text nicht in die untere Drittel-Social-UI legen; **sichere Zone** einplanen.
- **Keine Echtdaten** in Screens; Demo-Mandat, anonymisierte Namen/Dateien.
---
## Untertitel-Zeile pro Stufe (Sprechertext)
1. „PowerOn Desktop — Ihr KI-Arbeitsplatz.“
2. „Alles verbunden: Chat, Dateien, Quellen.“
3. „Der Chat nutzt Ihren Kontext — nachvollziehbar.“
4. „Dateien ablegen — per Drag and Drop.“
5. „Persoenlich, Team oder Mandant — Sie bestimmen, wer was sieht.“
6. „Ihre Systeme als Quellen — bewusst freigegeben.“
7. „Aenderungen pruefen — annehmen oder ablehnen.“
8. „Aktivitaet sichtbar — keine Blackbox.“
9. „Demo: poweron.swiss.“
---
## Marken- und Rechts-Checkliste
- [ ] **PowerOn** Schreibweise
- [ ] Feature in UI: **AI Workspace**; Werbetext: **PowerOn Desktop**
- [ ] Keine personenbezogenen / Kundenechtdaten in Aufnahmen
- [ ] Musik lizenziert
- [ ] Starke Datenschutz-Aussagen nur nach Legal-Abstimmung
---
## Verwandte interne Doku
- [case-study-power-desktop.md](case-study-power-desktop.md) — Argumente und Tiefe
- [product-teaser-poweron.md](product-teaser-poweron.md) — Plattform
- [social-clip-poweron-treuhand.md](social-clip-poweron-treuhand.md) — anderes Feature, gleiches **Stufen-Prinzip** mit Screens
---
*Stand: April 2026*

View file

@ -1,160 +0,0 @@
# Social-Media-Kurzclip: PowerOn Treuhand
Produktionshandbuch fuer **reine Screen-Aufnahmen**: On-Screen-Texte, Screen-Storyboard und Freigaben. Ziel: **20-40 s** (Reels, Shorts, LinkedIn). Ton: sachlich-treuhaenderisch.
---
## Grundsetup fuer Screen-Only
- Kein Interview-Footage einplanen, nur UI-Aufnahmen aus PowerOn.
- On-Screen-Texte kurz halten (max. 6-8 Woerter pro Karte).
- 9:16 schneiden (1080x1920) oder aus 16:9 sauber croppen.
- Alle Daten in Demo-Instanz anonymisieren.
---
## Shot 1: Customer Story (3-5 s)
### On-Screen — Variante A (generisch)
| Karte | Text |ca. Zeichen |
|-------|------|------------|
| 1 | Treuhand im Alltag | kurz |
| 2 | Ein Team. Viele Mandate. | kurz |
### On-Screen — Variante B (persona)
| Karte | Text |
|-------|------|
| 1 | Fiduciary / Treuhänder:in |
| 2 | Belege. Buchhaltung. Verantwortung. |
### Screen-Aufnahme (statt Interview)
- Navigiere zu **Treuhand > Uebersicht (Dashboard)**.
- Zeige 1-2 Sekunden die gesamte Seite, dann kurzer Fokus auf Kacheln.
- Sichtbar: `Positionen`, `Dokumente`, `Buchhaltung`.
### Optionales Voiceover
> „Treuhand im Alltag: viele Mandate, viele Belege, hohe Verantwortung.“
---
## Shot 2: Pain — Before (4-8 s)
### On-Screen — Standard (4 Karten, schneller Wechsel)
1. `Before:`
2. `Belege überall`
3. `manuell abtippen`
4. `keine klare Zuordnung`
### On-Screen — Schnitt-Variante (3 Karten)
1. `Before: Chaos in Postfach & Ordnern`
2. `Excel & Copy-Paste`
3. `Prüfung? Lücken in der Akte`
### Voiceover (optional)
> „Vorher: verteilte Belege, manuelle Schritte und wenig Transparenz.“
### Screen-Aufnahme (Pain visuell zeigen)
- In **Dokumente** kurz eine unstrukturierte Liste zeigen.
- Dann in **Positionen** kurz eine manuelle Erfassung andeuten (z. B. Tabelle ohne Zuordnung).
- Optional 0.5-1 s auf fehlende Zuordnung wechseln (noch nicht `Zuordnungen` zeigen).
---
## Shot 3: After — PowerOn Treuhand (10-18 s)
### On-Screen (4 Karten, über den Screen gelegt)
1. `After:`
2. `PowerOn Treuhand`
3. `eine Instanz · klare Akte`
4. `bis zur Buchhaltung`
### Voiceover (optional)
> „Mit PowerOn Treuhand ist alles pro Mandat gebuendelt: Positionen, Dokumente, Zuordnungen und Sync.“
### Screen-Aufnahmen — Storyboard (Reihenfolge)
**Technik:** Demo- oder Schulungsmandat; **keine realen Mandantennamen**; Cursor ruhig; ideal **9:16** (1080x1920) oder Ausschnitt.
| Nr. | Dauer | Navigation | Sichtbar machen |
|-----|-------|------------|------------------|
| 1 | 3-4 s | Treuhand -> **Uebersicht** (Dashboard) | Kacheln: Positionen, Dokumente, Buchhaltung; Bereich **Instanz-Details** (Instanz, Mandant) |
| 2 | 2-3 s | **Positionen** | Tabelle mit mind. einer Zeile; optional **Sync-Status-Spalte**; kurz Zeile anklicken oder markieren |
| 3 | 2-3 s | **Positionen** (optional) | **Mehrfachauswahl** einer Zeile -> Aktion **Sync zur Buchhaltung** (nur wenn Demo OK); sonst Nr. 2 verlaengern |
| 4 | 2-3 s | **Dokumente** | Liste; **Download** auf eine Zeile oder Upload-Dialog starten (ohne sensible Dateinamen) |
| 5 | 2-3 s | **Zuordnungen** | Mind. eine Zeile: Verknuepfung **Position <-> Dokument** lesbar |
| 6 | 2-4 s | *Entweder* **Scannen / Hochladen** *oder* **Spesen Import** | **Scannen:** PDF/JPG per Drag-and-Drop -> **Pipeline-Status** (laeuft/fertig). **Spesen:** verbundene Microsoft-/Ordner-Ansicht + Automation sichtbar aktiv |
**Kürzestes Set (wenn Zeit knapp):** nur **1 → 2 → 5** (Dashboard, Positionen, Zuordnungen).
**Nicht noetig im Kurzclip:** lange Passagen **Buchhaltungseinstellungen** oder **Rollen & Rechte**; hoechstens der Hinweis „Buchhaltung konfiguriert“ auf dem Dashboard.
---
## Shot 4: Closing (3-5 s)
### On-Screen — Englisch
- `Real business.`
- `Real wins.`
### On-Screen — Deutsch (Alternative)
- `Echtes Geschäft.`
- `Messbarer Gewinn.`
### On-Screen — Ultra-kurz
- `Weniger Handarbeit. Mehr Nachweis.`
### CTA (letzte Karte)
Setzen Sie die **verbindliche URL** nach Freigabe ein:
| Element | Wert |
|---------|------|
| Primär-Link | `https://____________` |
| Tracking (UTM) | optional `?utm_source=___&utm_medium=social` |
### Voiceover (optional)
> „Real business. Real wins.“
---
## Marken- und Rechts-Checkliste
- [ ] Schreibweise **PowerOn** (nicht Power On / poweron außerhalb der Domain)
- [ ] Feature-Bezeichnung konsistent mit Navigation: **Treuhand** bzw. interner Label „Trustee“
- [ ] Bei reinem Screen-Clip: keine Personen/Gesichter sichtbar
- [ ] Keine **geschützten Kundendaten** in Screens (Demo-Mandat)
- [ ] Musik: Lizenz / Ton über Plattform-Library
- [ ] Falls Mitarbeitende sichtbar: **Bildrechte** oder Silhouette/Blur
---
## Schnitt-Timeline (Referenz)
| Block | Ziel-Länge |
|-------|------------|
| Customer Story | 3-5 s |
| Before | 4-8 s |
| After (Screen-Only) | 10-18 s |
| Closing + CTA | 3-5 s |
**Gesamt:** ca. 2035 s; bei längerer Musik + Logo-Stinger bis 40 s.
---
## Verwandte interne Doku
- [product-teaser-billing-poweron.md](product-teaser-billing-poweron.md) — Markenkontext PowerOn

View file

@ -1,97 +0,0 @@
# Development Environment Configuration
# System Configuration
APP_ENV_TYPE = dev
APP_ENV_LABEL = Development Instance Patrick
APP_API_URL = http://localhost:8000
APP_KEY_SYSVAR = D:/Athi/Local/Web/poweron-swiss/local/notes/key.txt
APP_INIT_PASS_ADMIN_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEeFFtRGtQeVUtcjlrU3dab1ZxUm9WSks0MlJVYUtERFlqUElHemZrOGNENk1tcmJNX3Vxc01UMDhlNU40VzZZRVBpUGNmT3podzZrOGhOeEJIUEt4eVlSWG5UYXA3d09DVXlLT21Kb1JYSUU9
APP_INIT_PASS_EVENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERzZjNm56WGVBdjJTeG5Udjd6OGQwUVotYXUzQjJ1YVNyVXVBa3NZVml3ODU0MVNkZjhWWmJwNUFkc19BcHlHMTU1Q3BRcHU0cDBoZkFlR2l6UEZQU3d2U3MtMDh5UDZteGFoQ0EyMUE1ckE9
# PostgreSQL DB Host
DB_HOST=localhost
DB_USER=poweron_dev
DB_PASSWORD_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEcUIxNEFfQ2xnS0RrSC1KNnUxTlVvTGZoMHgzaEI4Z3NlVzVROTVLak5Ubi1vaEZubFZaMTFKMGd6MXAxekN2d2NvMy1hRjg2UVhybktlcFA5anZ1WjFlQmZhcXdwaGhWdzRDc3ExeUhzWTg9
DB_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERjlrSktmZHVuQnJ1VVJDdndLaUcxZGJsT2ZlUFRlcFdOZ001RnlzM2FhLWhRV2tjWWFhaWQwQ3hkcUFvbThMcndxSjFpYTdfRV9OZGhTcksxbXFTZWg5MDZvOHpCVXBHcDJYaHlJM0tyNWRZckZsVHpQcmxTZHJoZUs1M3lfU2ljRnJaTmNSQ0w0X085OXI0QW80M2xfQnJqZmZ6VEh3TUltX0xzeE42SGtZPQ==
APP_TOKEN_EXPIRY=300
# MFA Configuration
MFA_REQUIRE_ADMINS = False
# CORS Configuration
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss
# Logging configuration
APP_LOGGING_LOG_LEVEL = DEBUG
APP_LOGGING_LOG_DIR = D:/Athi/Local/Web/poweron-swiss/local/logs
APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s
APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S
APP_LOGGING_CONSOLE_ENABLED = True
APP_LOGGING_FILE_ENABLED = True
APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5
# OAuth: Auth app (login/JWT) vs Data app (Microsoft Graph / Google APIs). Same IDs until you split apps in Azure / GCP.
Service_MSFT_AUTH_CLIENT_ID = 840b759a-4d79-4a7a-9598-f3ed204d99d8
Service_MSFT_AUTH_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQnFBa1kxaG9WY1FJaWdCbVFVaTllUlJfU3Y3MmJkRmkzMDVDWUNtZEhlNVhISzJPcy00ZUVZcklYLXFMV0dIODV3NXNSSFBKQ0ZsZllES3diTEgySDF0T1ZCbFZHREZtcXFGSWNZN1NJbzJzczRRQWxoeVNsNzlsa0VzMHJPWHUydjBBclo=
Service_MSFT_AUTH_REDIRECT_URI = http://localhost:8000/api/msft/auth/login/callback
Service_MSFT_DATA_CLIENT_ID = 840b759a-4d79-4a7a-9598-f3ed204d99d8
Service_MSFT_DATA_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQnFBa1kyUW96aXFVOVJlLUdyRlVvT1hVU09ILWtMZnV2M19mVUxGMnFPV3FzNTdQa3dTbHVGTDBHTk01ZThLcjh6QUR5VldVZUpfcDlZNTh5YldtLWtjTll6VzJNQ3JCQ3ZubHdmd2JvaExDOXdvQ1pjWDVQTUtFWVAtUHhwS1lFQnJXWk4=
Service_MSFT_DATA_REDIRECT_URI = http://localhost:8000/api/msft/auth/connect/callback
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
Service_GOOGLE_AUTH_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQnFBa1kyd1hPd09vcVFtbVg0Sm5Nd1VYVEEtWjZMZkFndmFVS0ZlcTU0dzJnYVYzRkZWbjh0QldyZkhseDV2cUgxYkNHTzF6MXhqQlZ2N0UtbmhPeWRKUHBVdzV0Q1ROaWNuN2xjMmVzMjNZQ2ZYZ3dOTHgxaU5sTGRjVHpfakhYeWF0ZGU=
Service_GOOGLE_AUTH_REDIRECT_URI = http://localhost:8000/api/google/auth/login/callback
Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
Service_GOOGLE_DATA_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQnFBa1kySXoyd1BmTnhOd1owTUJOWm53WlZMMjFHNGJhSUwyd2NDUW9BanlRWVJPLU5jYzRlcm5QeW96d0JYUkVWVWd2dGNBVEpJbElZY2lWb0o5S0gyNnhoV1pnNXhpSFEyaklZZjcwX2lVU0ktMEJGN01DMDhXQ3k4R1BXc1Q3ejFjOEg=
Service_GOOGLE_DATA_REDIRECT_URI = http://localhost:8000/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd4ZWVBeHVtRnpIT0VBN0tSZDhLRmFmN05DOVBOelJtLWhkVnJDRVBqUkh3bDFTZFRWaWQ1cWowdGNLUk5IQzlGN1J6RFVCaW8zRnBwLVBnclJfdWgxV3pVRzFEV2lwcW5Rc19Xa1ROWXNJcUF0ajZaYUxOUXk0WHRsRmJLM25FaHV5T2IxdV92ZW1nRjhzaGpwU0l2Wm9FTkRnY2lJVjhuNHUwT29salAxYV8wPQ==
Service_CLICKUP_OAUTH_REDIRECT_URI = http://localhost:8000/api/clickup/auth/connect/callback
# Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI.
# Stripe Billing (both end with _SECRET for encryption script)
STRIPE_SECRET_KEY_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd5aHNGejgzQmpTdmprdzQxR19KZkh3MlhYUTNseFN3WnlaWjh2SDZyalN6aU9xSktkbUQwUnZrVnlvbGVRQm4yZFdiRU5aSEk5WVJuUnR4VUwtTm9OVk1WWmJQeU5QaDdib0hfVWV5U1BfYTFXRmdoOWdnOWxkb3JFQmF3bm45UjFUVUxmWGtGRkFKUGd6bmhpQlFnaVI3Q2lLdDlsY1VESk1vOEM0ZFBJNW1qcVZ0N2tPYmRLNmVKajZ2M3o3S05lWnRRVG5LdkRseW4wQ3VjNHNQZTZUdz09
STRIPE_WEBHOOK_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd5dDJMSHBrVk8wTzJhU2xzTTZCZWdvWmU2NGI2WklfRXRJZVUzaVYyOU9GLUZsalUwa2lPdEgtUHo0dVVvRDU1cy1saHJyU0Rxa2xQZjBuakExQzk3bmxBcU9WbEIxUEtpR1JoUFMxZG9ISGRZUXFhdFpSMGxvQUV3a0VLQllfUUtCOHZwTGdteV9rYTFOazBfSlN3ekNWblFpakJlZVlCTmNkWWQ4Sm01a1RCWTlnTlFHWVA0MkZYMlprUExrWFN2V0NVU1BTd1NKczFJbVo3VHpLdlc4UT09
STRIPE_API_VERSION = 2026-01-28.clover
STRIPE_AUTOMATIC_TAX_ENABLED = false
STRIPE_TAX_RATE_ID_CH_VAT = txr_1TOQd14OUoIL0Osj7A0ZQlr0
# AI configuration
Connector_AiOpenai_API_SECRET = DEV_ENC:Z0FBQUFBQnFGdnVHZlpWWWVaV1dERUItVjRfNWFMRXZUVjY5Ulp1ZXkyMmVZWUJPNzJ5anRucGNlOUFYSVNzZ1FaWlZLemVNbk5pbDgwOEZxbkxxM3U3UU1EdDJzczBaYmRDbld1N0hHdjROQUFmMUJmbDRMS1JWc3c4ZTFPMHY3OWVublBsUjFxNjhDSGRCaE9PR1JUN29iQjRqVFRINHB5dnJXeGxiQ2FTdnNnN283b3o1MnV3X09uc1pXeXZUclNTbjN4YWZyb2tGVmtGVmRnQmNlczI0WlZKRGZSYWgycnB0R2RfR1Fvbkt5bXN1UVBwS202SkZyRmJGTEU3MkxpcTk2d0QtOGxRckNLaTFLRnJBYlVSZDAydlpjMDVqYmktdHhuV3FLa2xrYkh4cGc3S3FGcnM9
Connector_AiAnthropic_API_SECRET = DEV_ENC:Z0FBQUFBQnFGd0hadGRhRFBOaXJSN2E1UnpLcHlkUHhoQS1FWFJBQlJ5cGsxcHNNMDlRM1JVbEJkWWE1ZXQzTThFQkRmQTBOeVNvUERXTVRTQ3Y4ekt5NU1XR3E2cWw4UlFNUXlGSmZmZU1JZXlwT3lUVGw0aWF4V1d6cVR6LTFsbGRjWWx5dVlodWNEUFJkZ0tUa3hSbjk3WjZ1Z3RPczZMYzA5QTlMbkVudFNVcG1xaTJuM3g3dDdSSFczbWJnODQ1S1J2djBnS3lQc2NFd0ttUThRVnFma3NOVlFIZm1ZZz09
Connector_AiPerplexity_API_SECRET = DEV_ENC:Z0FBQUFBQnFCdlFlNV9felVPcHVyMU9kVGhGZEt0MG9iRzRrTVM4TFJvSHhGOVo0U1ROWkdEMzRSWjhtMnFrZUhHTHNXelpLZ014RzRkMlIxZDJwcjEwc1dRamY5ekJMR1VLb2w4eEZqZENBRnFaZlRhb1h5VE05Tml1ZlVBWHBaTkJaZUE5NWprVklva0ZFZnB4cFFudGdkalpmTlBhdV9nPT0=
Connector_AiTavily_API_SECRET = DEV_ENC:Z0FBQUFBQnFCdlFlY1R2WGpuazk5M05SeDIyLWd3bHpKN3lUdlVFdjhvZEJXdlM4bGlBdTB1TjRia051YllDQ2lwM0V3R3dPd2lKVWxoSm9BNWl1ZFFlVkZ5cXh4TFRVU0Z4NVU5WVRjSUJPc01La3JyaVZSNkhYWU9PR00yMENEb0dRT3l5enEwSFlWZVVzTVR0UWQ4eUxvRmZvWHl0c0xRPT0=
Connector_AiPrivateLlm_API_SECRET = DEV_ENC:Z0FBQUFBQnBudkpGRHM5eFdUVmVZU1R1cHBwN1RlMUx4T0NlLTJLUFFVX3J2OElDWFpuZmJHVmp4Z3BNNWMwZUVVZUd2TFhRSjVmVkVlcFlVRWtybXh0ZHloZ01ZcnVvX195YjdlWVdEcjZSWFFTTlNBWUlaTlNoLWhqVFBIb0thVlBiaWhjYjFQOFY=
Connector_AiMistral_API_SECRET = DEV_ENC:Z0FBQUFBQnFCdlFlelh2T2hqNGcxV0hMV1FKbmFDZjVHUWF6T2FXbGlCSnQzSzNXLWJHeXBFWE1nUlh1b1NHY1JRSEVtTVEtc1MtUnZrX2ZCcURqQ2FYNmFWa2xudGJtS3g2eVo4MFZMd09nZTBNMmo1ZHU0bzBJdFRqLVhHSVZNb2Zrc0VkUXI0SVk=
Service_MSFT_TENANT_ID = common
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = DEV_ENC:Z0FBQUFBQnFIc3YtU0x4LTlHbTY1NUVGY2V2bUdmck85dDh1ZWVKa2ktR0N6NjdlTGFrUHMybVQ2bVRLN01XNFRZR2lyN0ZNSHhzWVVGNnVtZjRjV2hhR0ViTDYwT25lSmxJY0pSTkl3OUEyT0JxMFVYRndfUFJudExMajdTYUNXS01JU2lhQzZmNWFYdXA4aVZ5Zkh4Zko1Z00tcEE5ZFEwQkFVa1oyR296YXozRFI2WUdXN0ZSREFFclFNaTd6OUVlSmFxS1BTSlNJbnlWNHNfbkk4QzVOUGlkMzdfQUZxUlJOVEZzUlN1aWRWY01JZmlRM0JNZE1EZ3BmbW10c3BDdERpa2FMakstQUlqVEVlRC1hUmZoeFVoQ3pYNXRlRFVSTlI3ekJrU0QwSHBSaWxiSGU0akFGMXUtY2Q0RnUzS0tPOEQtcTdVdWhQeHFDM1hRRVVMcUxCeklvWHNWRUN2bjVHZUUwLTVtaGpUbWdPUnJabWlIcHZ5UjNtN0NMTUNRN29ZRGVXU28xQmhJTVg2eEZnaUdrcW9UVklHMHJycm1nT0JkdGJReVVHeV8tYm12UDlOU0lpNHFidXBQbUFSSVVmWUl1M1BVMFFncm0xSldkVzBrb2poRFMyaVUwcUZvMHl0QlZIZ1h1MjZwR3AtZWhqdzN4UVhtT2hUa1lQU3VudzNXdW1FcVY3VnQ3RmpkQnFQemlrQlF3WGhBNWxOZXJ6Zm9KVFlEZExUXzlqODhYaFNNMzVWTzFNMmVTcWdodDZoRmZTUzlhLVlOSU5fYW1vNXctaFpFMC1pUllRZW11d1JQN25sbldHVjI1anc2UC1ycndjTGtxWk55WmpJeU1wOVR0RnlTdFpad1dkRmlUNDE0d240TDlKc3JFUXdOYzd5UTFYSXUzLTQ2Y1ZGcWE3R2RyQ0I1WDMtMHBScEFzZDV4UEkyanh4ckJZUjdTYnJGZjAxQkU3MEJ6OXdybGRaWHNod1hZZEhVOXRpMWRLbVJsRGd0UDRDN3JsRzF4T0RpcnczRU5TM0RKVjVkWTRqNTl6bmhQdmdvaEg1U2kya0QtQ0l4ZHVUcGxkNi1vNVVVOEcyWXhxZWc5N1lKMk4tT0o3ZFVzYjJtT3NVZFJiSTFNUnpaSmFOeDZaLWVpZlc0VUhZRHdXOUMyQ3cwaXBQUDRJN1g1YkwzaTFiRVRxRFY5UTdZU1dSaGR6NUw3aEtac2RENXF3WEpVN0dXVTlQR0F6MFlpWl83MU44NVR1ZUtPVUNlZ205YUIwOFoxUDBvTlI0SU52emVvQ3VZXy1jTlFXRWZXQ0d5RHJ0eV9JeE5wMHl0b3FVSjNoVzg2d21hYVNYY3Q0dkFaVEZwa09tRnFBbEtoOUlGY2xkeVJoZGYzQUxYNFZfb0ZiaU5VRjJPbGhieXYtWTFKckZwenVCUGFva1IwVVFORVQ4SDMxWHVuRWhBRGd0cVlsc3kyQ0RyY2ZIVDlwcGh5ampySV9uOVpsVmlWbGoxMEg3SXh6NzRJbmZXRlhMMWc0RXhzeWtnQlJ0VnZSdENkbEpOdENwUzItUjZhZWFYRFhzbDM1WDBxaGFPX19CSG1KZjRTTU5JemcxZzJRSFY5bkx4TTlIZFNHOW1USWxBYWhEZ1FSNVdSSDJETUZwMi1Hd0RESkF2cVA1TVJGTEtPUl9oN3gzVEIwSzZOVzlOWXhNa2I1Vzc1SV9tdENfRy1rQTNzRlZGSTYwQmJIaGswZUNWSnRDVXFfdWFCckZZcnJOT2Rfb3FrcWI4S1lVRTMyRnZJQTRZV1VsU0xobGRjekhtbG9LamR2d1hfVklsM3JBeW9SRzJnWVdiWDRzN1ltcXdSVGoxRVBvczViVXNjMUxBazZUdS1WbkRQX0h1MzdNd3ltVDUzd2FGdi1XeUMybV9ia1YxQVBPdnUxY1dfT2M5eEpZR2JHMkdZbWdDZTRERXRYOWxodndkTXltVW40c0t0bVA5YWxuRzM3LWlCdmJiYmF5dkNBY3ozbUw1Zm5zRmpBdk5ORmFZRWJKM3Q2UDdKNl9zaUV5eVVGbkF0QmZSZzk5dGo3UjNIQWxwcjRlVTdUT2s1VGFjdndvX2c3d1VmaHRMZU10M1ZKVk9Ma3dZb1kwYVV5Z2NlTjUxdUYtZXRnRTRzQlp1aFp0OUF5TVBwN1gzU21kRmJ6OUlOeUFOOEhEOU5WSENNZndvLXdoVUFJYVFDTWEyakJEcTVSVDhJOWJscU8taThqNUZkdThCOUlXcldndFBTZk9QVnlMaUphUU5sUktpb1plZDZOQnFzNFNMUzRWbWFVQWhUWmJfem96X0cxWXVTcUxCeDhOc3E2OEpFa2lzWHFIV0p3eGdBZmN1aXBhYjExZTZqaUY4S0ZudTNhcUx2WlpuTU9lNUk2ZmNyN0JCODdYMGNEU2JsZkZXYlRFaTJQUTI5RU5SMmtkV1NHQTVTTjEyZGZLYnhTNTg2Nl9aaWJqX2Q1U1NwQ3pRTGRBSUw0N3FNQ0ItMks1QVZmbURYVWdHMWFZTWhGNURVOUg0bGVuMUozanlxTnRwbVlGX2RnN2FBVTZlZjhDaXVzZEtVR1Z5azhzWHRrS1dYSG9rYkowTjQ1N0hyRWdNVWMya1ZmWmZvSnVTdHNiMHFDODNLckpjQ081SFlieGxuM0picGhKMnNQRURwY2hpQzF3dHRnNEFWcUlPYjVxZEhod0JDbWZhU01Ob21UWmRwd0NQRlpjOE5CUFBOT004U2JKNkFSUlFzRklYZGJobUoxQzZzT2wzZ3J1Z05aYThRVVNzcFktMGJDcXFfSkxVS2hhajI3dTdrR2poa21ZM3Z4UzFRblFsOFlOZVVUM0YxaFRuNjFWQ2E4ZlhvZjZpMWFtOGRuaGx0MTZxZE9TY1dsTTMyMHhsNXJ2MkduaGRkZXpYUWJ3cEt1U3YwMC1IRzM5eWRCb0lvaUhTQ2R4XzhEZl9zRk5GeHhCSWx2X3BkUkJ4NFZLVzdVRFZkbnpNNkpjUTFHY1pDV0ZOMFBaNTVpLUlmSnFrX1N5X05MTjRUeTVERUs5MG9kMFJ3di03U3BpMUM4YXNwaG1fangwYURIVjBpSVdCUkt4UW5HbWtGOUh3TUdPZjMxYXpVZDcwTmlDcTR6WldZb3VzbHRpRUgyN2lFTjlpUV85T0M4blJxMWx0cC1iU0FDOHhueDBLYjdLZGhNbjFPbE1RdmhhNlEzX3ZpT2ZsYllwNkU5TE9fZWFabDE4RWRoRWxiMk5aVFZrWmxjaW5MX1VrUGhUN29vbU1tWldESnczYTNBQ1RPd1VTNGNJdjdJU3p3QXZQLVlDNkQ1cTh4Rk1WNnRMUi1DT3VGREFPa28xejc2NUl1dzJSa2hCTlJublBRNGkydlJVRjlFbFotOWtraWFqQkNNTXBpT1hZM0NXNEpObGMxQUNuS29rOExMSnMxT3NLbjNfLTdpQW1BcDMxR1RZdVRvbElGbENWbHJqRlVrTXhYbFdiMmItUzlxR2ZxT2FCWXpMVVJYZXBfSFVwNTczU3JHUVhET3hSWm80Ry1KcE9mV3FYejVHSEVSS0pxOUtCc3V2VHNFVkRqYk5Od20tM0ttdFQ1eGdsc091WGFYNFgybzNVd3ZvbzEwUDJ0T0hvTVd3YnlHNnpNWC0wbkJOQTIwQ3VYdlUzaXY5NFhDNlNOOW9UdGZNUk4zZ0VJakpwS21SZlJtQjVWLUxfejFYZFc1cjRwR3ZUOGdZb2VJaTdJUS1MYlRJb0ZFYW9uYzM3MDd4b09BR1pnTEh3RFpnaGhxZURQamllNUhqTHg0cHJfN08wMkdGSVQwQUlqWDhLVGViY3J5NlVFTzY3RGhGQ0R6aXNsb2w4dnBVYndTd1Jhd3IwS1BxY0h1X05RcGsySzVNbXR5YlBVQi1IOGFUNkh5QjhRZk5BQmZvcGF6ZTNXenZkdy1GRjFGdE1saGdMSnotUkIyX1VqTlZFWnJER1YyNGQtMFZHU3hmRVNPUWFCdXV3QUxzOGVSbF9EdEZGUFNxbTdiYm5oWHdYak5qa3Zoem5WY1ZUdDREVUxGX0VQeS1jckhqS2lRLXQ1Y2tyOFRjYnVhajNUZmZOUE9kbU9PYXdqdk5DYUtEOVFiMW9yZTYxMFNUaDdvUTExUFZ1bklYSkRKTnJ1RURvOTR3ODREcWdWeHpRS2RETjZqeXpvbUpxMW5lWl84RzVocmJFQ3JfZlpMd3RCZEo5RWZ0MzIxNWV6bHlwdWJJWXhoaWxlM2FHSjBhWG14Sk94ZV96cXFvU1JwWDdKZldmZWdvdWVKdXVfaS1jZjdENXQzSzNyb1d3eWhUMU53QzgxemRiTTlkdFRxZU1OdEN5c1kxOEd2MTJMcnBJWEE0eXdJdFpOYVNMQTNLR292UFlGb0Ztdz0=
# Teamsbot Browser Bot Service
# For local testing: run the bot locally with `npm run dev` in service-teams-browser-bot
# The bot will connect back to localhost:8000 via WebSocket
TEAMSBOT_BROWSER_BOT_URL = http://localhost:4100
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = True
APP_DEBUG_CHAT_WORKFLOW_DIR = D:/Athi/Local/Web/poweron-swiss/local/debug
APP_DEBUG_ACCOUNTING_SYNC_ENABLED = True
APP_DEBUG_ACCOUNTING_SYNC_DIR = D:/Athi/Local/Web/poweron-swiss/local/debug/sync
# Azure Communication Services Email Configuration
MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt
MESSAGING_ACS_SENDER_EMAIL = DoNotReply@poweron.swiss
# Zurich WFS Parcels (dynamic map layer). Default: Stadt Zürich OGD. Override for full canton if wfs.zh.ch resolves.
# Connector_ZhWfsParcels_WFS_URL = https://wfs.zh.ch/av
# Connector_ZhWfsParcels_TYPENAMES = av_li_liegenschaften_a

View file

@ -1,92 +0,0 @@
# Integration Environment Configuration
# System Configuration
APP_ENV_TYPE = int
APP_ENV_LABEL = Integration Instance
APP_API_URL = https://api-int.poweron.swiss
# Force SameSite=None+Secure for auth cookies. Optional if APP_API_URL is https://
APP_COOKIE_SECURE = true
APP_KEY_SYSVAR = /srv/gateway/shared/secrets/master_key.txt
APP_INIT_PASS_ADMIN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjWm41MWZ4TUZGaVlrX3pWZWNwakJsY3Facm0wLVZDd1VKeTFoZEVZQnItcEdUUnVJS1NXeDBpM2xKbGRsYmxOSmRhc29PZjJSU2txQjdLbUVrTTE1NEJjUXBHbV9NOVJWZUR3QlJkQnJvTEU9
APP_INIT_PASS_EVENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjdmtrakgxa0djekZVNGtTZV8wM2I5UUpCZllveVBMWXROYk5yS3BiV3JEelJSM09VYTRONHpnY3VtMGxDRk5JTEZSRFhtcDZ0RVRmZ1RicTFhb3c5dVZRQ1o4SmlkLVpPTW5MMTU2eTQ0Vkk9
# PostgreSQL DB Host (porta-int-db on Infomaniak Public Cloud)
DB_HOST=db-int.poweron.swiss
DB_USER=poweron_dev
DB_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQnFGTFJneVFGQ09JYVgwVWRGXzRSQjJ2RnlGYS05WllIMURpUUNBS0poQS1yLUJDaFFQS2IyLTNTSTUtRTBfekF1R1U5dUhiOXdYdi1WSVF4bUltczVQUVJQN2Q0Mng3cHFWVndZVDJxc2ZicXRXVnc9
DB_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNUctb2RwU25iR3ZnanBOdHZhWUtIajZ1RnZzTEp4aDR0MktWRjNoeVBrY1Npd1R0VE9YVHp3M2w1cXRzbUxNaU82QUJvaDNFeVQyN05KblRWblBvbWtoT0VXbkNBbDQ5OHhwSUFnaDZGRG10Vmgtdm1YUkRsYUhFMzRVZURmSFlDTFIzVWg4MXNueDZyMGc5aVpFdWRxY3dkTExGM093ZTVUZVl5LUhGWnlRPQ==
APP_TOKEN_EXPIRY=300
# MFA Configuration
MFA_REQUIRE_ADMINS = True
# CORS Configuration
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://porta.poweron.swiss,https://porta-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss
# Logging configuration
APP_LOGGING_LOG_LEVEL = DEBUG
APP_LOGGING_LOG_DIR = srv/gateway/shared/logs
APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s
APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S
APP_LOGGING_CONSOLE_ENABLED = True
APP_LOGGING_FILE_ENABLED = True
APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5
# OAuth: Auth app (login/JWT) vs Data app (Graph / Google APIs)
Service_MSFT_AUTH_CLIENT_ID = 840b759a-4d79-4a7a-9598-f3ed204d99d8
Service_MSFT_AUTH_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kydlVubld1d1h6SUNSWW1aZ3p4X3Zod1NDTjhZVnVYS2lqOERGTFp2OXJ4TGRiNlRLVFpzLUVDTUhkZGhGUWdxa1djdEV5UWkyblN1UHZoaFBjaExNTEpGMG1PRGJEbDdHVll0Ungwcl9JemZ4ZXFzZUNFQmFlZi1DZFlCekU1S3E=
Service_MSFT_AUTH_REDIRECT_URI = https://api-int.poweron.swiss/api/msft/auth/login/callback
Service_MSFT_DATA_CLIENT_ID = 840b759a-4d79-4a7a-9598-f3ed204d99d8
Service_MSFT_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyS1hWZXEzUzZTTE5MUlJncVowMU95Y0hmV1hveDBZOWdLU1RIUWt3SGlXNGxVTXVKc2QyQmtmWTlJRU43ZnRDdnlDTGxQY0hTU25CWWFFdDhUem9HU0VYcTFJTVFEbVk0dUhmVzJNVlEzNTNWdjdmaW9WeUVDVW5PRmNFZEQzNTY=
Service_MSFT_DATA_REDIRECT_URI = https://api-int.poweron.swiss/api/msft/auth/connect/callback
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
Service_GOOGLE_AUTH_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyUTUwNXNGaHRNaGxxbF9sdWJ3Q0xLYU5yOHB4Yk8zMDZvQ29yaEhWOE5JMENXRk5jb2ZBdzRKQ2ZTTld6ZlIxemhOYzN1VE10TjBDRWZEMXlLVWRNYjZ0VG5RZ3I3NWt0SEJzMzdsUmRzcVNmbktRNHZqTUF6a2EyUkVUSFJnZFE=
Service_GOOGLE_AUTH_REDIRECT_URI = https://api-int.poweron.swiss/api/google/auth/login/callback
Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyV1FRVjF0c0d3d0dyWU1TdW9HdXVkdHdsVWZKYTJjbGZPRDhMRjA2M0FkaUZIVmhIUmFKNjg2ekFodHd6NG80VTI3TC1icW1LZ01jWVZuQ1pKRm5nMW5UREJEaGp2Wl9oRDRCSmZVT0JpTnkwXzgwY0pkV29yczQ5akF2d1ZGcVY=
Service_GOOGLE_DATA_REDIRECT_URI = https://api-int.poweron.swiss/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5SE1uVURMNVE3NkM4cHBKa2R2TjBnLWdpSXI5dHpKWGExZVFiUF95TFNnZ1NwLWFLdmh6eWFZTHVHYTBzU2FGRUpLYkVyM1NvZjZkWDZHN21qUER5ZVNOaGpCc3NrUGd3VnFTclF3OW1nUlVuWXQ1UVhDLVpyb1BwRExOeFpDeVhtbEhDVnd4TVdpbzNBNk5QQWFPdjdza0xBWGxFY1E3WFpCSUlNa1l4RDlBPQ==
Service_CLICKUP_OAUTH_REDIRECT_URI = https://api-int.poweron.swiss/api/clickup/auth/connect/callback
# Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI.
# Stripe Billing (both end with _SECRET for encryption script)
STRIPE_SECRET_KEY_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5ekdBaGNGVUlOQUpncTlzLWlTV0V5OWZzQkpDczhCUGw4U1JpTHZ0d3pfYlFNWElLRlNiNlNsaDRYTGZUTkg2OUFrTW1GZXpOUjBVbmRQWjN6ekhHd2ZSQ195OHlaeWh1TmxrUm10V2R3YmdncmFLbFMzVjdqcWJMSUJPR2xuSEozclNoZG1rZVBTaWg3OFQ1Qzdxb0wyQ2RKazc2dG1aZXBUTXlvbDZqLS1KOVI5M3BGc3NQZkZRbnFpRjIwWmh2ZHlVNlpxZVo2dWNmMjQ5eW02QmtzUT09
STRIPE_WEBHOOK_SECRET = INT_ENC:Z0FBQUFBQnA4UXZiUUVqTl9lREVRWTh1aHFDcFpwcXRkOUx4MS1ham9Ddkl6T0xzMnJuM1hhUHdGNG5CenY1MUg4RlJBOGFQTWl5cVd5MjJ2REItcHYyRmdLX3ZlT2p5Z3BRVkMtQnRoTVkteXlfaU92MVBtOEI0Ni1kbGlfa0NiRmFRRXNHLVE2NHI=
STRIPE_API_VERSION = 2026-01-28.clover
STRIPE_AUTOMATIC_TAX_ENABLED = false
STRIPE_TAX_RATE_ID_CH_VAT = txr_1TOQd14OUoIL0Osj7A0ZQlr0
# AI configuration
Connector_AiOpenai_API_SECRET = INT_ENC:Z0FBQUFBQnFGdnVIcUxPOUVXT0NlUlJNVENjRi1iLXdsR1ZuOXU3Sk1qbmVZOThYdUZrNGlDREJmMkttRVNyWlNsMHlDc2pnQ1VyZ0lzYXVkc0hHNm95bjFrejNRWVVGUWZOVTVYOGpKcF9QNGttc001TE9VdFdKa2FyUEYxY1VYOE1RenBObmNMbVFHeTdHbGpORVAwOWc3Rng1dWtlUEphZmVKV1otSE03a2FTVHVvMlNONWZ3N3hMR2FmdTEtdkdWOHV5d0RYWlZ3dVV1SEpKNHBjWG1QTEZ6SE5oS1VESEJ2MmVSRmh4azd3d1RmQ3FjMDVsbWxNc2EzZDdWMXYyaFBOMTZFSUltUkk2ZTEtZ0FHRW5pZkhON1Rna3lYX1Z5TWRQNkZEX3NXVHlPYkVuMW5zcE09
Connector_AiAnthropic_API_SECRET = INT_ENC:Z0FBQUFBQnFGd0hhbGxMRUlZc1A2d1RvOWdFX3NkQXM0RG5LOTZQYWpOc21tTTJWU09nbS12M29YLVVzVk8zWGdrWXIzb05meW56dkRtTElVN1ZndkZ5eHdWMGxGVjBPTlRvTGxpTzVzcFlzdnVhTTh0R0gtM2M2Mk9ac3dnc0RYYkx2c3BDdnoxVXJLX2tPMTVpZXdmQll3cHF3dEhGWGRlb3JLZjlNTVJpZTN1TzFtMU5yZmdXTnZuZ1lXN0p5VUdsVXBDUXJoY1Y3aFBkUW1HbmJJZmZaR1cwTVNQR0VZUT09
Connector_AiPerplexity_API_SECRET = INT_ENC:Z0FBQUFBQnFCdlFla1h1R1M3QlQ5XzJhS0x4eXFpTkZ3WHpLMWVZZldRMGpMX2psMFZ2RmpETTZMZ3ZXblo2MnhyemxYWXRsMHN1LXdZU3k5ampEMjMtdzcyb1J4Ri1rTmxPOWhJMF9MMEtzZ3d5dFZxSFY3TjNac3ZpTVJxUFFmUVpXeHEtbVBTUmtiR0lhQjhVcjM3U1NNX1ZHY1NxUFJ3PT0=
Connector_AiTavily_API_SECRET = INT_ENC:Z0FBQUFBQnFCdlFlbmRSZVRjTzVKRklFbFgwdVZJaE5jNVoyX3dVTVlRUFVUenc4X1JOX2laOHRoTU9mN1lTUVRzb2xNZjJXVjhEYnVIaXdkSWN4NEpJbTFJZFN2cmkwUkJ0ZXNKT2NidktjdDFJX1BkZ3QwU3dQRzg0aG9aNmtxc1FZZ1ZBRjQyM3lOSS1EYkpqWmxoV0xWWE1Fc01uN3RnPT0=
Connector_AiPrivateLlm_API_SECRET = INT_ENC:Z0FBQUFBQnBudkpGSjZ1NWh0aWc1R3Z4MHNaeS1HamtUbndhcUZFZDlqUDhjSmg5eHFfdlVkU0RsVkJ2UVRaMWs3aWhraG5jSlc0YkxNWHVmR2JoSW5ENFFCdkJBM0VienlKSnhzNnBKbTJOUTFKczRfWlQ3bWpmUkRTT1I1OGNUSTlQdExacGRpeXg=
Connector_AiMistral_API_SECRET = INT_ENC:Z0FBQUFBQnFCdlFlU2tMLTFnQWhET2Nia2pTcVpBakRaSVFDdUpHRzZ1bkhGVVhMeEVlSnFZU3F3UFRBUkNMMU4tQU92OUdTeDlpM2VZbXJzLURQZ1lPLVB3azgxSDZabkhkSHJ5Y005aWhtcDJzajk3a2JDQUxCZlNKRGw5elJuSzJMUUpTZ2hiSlU=
Service_MSFT_TENANT_ID = common
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = INT_ENC:Z0FBQUFBQnFIc3YtSjhlcklrU2JCOW5mdHFHd0dLTUZZZk9PT3o5RWt5RjAxX2s3ekJRLUUzU0dNSnNseTE4bUpNTnZSTWg0QV9mWm5iX19aWjV4YnRXU1JBSm1INVB5dXNRT2JiYk1tLWRSS29pdTRMdS1lMDZxMkx4VTh3bU5aVWh3cEwyOE1QcXVockgtZWh5bzdNVXQyemFuSmZqRzZZYmNGN21JdjNwNWpPRXB6WU1qSU5rZUVSb3JBS0lhcThvakkwbTRUUHhBdjRZdWNsZ1Z1RmFaNGZLcEpaNVNLdFAxYzFXdTJydU9COWJ0bkNyYUF2X2FNc1BfT05teEs1SE9PeGhPd3VJSFY2VFJ5VEl6V3R3bzd6OTVKTEVRcmt5ZzdBMXBFY1A5dUFJRFJONFBlaDlJcjNBQnBraC0wMTBhNW8wYWZaeHNWclVTOVotLTdWSmVuYzJKcUZSUkdrdXB3VEVESzd4UTI0bGd6SzdCajdoazZXVTVCaGRiaWJaOHg5Z2thSWItcS05U25DbUdrT2M1QV81WEg2dlJfMlBtZU9Bc3V5bmtBWHRoRUVLR2lWNHY3M3hHcU1raFRFOWQwSEtUU1RDWDFRNFlkNHVnTkZDbk5zS3RZeGR2Z015RnRGc3NndFVEQjc4bVpNeE81bXc1MnQ2QjNZeHZCbUJJZVJ2TE5xWEd4M3hHT2hJWW5DOWMxQlNmZE9uMVRGVnRwTUlXZjZCRUZBLU9GWVZGWFpZbUE3WVlpZU1DX1Z0bWQ0bjlaRThHOE9WR3VOVzlYWS1JampTNmxkNmFxWG54WDJjallIT3UyT0tGSzJpeG1tX0JoQjZxbEpESHBhMWZFa205bjdvTVFwSVVidnVzdURZVDAzVVpkekJ2SVZTZmhxQVJ2OWpuRGR2WFE3elMtb3B2ZzhpQVNvRmkzbzRrY1BuamVzM0E2eVM0bXBHTHgtYmhsVG5jNlB1Q1JHZU9HUlNfaTJSQkcwS2FSZnZSOW9oZzdXa1RUVTVTZTgwY01GYXQyQ0xWX1Fnb0xaOTRQY3hTclgweVJ5clc5OVpRWWlDb0JQVXoxVDA0bW8zUE55aGowb1ZZNEpBN2UtSTZTY2llRGhISFFkYWFYVlVBQ0IzbGxzVTQ2V2dsUGV1Y2I5bEZLRnlwdXRHMWZVcnBaTXNzNzNkUVFqR2xnSEQ1VlpTdXpwMFVVYjQ0enFlUnk0d3dDQUtSS1dUVnNyYnBKQW9TRjJxN2JNY2NhRWNONWRpWU5RbzNNZVJBS3EzN2ZMZ1E5VXQtMDFTZklLY1JiSDNYRlFuOF9VYUktS0xoY2IyR0xkT19qTEpIV1p6RFExUWNCQTdqN1kyS0Jaa2lyMDluenc1MS1vdmhPVlE5OUphWEY2dXFYNE04Z3lBUG5DNGZjTUVnYzEzYWhzTHpMdVBzT0dzRGJaT2x5b0pVbWJtUzJxdEd2VGtrc01kTlNPNURoVHhwZzU1d3pTZGJiTUZIME5tQ0xqNWJ2QS1QSEJHV2FEOExHWDByV19rVnc2R2pibnNENEo1cTh4bGNMX2ZpSTBMcjRvQWRhbW5xYVBiZkZzWTRERlVESEU2aHpvdzNMTjlCazRYeEJhMmZwdXY5T25IYkFTaUM3SmdIV1FCX2xxRXctWHZQOHgxLXI1c1JkWmcydkFTUmxFSU03cGtnallnTXplOElQbEJRSEE2aW5KREU0YUxwX25wOFhuS2RIbms1dXNIRHBtNjFtb3B3UGVGb0hwOENKM1hMclBwa3NBa2pFYnZYbEtFbUF0Y3pmeFRmMDNMaTZrR1BZWnBrNUQ1WlU1NVZQSWUxN3dwcXhhcjdXNTl4LVVpYVF3Y0wtRmFyNXZRNTE3UUc2cHVaVVNpaVdHbXRqQVJNZWZmNjdQQ2lwTGd6RFFZN2tSY2NEdmxvaXk4MTZMcmg0VGo3MTN2R2V6cmV3YjdQVlNEZTQySUpaY2pkTHZzUzdJLVJ2WnlOQ3Vmem5FZXRaWjBMWjF4ZEF3ZHJ4VF8tMVNsRnljejVsaEpGOU5JbnhydjNVdzNMOENrWUVsbXp0ZEhuVE1Vd0RJcnp2N0RXUGFuNDM2OXBPbV9LRDUwTWk1NHYwaDhlVEhKUmtEa09INURwNjV5ZE1VWmpRSGdjeXJNc3FqcjZDdmx5WXluNWZ2VlpsWmR2TXVXVnBubEFmQlRfaGRwRndCVXVkMjkyLWVhaDQtZDN1cmFZLUoybGRwbGQ5MTExU2NnZ2lueVNfSjFDQ2NkWGtNX2M1T2I4YnVJOUFueGIxbG1EYlZOcFYtQlE3cm90SE40X0ZjalhLdXM5S2l5aW84ZUJPMlR4MU9EVkhZcHdrX1Zqc0NhWEJacDZHMzQwSzdkdi1Rd2s4Y1dfLS1ES0NfYTNxYl84UTN1S0lIM0pVTTNEYlJ0YW55Tk4yVjBONXNTQWtVZTJ2V3B5eHBJcG9IWGRMMklob0hMbVVZZzJKbTFMUExOQm5HSEZzWHU0VGVIWlJMVzFLeFB0NkkyWFkwWk0wdjdHRmxSWFFoSkJ2Vm5NUWNQQlp6YWlIc2NKLUdhOVVycHd5N3NFMDNVWlAxZGQ1NzRGbm9LcWxEb2tKR1RnVEtvRUc1d3l4aU1IOUQ5RldUT3Z0a3lpRHpVSWJ4MjU4RWY5MEpCQ0VFdHNMbnkxOGswcE44QzJwNXFCVGpIa0VGc2VNXy1qdzVNRU9DaXg2MW9VX3FjUk41QVFVLURwVGFLRTkyNWlENy1IcGZjNW9wY0Y5Q3d5eFg5emVUUF9hV3ZTQWNaNEN0VzdJRlFBR0picXJoUERacWNLbDZhTE8wdWlfZ3kxd2QzOXBOZV9uaUNGMkNJbGhNd3k0S2t3dTRGWVVxTTFRRlg3Ui1zLW1FLU1Mai1yaURjb2Fob2c4MDUyRHN5aldUVWMxLTVNbm5VQTdrYy0zLVFyOHRkNzZ3dGdhbXZXN3JHNkdfZ2RuRXFDM3R2TVB1cDNOdWZGTmpFNnNFTmMxTmFuZDdJUld5bERyQkJ0TGZXRk54NEdqN09hSmVMYV91NXUwNXFvMl9KV0hBNlB4bklNQ2U5WGZLUTdlX2dJenVGcDYwWHBsdTNpbE5mWGhWeXFuUkFPV0puR2h0RkhrR2MwTzJGUmp4bUR6UFlUWTlNbTJLa19hTUZZR0dscVpBbFBReTBRMDNseXo4SXNnZWt4VFdpOERqLV9ZczRkR0QwRFJQM0pqdHluWktDUlp6WU9XSjVNZi1tYnNzcVlGTDRFMzNlSmRTazFfTkNxSjAwM0wxNk9Sd2h1SWpfOW5MVWMtVXYyYlVZR0VuaHRpN1pnNnpHME5raVBMd2h2dDRyMV8yZGFJNnlkcmhtSWdmNlpLN19NcjNkc002dXFxQzhTaDZzRlgzNUJ1SzVpVnp6NVU1Y2luUlM4UEJoajNTOUJadnE1MlhzV0kxSzBObXkteVhNM3RKYW9heDVWWFJ1NGlDM0l0elRPbThwUU9oYkVkbC1PZFNLSHY3WHJiZWpEamNIVC00MlNNWV9qcHdjNDRjRlVhZXlrLTlicVBNaDlDeXdRb0Fwc3RmUGFvbURQZ29yckliaS1VUDNxcXVlYTJJRUhXNUVobk1KUDhHZE16UzBLeDViYVRwZWY3d2w0d253eEZYcExKRGpsaGlBUElaTzB3eUVadnROX1dabENGb3R4ZF9aS05KY0dHTVZaYzRFc1Z4TlZGbFd2NjdYRzJMTzVwU2NaN1Y3MzQ2Z2pzV2RSMzJBbjg0MEhaZmhoREloY0oxOFdjNDZNdVZfYlRKU1Q1M2hYdHgwUjVsTV9USjZCZXlQTTdNRWc3bUxOcXRDVkpTdnJxR0hkWWpaRUdrOEFyNHk4MENwVzdob0hUSkJvam4zZW1kcGxZUjg0RXFRNnBxSUg1MDVHdHRwVlFkWWhHM0ZyZVFvMF96R2V5YjBuMnVZTU5CQ3pVci16SGJlQTQtbnFLa1E2eHFncUg3UmYyYlZvOF82a3d2ZE4tbmxIUlNYYjlrck9QYk5CcV9faXludS1yem1JNjFBdVYyb21RQWFMMFkxX0s1TjQ4czZ2WXI3X0FzRWdNTlZndHl4bnVOTHl2YlZfaURQV053dHl4N1czRFdzaVFnRHB0MWRDV2ZuU2lzX1NZZkRQYzhsT3ItZWw0dVJlVmtFWUM5cEppOGxuYVdpQkN5dV9hQ2dodTJvV3REVkw2dVVDaGtvc0Zqd0V2dldLZEVNRVRRNVRUVmw5aHZmZEpHdk1wS0xwRFc5Vmx4dTdfdGZDRUtCU29qdEVIOW5VdjBmeGpFMFZHSUthamtVN1E2bDZqaEFackVSQnZMN0tyaUhIcUs1ZHMzMzl2TnhadGIwZW5QNS1BM3pSODY3WVFsLU1jeUpCMG1PWmhPVT0=
# Teamsbot Browser Bot Service (service-main-teams-browser-bot on Infomaniak)
TEAMSBOT_BROWSER_BOT_URL = http://teamsbot.poweron.swiss:4100
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = FALSE
APP_DEBUG_CHAT_WORKFLOW_DIR = ./test-chat
APP_DEBUG_ACCOUNTING_SYNC_ENABLED = FALSE
APP_DEBUG_ACCOUNTING_SYNC_DIR = ./debug/sync
# Azure Communication Services Email Configuration
MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt
MESSAGING_ACS_SENDER_EMAIL = DoNotReply@poweron.swiss

View file

@ -1,91 +0,0 @@
# Production Environment Configuration
# System Configuration
APP_ENV_TYPE = prod
APP_ENV_LABEL = Production Instance Forgejo
APP_KEY_SYSVAR = /srv/gateway/shared/secrets/master_key.txt
APP_INIT_PASS_ADMIN_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3UnJRV0sySFlDblpXUlREclREaW1WbUt6bGtQYkdrNkZDOXNOLXFua1hqeFF2RHJnRXJ5VlVGV3hOZm41QjZOMlNTb0duYXNxZi05dXVTc2xDVkx0SVBFLUhncVo5T0VUZHE0UTZLWWw3ck09
APP_INIT_PASS_EVENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3QVpIY19DQVZSSzJmc2F0VEZvQlU1cHBhTEgxdHdnR3g4eW01aTEzYTUxc1gxTDR1RVVpSHRXYjV6N1BLZUdCUGlfOW1qdy0xSHFVRkNBcGZvaGlSSkZycXRuUllaWnpyVGRoeFg1dGEyNUk9
APP_API_URL = https://api.poweron.swiss
# PostgreSQL DB Host (porta-main-db on Infomaniak Public Cloud)
DB_HOST=db.poweron.swiss
DB_USER=poweron_dev
DB_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQnA4UXZiMnRoUzVlbVRLX3JTRl94cVpMaURtMndZVmFBYXdvdnIxLV81dWwxWmhmcUlCMUFZbDhRT2NsQmNqSl9ZMmRWRVN1Y2JqNlVwOXRJY1VBTm1oSjNiaFE9PQ==
DB_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3elhfV0Rnd2pQRjlMdkVwX1FnSmRhSzNZUlV5SVpaWXBNX1hpa2xPZGdMSWpnN2ZINHQxeGZnNHJweU5pZjlyYlY5Qm9zOUZEbl9wUEgtZHZXd1NhR19JSG9kbFU4MnFGQnllbFhRQVphRGQyNHlFVWR5VHQyUUpqN0stUmRuY2QyTi1oalczRHpLTEJqWURjZWs4YjZvT2U5YnFqcXEwdEpxV05fX05QMmtrPQ==
APP_TOKEN_EXPIRY=300
# MFA Configuration
MFA_REQUIRE_ADMINS = True
# CORS Configuration
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://porta.poweron.swiss,https://porta-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss
# Logging configuration
APP_LOGGING_LOG_LEVEL = DEBUG
APP_LOGGING_LOG_DIR = srv/gateway/shared/logs
APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s
APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S
APP_LOGGING_CONSOLE_ENABLED = True
APP_LOGGING_FILE_ENABLED = True
APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5
# OAuth: Auth app (login/JWT) vs Data app (Graph / Google APIs)
Service_MSFT_AUTH_CLIENT_ID = 840b759a-4d79-4a7a-9598-f3ed204d99d8
Service_MSFT_AUTH_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnFBa1kyeUZORDYxOFdlNHk1N25kV3pSQVJMUVFwLUFlMzlzQjQ1eVljOTlzX184RndsTmtTV1FjdWkyQlBiUkdCbGt5S2ltZjJxa2I2dHBMdnJqZnhFSnBCampHYjB3RG5URDM1YzZSLVd6TGdaRXRVcEdadE5zM2thNV9SZy1KZDdLSHY=
Service_MSFT_AUTH_REDIRECT_URI=https://api.poweron.swiss/api/msft/auth/login/callback
Service_MSFT_DATA_CLIENT_ID = 840b759a-4d79-4a7a-9598-f3ed204d99d8
Service_MSFT_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnFBa1kySk5uMmlWczBWTE00MHBIcWlBbVJmVmc3MlBWbDA1YTFaS3psZjVLd3d1X2FvRHV0X0c5blpLV0FpY05aMTJMMzUtcG8wakF2TlM3SGQ2VjFZM3JLT1MwTlZ0bm9BRlpkbHVPQTFNaXJvazlQRzN4M2ZZNEVhV1JHV190dWluSUk=
Service_MSFT_DATA_REDIRECT_URI = https://api.poweron.swiss/api/msft/auth/connect/callback
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
Service_GOOGLE_AUTH_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnFBa1kybjVVZ0FldUE1NTJiY2U1N0I0aVU0Z2hfeWlYc2tTdmlxTS1NdGxsRnFHdjZVcW5RRHZkUFhzUTVyX2RaZHlrQThRdTdCRmVBelBOcDlsbFQyd19SZExuWEM5aTcwQ0FvY3ctMUlWU1pndDE0MkdzeTZZRHkwLWU3aW56LW1jS20=
Service_GOOGLE_AUTH_REDIRECT_URI = https://api.poweron.swiss/api/google/auth/login/callback
Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
Service_GOOGLE_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnFBa1kyMnFma3VPOVJtTFFrNDRLN0NkWHY2dUZDWlJzdDVMd3p3N19IY0tWdURRRzExOGZCMjJOYmpKT1E0cTVwYlgtcVJINTY0anZPc1VoTW00cHl6NVh3ZHVTek1oT1RqWUhtamRkZ1dENWlwNTlZSU1oNWczeGdEOC1Gbk5XU2RBcmI=
Service_GOOGLE_DATA_REDIRECT_URI = https://api.poweron.swiss/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnB5dkd6VGw5WDdhdDRsVENSalhSSUV0OFFxbEx0V1l6aktNV0E5Y18xU3JHLUlqMWVJdmxyajAydVZRaDJkZzJOVXhxRV9ROFRZbWxlRjh4c3NtQnRFMmRtZWpzTWVsdngtWldlNXRKTURHQjJCOEt6alMwQlkwOFYyVVJWNURJUGJIZDIxYVlfNnBrMU54M0Q3TVdVbFZqRkJKTUtqa05wUkV4eGZvbXNsVi1nNVdBPQ==
Service_CLICKUP_OAUTH_REDIRECT_URI = https://api.poweron.swiss/api/clickup/auth/connect/callback
# Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI.
# Stripe Billing (both end with _SECRET for encryption script)
STRIPE_SECRET_KEY_SECRET = PROD_ENC:Z0FBQUFBQnB5dkd6aVA3R3VRS3VHMUgzUEVjYkR4eUZKWFhPUzFTTVlHNnBvT3FienNQaUlBWVpPLXJyVGpGMWk4LXktMXphX0J6ZTVESkJxdjNNa3ZJbF9wX2ppYzdjYlF0cmdVamlEWWJDSmJYYkJseHctTlh4dnNoQWs4SG5haVl2TTNDdXpuaFpqeDBtNkFCbUxMa0RaWG14dmxyOEdILTNrZ2licmNpbXVkN2lFSWoxZW1BODNpV0ZTQ0VaeXRmR1d4RjExMlVFS3MtQU9zZXZlZE1mTmY3OWctUXJHdz09
STRIPE_WEBHOOK_SECRET = PROD_ENC:Z0FBQUFBQnBudkpGNUpTWldsakYydFhFelBrR1lSaWxYT3kyMENOMUljZTJUZHBWcEhhdWVCMzYxZXQ5b3VlTFVRalFiTVdsbGxrdUx0RDFwSEpsOC1sTDJRTEJNQlA3S3ZaQzBtV1h6bWp5VnlMZUgwUlF3cXYxcnljZVE5SWdzLVg3V0syOWRYS08=
STRIPE_API_VERSION = 2026-01-28.clover
STRIPE_AUTOMATIC_TAX_ENABLED = false
STRIPE_TAX_RATE_ID_CH_VAT = txr_1TOQZG8WqlVsabrfFEu49pah
# AI configuration
Connector_AiOpenai_API_SECRET = PROD_ENC:Z0FBQUFBQnFGdnVKZ2Z0U2s4cnpUN01mRVkzQUYyVm43NzZLOWJBODlvRlNFdTNGbzZHblNzUFJ2X0I3SXRFQTRXWlFYZjY1aUVVOTgxSU1KemZ4Wkl1NzFQb2JIcnM3bjg5bkRDYmpNVjVjTG55QmtSUVpZejEtckZTd20xRzd2NmhVSkJUQTFUZWk0dzhrUnJuNWZPa2NPSDR6QnQ1a0RCbWM4Y1h3Mmh2NmQ5SHFOR2FISndEMzF4Y29YcVlKaVNyNGM2VWFINUg4MjVMcHZJTUxVWXJNNUZVdW9GUkx0ZkZlZTJqRGI4ZkVuaklHMEotb3FyOEFka1c2WC1VclJZeHFucmJlRjhlUUhLNWdFX0xaRFp0ODNFNFZaWEdSTU5QbDcxbUxlclN0X2t0c1dpWXVJeFE9
Connector_AiAnthropic_API_SECRET = PROD_ENC:Z0FBQUFBQnFGd0hjRzFXSXhjMVZWOW40ZFRRREItclVxODVDdFdSa2tzVVJ2TWVZaVl5UE40YzgxR2d4RVdhVUFaQy1VZVRRMzFnZW1NcjlNY1h6ZVJta3F5STI5Y2taRVlXbFREb2paMTZpRVZpdEVBVnJrSjlvS1lSMzB3V3FkWW56WlNhQUFiby1Mb2RCb0VHQ2NYYmNOUGZ5UEdseGJic2ZSQk1ReXlTRnJITVY3SEdPb296eGNIdXNRME5LOTlZUlRvclJRX2R3ZHlxM0puXzlWRzY2eHliY1FUNmxSZz09
Connector_AiPerplexity_API_SECRET = PROD_ENC:Z0FBQUFBQnFGdnVLODRmYXo5T3BxSDJnZXgzRlFfR0oyWXVkeVRZbk14VkdDV3pTaWVfV3Y3R21LaWJpSC1laTg1T3NYREI2RzBBWWtraFJud0U2ZnhVQzJ0bnViVzJtOWh4dDZ3VUdoZUxaUzdhSkM4N3ZOOTFINmV1TGNmRE9RRmtfeTduVEV6QnYyRTZJaGxGb3ZFSmZmZ1JxUDdFSVBRPT0=
Connector_AiTavily_API_SECRET = PROD_ENC:Z0FBQUFBQnFGdnVLcTlLSFJ5b0gwRmJLMFB5MzA3S3FYbmhKV2VzbHI0ZFUzOUJNdHVYQlQ0ckdicW1WWG5CNEkyWVlrR0gwQ0ZramJ1c19JS290MmlvWVhYWW92cEhIdmRTRXdPQzZpVFdDaU9MQzFlMEdPYUVnYy1HZlM1ODVuYnZGRnVZVFZpYzZBcUNRekVBZFFzVExQV254OUZ0aHVBPT0=
Connector_AiPrivateLlm_API_SECRET = PROD_ENC:Z0FBQUFBQnBudkpGanZ6U3pzZWkwXzVPWGtIQ040XzFrTXc5QWRnazdEeEktaUJ0akJmNnEzbWUzNHczLTJfc2dIdzBDY0FTaXZYcDhxNFdNbTNtbEJTb2VRZ0ZYd05hdlNLR1h6SUFzVml2Z1FLY1BjTl90UWozUGxtak1URnhhZmNDRWFTb0dKVUo=
Connector_AiMistral_API_SECRET = PROD_ENC:Z0FBQUFBQnFGdnVLRHplbzNheDhIdndsU0xUeGlBYVVXWDRzOF9Tek41WjEtSmNqbnVHRXFaZ0dramlfZWlQelpJWVh5T0F2azBaQWU3ajU0TWljaGpMeTlra0g0LVhKeTRKNGxKY0ZqSkxwdTJLdWM5cWdMVC1TVkpLb2lPdHhyeWtieFJFOHdkVy0=
Service_MSFT_TENANT_ID = common
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = PROD_ENC:Z0FBQUFBQnFIc3YtNDZzenJuZEZiQnVMOWRmZjl3R29QOWZRaGlPdk56WG1DR0FSZU5DM3dENWdoMmRpaks1U1VDNDJkZ3d3UXhSbXlkZ2h3SGZfdk54WXVidF82VkdJQXZiRTk0UlhZaUY1b2kwNzNPSm52VFdsdkwtaHJBb2dpRDBVLXRwd19Bb0dUZDkyV1VWZDJ1TG5mZ0ktYXpuS3U1U0JkZUk5TXpMdnhOaUtMN3BIb0pEZ1N0SlpFN3NNby15VTRfWWtxaF9DYjlJcnVKb0ZualVMTUx2aVNGY0JJdE1oZy1xSVBUZDF1aDM0TGVlTzVrNkFHcjlhcEk0SmRIMTFGdDFTMVUxX1dERk9NTXZMb0tVTFRoc20xME1uRkdVV0Z5N200ZTQzSjVsVExoa2VRZmFBU21ZczF0Vm9Ib3BZM2ZneDkwak12UmFyWWd0eng3ZVVFTUFLVzNOazcxeUhLVWUxcEFIZWtNRi1mT29kM1pqNGJJUUh3UVBlNGY3SlotOWZFUk5aQXFXcUFVdnUzc0Z5bERXYUNPbG14VnBNenFvb2tiQ3lZeHNHUVBlQTdTdVdXOEkxaGxCX016WWktWmN2WFcwM0VmVHdvMHVnY212VFE2cjJwUjdENkFCZF9GcUktWWpmWlNXNWVTMHBPdzVxRi15d3FSRDFra2k0NEFmTmpUeVh3SHRuZWE3WGJ4eUNIcE5tdnRqX2NCZnJoMEI2emU4U0ZYN1Nmdlhva1NacFo3UFh3WnpSdGw5ZmNpSGhicFo0ZThReXl3LW9vUzZaMkFHX2lJalFEMWtjZVdqbVpIZGk0cEdEU01TMl9xQkdSNDllTS1GV3lXS0xROTJvSlhaTjlXenJhQ3lOd2p0VjR5ZjEyektUZGJ3UThJOVJuMzhsTTVBVW9BcDFtcjk5Y0pVeW0zX3R0Nk81R3VDRWEzZnRqSXhFUW5ONHFTSWlwQU4yazlDb01KYlFQRjBFVTljdEJIY29WdF9hUkRJOThVTVFfWlJQUXI0Z3RzWFlzR1ZxUWFBd2I1SW1EMWlKdVprT3dKYTlaREp6TkZEZmVsZGEyalZGc3dHaUkyamdmQWtUT2czNzBCZEg0Vk1HSHFpRnhRYzBRNnN3TFkyaE9uMTVXN1VJTmJwbTNUMTdZbVRyc2d6Yl9aaVBXNmFvanROQVhfbWpXTDRlR1RfbklnYnJUQTZPX2JfNnlrWDVDUWJ4Z3YwNXVsTkJFQlRhTG5DVHpwejdsMGl1bzRfRXRTU2dmb3BVMUo4VkQwa0hsTmFBZnVjVzRrQmNzS2R0ZHNGV24yQnktWENtMUp6eG1MQW1ENE1vWFpFUF9PMEpWZVlxX05hSW1QUGlVT1l3MFp4bDBDZVVldHlEUlVCY1VvVlBNTlBhWFlmcVRobDNqRHo0QjZvNDBqVUVKN3JOb2dtYXQxSWw5NERSeEVRdHNUWndzUkY5RjdBOG1FZFRiVTNVSzl5bDNwdTl2SVd5aW5Ub2Q1YlBDRnpBUDkteU44YnV5X05ONmNndm9teUpqaFZVcVlHdGVRcXRpZkJLVnRuMTJSUFhGWndibExqRW03YUJTWXZXUXJ5WXlvd01ISDFuUFpaMFJzNFVQbWRUb2h1Zi1rcXJXMkRQSUFPeWFJN3lzOFc1d3BjWG1kbWlQWGUwelNiSnJXbUpnajdlQTlQR19XNTF0Q3JYcUMzaGp3eU0yZGhKa3FtX0tleHBfekZaWlRJRlZlSzNDVU56cml0TnFJeUc3b09uYVlwbGxFVFR6WFJVMzRmak5yWjBhcjl5ZmJpQ3hpajRXV1dwbDF5N25tNnI2bWtFem1TS08yV3JybUF0enYxRXpkUVdTNVp4WVB0aldJUUN3TnhHcHdMczh5MTFETzNWLXZFSktsdU1vM1JSNXhraDlJRDl0MEhvR1NOQWRaQW1NdzhpZnFVa1hvdXNwY2FvaThHQjVMOXdySnNIcWJlWERfLXVOcHhpN2ZZOW4yVzB3VTI2a3hvVmFkc29aX2ZUZkY5bi04WEV4MTlxNXQ4cTcwaHE4X3hDWkQxelRwSUl2amZOQ0JXRlJjRFhJNVhjNjRmaXp5eG15LTN1MFRvN3BHTFRZQ1ZFVFYyNUxleFpKTHlIVzRnVHk1Y3ZUbV9RUDdqN1Z2M2ZqVG8wa2RoVHJPeENFRDNHV0wwdi1DbEdOVDFJZnRiZGEydlZyM2tQVExOVlo3LXhIUnhZUnB6a2UzZXNtTjR0S2NzUmFNOWNiSHhHTnJDWHowWk1tbVFKUC14M25aQ1hyYjhJM2pxOEtZY0J1WTZrU3l6cDJOdk5iSXpBUk41MFFVellVZFU4UWVDZXFkQnJFbGxQX2J0S3pReU8zZUdsZUgtTnJuSlpfTjdxR3UxWTBEV0JaRV93eE9qa2dNa2tVTHRxMWNyeUh2VWNrYkdKM3BZOURkUlBxUDA3R2M4NnlMTVR2dmNMZi1lZlhzalRJWlFocGRleVRJYXBBY2hCXzFGZEU4ZVFxbHNic3RDV2FYN1dNaWpkaGdwYTEzRkZYRlEtRXR1cERHdnJKX1Zzb1Q0MnVYZkVhb0VYU1JPdFhoV29TMlhTaEppR1lTTURLYmZnNS1pSzl4T1k5MXJ0YV9qX0ZyQ1R6RFFzRndrTW9IUVlxcG5jcTEyYVU3dkpIR0tZZTZiOXNIRFpIalRtUDFBLVNyd1NfNUMtLW52NVpFZGpQenJCOGw0UlJZNlZVT1ZXTm92R3k4c3hTQXFoNFE3TUFHcjRWc01zT082anJZT0laakl5VUk1WDdDaWlubjIwS3RNcjBjTTdpbUNxSmxNR05JaWtEQURlS1h6N2h0NE9CcW5rQ3NXWkwyNXVBUU5mLTU5MG8xX29xZ0t6Z2pKWmhMNG1BNXBhYWkzY0loSmluUXNKdURwQWRIV2laM2dHQTFxV19lbkZXWmdfWEdiWEZsMGVIWDdoMnJ5dzM0ZGtBM3BSRVp2QzFNbFJSWXBManN5WmFVMlp6aUpWMF9jMTRPbWptM1lsTE41NG1kUW4tT0ZqTzNaZnZ5ZzBLZzNNc1N1X2FMMVJ0N3o4a25LMkxKVUE0dTNhU3hZX3RFMUtKcEgtX1B0cTdEMmYyMzdPaEhoeWhaUGRITC11NzRWYTJnZldiUkFvdG95a1RwWnNKaERkT0kxN1RJMzZQZzFiSjl1SlJieTJjaHBMYmZDUlhTT2hvQnRPaTNhS3NzaVc1Tms0X0FyUHRsSXdCLW1OUWk1RkRKc3pqSjVQTFFROEN5M3pxUGVjZHI4SVM3Qmx1S1A2bEEzNWlVWkFndGpUSm4wcV9jRjQ5T0l1c3ZqN0w3Z1dMV2ZtbU9MbTVSOXphX3VLMko2ZEs3U0NIaFFIMVFIcnN0OGIxSjdxNGlHUHRnOEJDaGwzcXJYNFBnOGdFSVFuSGUyOWJ3WmtlVGhGQWk0THdZd1hUbGRydk83SWVzWUJrb21tSlNvVkJjdWYtcWo0aEc1Ri1XNTZoSENaRWJISmp3UlJNMU9vSnNzZ0VudXpxMDA3aGdfSDBNZlA0Y1gybkF4dGl6SzFOc1VMN0dzVkQxVllkSDhyby12SWNxTFRYdThJUm13S3p3cGFYc05TbVc2YVNtZEdCOFBCUXhadkIzNmdkbXpnc1pLYUhzOEtsY2kxVmNYZm9wOS1LOERLRHJhY2VhanNjaThUZW1rS01wUW05SFJxOGd1VF9STlJZWDRiTV92dXlQTkdxN3BYYTN1SUhRSjRNTy1PZWpGd0xhUlVES0hiWE5LUkM5dHNvenR3TVMySC1ueUZXUkxFY2VyRmhISGc2U2ZxeXY2VkJULV9pOTU1QkI5VUNndnVQcVItTW96VTBqRTdzem1IQ1UxVWtWdjhvTERFeGJ6M3dJNERUV1BTeUlRcG1fbUVjQ0lNREF5QkpLeHJHRkFxQS1kZEE4bXJ2aVVSckVoTkZwNGtoRElIcUktQjA1bkNRclM4dWlqUVRXXzdlQ0VjQWZGSTZlR01NQmU5bHQ3bGNtZWU1eHVvRVdQRVU4Rmx0OFRTaWF3cGgyeFJoM25sRk1GNXJtdEpfcEJmYVFrZXd4eXl0c0ZKVjQ3MkFNRjh5bDBTbFZNd256dmxpQlo5Z1FRM1ZmVTJSb3VrZTk3cXVQYmZ6SnNUWGhlSUhrUjVWUHFwemNmbW1scWVxTkcxT1p5dVlvUjhCSVJaSnBjU0dpc3YzVkt1WUtrd2xoQlVNQXh1eDhmTXNISWMyUnBUMmIwamxlS0tjMVRiWDlBcE03b1BHR1FmdmlsX2ZlMTNCaFNvNG1TeTNiQXRNZ2Y1eE1IaFAxTUZGZ1YyZjEzTG9PaGRCdHJzVlB5Mm12T1NiX2RyT2d2RERCRWFHT0dadW5DZjNtdXE4cHhEQlpub2l3bz0=
# Teamsbot Browser Bot Service
TEAMSBOT_BROWSER_BOT_URL = http://teamsbot.poweron.swiss:4100
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = FALSE
APP_DEBUG_CHAT_WORKFLOW_DIR = ./test-chat
APP_DEBUG_ACCOUNTING_SYNC_ENABLED = FALSE
APP_DEBUG_ACCOUNTING_SYNC_DIR = ./debug/sync
# Azure Communication Services Email Configuration
MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt
MESSAGING_ACS_SENDER_EMAIL = DoNotReply@poweron.swiss

98
env_dev.env Normal file
View file

@ -0,0 +1,98 @@
# Development Environment Configuration
# System Configuration
APP_ENV_TYPE = dev
APP_ENV_LABEL = Development Instance Patrick
APP_API_URL = http://localhost:8000
APP_KEY_SYSVAR = D:/Athi/Local/Web/poweron/local/notes/key.txt
APP_INIT_PASS_ADMIN_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEeFFtRGtQeVUtcjlrU3dab1ZxUm9WSks0MlJVYUtERFlqUElHemZrOGNENk1tcmJNX3Vxc01UMDhlNU40VzZZRVBpUGNmT3podzZrOGhOeEJIUEt4eVlSWG5UYXA3d09DVXlLT21Kb1JYSUU9
APP_INIT_PASS_EVENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERzZjNm56WGVBdjJTeG5Udjd6OGQwUVotYXUzQjJ1YVNyVXVBa3NZVml3ODU0MVNkZjhWWmJwNUFkc19BcHlHMTU1Q3BRcHU0cDBoZkFlR2l6UEZQU3d2U3MtMDh5UDZteGFoQ0EyMUE1ckE9
# PostgreSQL DB Host
DB_HOST=localhost
DB_USER=poweron_dev
DB_PASSWORD_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEcUIxNEFfQ2xnS0RrSC1KNnUxTlVvTGZoMHgzaEI4Z3NlVzVROTVLak5Ubi1vaEZubFZaMTFKMGd6MXAxekN2d2NvMy1hRjg2UVhybktlcFA5anZ1WjFlQmZhcXdwaGhWdzRDc3ExeUhzWTg9
DB_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERjlrSktmZHVuQnJ1VVJDdndLaUcxZGJsT2ZlUFRlcFdOZ001RnlzM2FhLWhRV2tjWWFhaWQwQ3hkcUFvbThMcndxSjFpYTdfRV9OZGhTcksxbXFTZWg5MDZvOHpCVXBHcDJYaHlJM0tyNWRZckZsVHpQcmxTZHJoZUs1M3lfU2ljRnJaTmNSQ0w0X085OXI0QW80M2xfQnJqZmZ6VEh3TUltX0xzeE42SGtZPQ==
APP_TOKEN_EXPIRY=300
# CORS Configuration
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron-center.net
# Logging configuration
APP_LOGGING_LOG_LEVEL = DEBUG
APP_LOGGING_LOG_DIR = D:/Athi/Local/Web/poweron/local/logs
APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s
APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S
APP_LOGGING_CONSOLE_ENABLED = True
APP_LOGGING_FILE_ENABLED = True
APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5
# OAuth: Auth app (login/JWT) vs Data app (Microsoft Graph / Google APIs). Same IDs until you split apps in Azure / GCP.
Service_MSFT_AUTH_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_AUTH_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm83T29rV1pQelMtc1p1MXR4NTFpa19CTEhHQ0xfNmdPUmZqcWp5UHBMS0hYTGl4c1pPdmhTNTJVWUl5WnlnUUZhV0VTRzVCb0d5YjR1NnZPZk5CZ0dGazNGdUJVbjkxeVdrYlNiVjJUYzF2aVFtQnVxTHFqTTJqZlF0RTFGNmE1OGN1TEk=
Service_MSFT_AUTH_REDIRECT_URI = http://localhost:8000/api/msft/auth/login/callback
Service_MSFT_DATA_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_DATA_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm83T29rV1pQelMtc1p1MXR4NTFpa19CTEhHQ0xfNmdPUmZqcWp5UHBMS0hYTGl4c1pPdmhTNTJVWUl5WnlnUUZhV0VTRzVCb0d5YjR1NnZPZk5CZ0dGazNGdUJVbjkxeVdrYlNiVjJUYzF2aVFtQnVxTHFqTTJqZlF0RTFGNmE1OGN1TEk=
Service_MSFT_DATA_REDIRECT_URI = http://localhost:8000/api/msft/auth/connect/callback
Service_GOOGLE_AUTH_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_AUTH_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETDJhbGVQMHlFQzNPVFI1ZzBMa3pNMGlQUHhaQm10eVl1bFlSeTBybzlTOWE2MURXQ0hkRlo0NlNGbHQxWEl1OVkxQnVKYlhhOXR1cUF4T3k0WDdscktkY1oyYllRTmdDTWpfbUdwWGtSd1JvNlYxeTBJdEtaaS1vYnItcW0yaFM=
Service_GOOGLE_AUTH_REDIRECT_URI = http://localhost:8000/api/google/auth/login/callback
Service_GOOGLE_DATA_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_DATA_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETDJhbGVQMHlFQzNPVFI1ZzBMa3pNMGlQUHhaQm10eVl1bFlSeTBybzlTOWE2MURXQ0hkRlo0NlNGbHQxWEl1OVkxQnVKYlhhOXR1cUF4T3k0WDdscktkY1oyYllRTmdDTWpfbUdwWGtSd1JvNlYxeTBJdEtaaS1vYnItcW0yaFM=
Service_GOOGLE_DATA_REDIRECT_URI = http://localhost:8000/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd4ZWVBeHVtRnpIT0VBN0tSZDhLRmFmN05DOVBOelJtLWhkVnJDRVBqUkh3bDFTZFRWaWQ1cWowdGNLUk5IQzlGN1J6RFVCaW8zRnBwLVBnclJfdWgxV3pVRzFEV2lwcW5Rc19Xa1ROWXNJcUF0ajZaYUxOUXk0WHRsRmJLM25FaHV5T2IxdV92ZW1nRjhzaGpwU0l2Wm9FTkRnY2lJVjhuNHUwT29salAxYV8wPQ==
Service_CLICKUP_OAUTH_REDIRECT_URI = http://localhost:8000/api/clickup/auth/connect/callback
# Stripe Billing (both end with _SECRET for encryption script)
STRIPE_SECRET_KEY_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd5aHNGejgzQmpTdmprdzQxR19KZkh3MlhYUTNseFN3WnlaWjh2SDZyalN6aU9xSktkbUQwUnZrVnlvbGVRQm4yZFdiRU5aSEk5WVJuUnR4VUwtTm9OVk1WWmJQeU5QaDdib0hfVWV5U1BfYTFXRmdoOWdnOWxkb3JFQmF3bm45UjFUVUxmWGtGRkFKUGd6bmhpQlFnaVI3Q2lLdDlsY1VESk1vOEM0ZFBJNW1qcVZ0N2tPYmRLNmVKajZ2M3o3S05lWnRRVG5LdkRseW4wQ3VjNHNQZTZUdz09
STRIPE_WEBHOOK_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd5dDJMSHBrVk8wTzJhU2xzTTZCZWdvWmU2NGI2WklfRXRJZVUzaVYyOU9GLUZsalUwa2lPdEgtUHo0dVVvRDU1cy1saHJyU0Rxa2xQZjBuakExQzk3bmxBcU9WbEIxUEtpR1JoUFMxZG9ISGRZUXFhdFpSMGxvQUV3a0VLQllfUUtCOHZwTGdteV9rYTFOazBfSlN3ekNWblFpakJlZVlCTmNkWWQ4Sm01a1RCWTlnTlFHWVA0MkZYMlprUExrWFN2V0NVU1BTd1NKczFJbVo3VHpLdlc4UT09
STRIPE_API_VERSION = 2026-01-28.clover
# AI configuration
Connector_AiOpenai_API_SECRET = DEV_ENC:Z0FBQUFBQnBaSnM4TWFRRmxVQmNQblVIYmc1Y0Q3aW9zZUtDWlNWdGZjbFpncGp2NHN2QjkxMWxibUJnZDBId252MWk5TXN3Yk14ajFIdi1CTkx2ZWx2QzF5OFR6LUx5azQ3dnNLaXJBOHNxc0tlWmtZcTFVelF4eXBSM2JkbHd2eTM0VHNXdHNtVUprZWtPVzctNlJsZHNmM20tU1N6Q1Q2cHFYSi1tNlhZNDNabTVuaEVGWmIydEhadTcyMlBURmw2aUJxOF9GTzR0dTZiNGZfOFlHaVpPZ1A1LXhhOEFtN1J5TEVNNWtMcGpyNkMzSl8xRnZsaTF1WTZrOUZmb0cxVURjSGFLS2dIYTQyZEJtTm90bEYxVWxNNXVPdTVjaVhYbXhxT3JsVDM5VjZMVFZKSE1tZnM9
Connector_AiAnthropic_API_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpENmFBWG16STFQUVZxNzZZRzRLYTA4X3lRanF1VkF4cU45OExNMzlsQmdISGFxTUxud1dXODBKcFhMVG9KNjdWVnlTTFFROVc3NDlsdlNHLUJXeG41NDBHaXhHR0VHVWl5UW9RNkVWbmlhakRKVW5pM0R4VHk0LUw0TV9LdkljNHdBLXJua21NQkl2b3l4UkVkMGN1YjBrMmJEeWtMay1jbmxrYWJNbUV0aktCXzU1djR2d2RSQXZORTNwcG92ZUVvVGMtQzQzTTVncEZTRGRtZUFIZWQ0dz09
Connector_AiPerplexity_API_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd5ZmdDZ3hrSElrMnQzNFAtel9wX191VjVzN2g1LWZoa0V1YklubEdmMEJDdEZiR1RWeVZrM3V3enBHX3p6WUtTS0kwYkFyVEF0Nm8zX05CelVQcFJUc0lwVW5iNFczc1p1WWJ2WFBmd0lpLUxxWndEeUh0b2hGUHVpN19vb19nMTBnV1A1VmNpWERVX05lQ29VS20wTjZ3PT0=
Connector_AiTavily_API_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEQTdnUHMwd2pIaXNtMmtCTFREd0pyQXRKb1F5eGtHSnkyOGZiUnlBOFc0b3Vzcndrc3ViRm1nMDJIOEZKYWxqdWNkZGh5N0Z4R0JlQmxXSG5pVnJUR2VYckZhMWNMZ1FNeXJ3enJLVlpiblhOZTNleUg3ZzZyUzRZanFSeDlVMkI=
Connector_AiPrivateLlm_API_SECRET = DEV_ENC:Z0FBQUFBQnBudkpGRHM5eFdUVmVZU1R1cHBwN1RlMUx4T0NlLTJLUFFVX3J2OElDWFpuZmJHVmp4Z3BNNWMwZUVVZUd2TFhRSjVmVkVlcFlVRWtybXh0ZHloZ01ZcnVvX195YjdlWVdEcjZSWFFTTlNBWUlaTlNoLWhqVFBIb0thVlBiaWhjYjFQOFY=
Connector_AiMistral_API_SECRET = DEV_ENC:Z0FBQUFBQnBudkpGeEQxYUIxOHhia0JlQWpWQ2dWQWZzY3l6SWwyUnJoR1hRQWloX2lxb2lGNkc4UnA4U2tWNjJaYzB1d1hvNG9fWUp1N3V4OW9FMGhaWVhjSlVwWEc1X2loVDBSZDEtdHdfcTA5QkcxQTR4OHc4RkRzclJrU2d1RFZpNDJkRDRURlE=
Service_MSFT_TENANT_ID = common
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETk5FWWM3Q0JKMzhIYTlyMkhuNjA4NlF4dk82U2NScHhTVGY3UG83NkhfX3RrcWVtWWcyLXRjU1dTT21zWEl6YWRMMUFndXpsUnJOeHh3QThsNDZKRXROTzdXRUdsT0JZajZJNVlfb0gtMXkwWm9DOERPVnpjU0pyUEZfOGJsUnprT3ltMVVhalUyUm9hMUFtZEtHUnJqOGZ4dEZjZm5SWVVTckVCWnY1UkdVSHVmUlgwbnAyc0xDQW84R3ViSko5OHVCVWZRUVNiaG1pVFB6X3EwS0FPd2dUYjhiSmRjcXh2WEZiXzI4SFZqT21tbDduUWRyVWdFZXpmcVM5ZDR0VWtzZnF5UER6cGwwS2JlLV9CSTZ0Z0IyQ1h0YW9TcmhRTXZEckp4bWhmTkt6UTNYMk4zVkpnbUJmaDIxZnoyR2dWTEYwTUFEV0w2eUdUUGpoZk9XRkt4RVF1Z1NPdUpBeTcyWV9PY1Ffd2s0ZEdVekxGekhoeEl4TmNqaXYtbUJuSVdycFducERWdWtZajZnX011Q2w4eE9VMTBqQ1ZxRmdScWhXY1E3WWhzX1JZcHhxam9FbDVPN3Q1MWtrMUZuTUg3LVFQVHp1T1hpQWNDMzEzekVJWk9ybl91YUVjSkFob1VaMi1ONEtuMnRSOEg1S3QybUMwbVZDejItajBLTjM2Zy1hNzZQMW5LLVVDVGdFWm5BZUxNeEFnUkZzU3dxV0lCUlc0LWo4b05GczVpOGZSV2ZxbFBwUml6OU5tYjdnTks3Y3hrVEZVTHlmc1NPdFh4WE5pWldEZklOQUxBbjBpMTlkX3FFQVJ6c2NSZGdzTThycE92VW82enZKamhiRGFnU25aZGlHZHhZd2lUUmhuTVptNjhoWVlJQkxIOEkzbzJNMjZCZFJyM25tdXBnQ2ZWaHV3b2p6UWJpdk9xUEhBc1dyTlNmeF9wbm5yYUhHV01UZnVXWDFlNzBkdXlWUWhvcmJpSmljbmE3LUpUZEg4VzRwZ2JVSjdYUm1sODViQXVxUzdGTmZFbVpiN2V1YW5XV3U4b2VRWmxldGVGVHZsSldoekhVLU9wZ2V0cGZIYkNqM2pXVGctQVAyUm4xTHhpd1VVLXFhcnVEV21Rby1hbTlqTl84TjVveHdYTExUVkhHQ0ltaTB2WXJnY1NQVE5PbWg3ejgySElYc1JSTlQ3NDlFUWR6STZVUjVqaXFRN200NF9LY1ljQ0R2UldlWUtKY1NQVnJ4QXRyYTBGSWVuenhyM0Z0cWtndTd1eG8xRzY5a2dNZ1hkQm5MV3BHVzA2N1QwUkd6WlRGYTZQOUhnVWQ2S0Y5U0s1dXFNVXh5Q2pLWVUxSUQ2MlR1ak52NmRIZ2hlYTk1SGZGWS1RV3hWVU9rR3d1Rk9MLS11REZXbzhqMHpsSm1HYW1jMUNLT29YOHZsRWNaLTVvOFpmT3l3MHVwaERTT0dNLWFjcGRYZ25qT2szTkVFUnRFR3JWYS1aNXFIRnMyalozTlQzNFF2NXJLVHVPVF9zdTF6ZjlkbzJ4RFc2ZENmNFFxZDZzTzhfMUl0bW96V0lPZkh1dXFYZlEteFBlSG84Si1FNS1TTi1OMkFnX2pOYW8xY3MxMVJnVC02MDUyaXZfMEVHWDQtVlRpcENmV0h3V0dCWEFRS2prQXdNRlQ5dnRFVHU0Q1dNTmh0SlBCaU55bFMydWM1TTFFLW96ODBnV3dNZHFZTWZhRURYSHlrdzF3RlRuWDBoQUhSOUJWemtRM3pxcDJFbGJoaTJ3ZktRTlJxbXltaHBoZXVJVDlxS3cxNWo2c0ZBV0NzaUstRWdsMW1xLXFkanZGYUFiU0tSLXFQa0tkcDFoMV9kak41ZjQ0R214UmtOR1ZBanRuemY3Mmw1SkZ5aDZodGIzT3N2aV85MW9kcld6c0g0ZDgtTWo3b3Y3VjJCRnR2U2tMVm9rUXNVRnVHbzZXVTZ6RmI2RkNmajBfMWVnODVFbnpkT0oyci15czJHU0p1cUowTGZJMzVnd3hIRjQyTVhKOGRkcFRKdVpyQ3Yzd01Jb1lSajFmV0paeEV0cjk1SmpmdWpDVFJMUmMtUFctOGhaTmlKQXNRVlVUNlhJemxudHZCR056SVlBb3NOTEYxRTRLaFlVd2d3TWtxVlB6ZEtQLTkxOGMyY3N0a2pYRFUweDBNaGhja2xSSklPOUZla1dKTWRNbG8tUGdSNEV5cW90OWlOZFlIUExBd3U2b2hyS1owbXVMM3p0Qm41cUtzWUxYNzB1N3JpUTNBSGdsT0NuamNTb1lIbXR4MG1sakNPVkxBUXRLVE1xX0YxWDhOcERIY1lTQVFqS01CaXZKNllFaXlIR0JsM1pKMmV1OUo3TGI1WkRaVnYxUTl1LTM0SU1qN1V1b0RCT0x0VHNLTmNLZnk1S0MxYnBBcm03WnVua0xqaEhGUzhOU253ZkppRzdudXBSVlMxeFVOSWxtZ1o2RVBSQUhEUEFuQ1hxSVZMME4yWUtaU3VyRGo3RkUyRUNjT0pNcE1BdE1ZRzdXVl8ydUtXZjdMdHdEVW4teHUtTi1HSGliLUxud21TX0NtcGVkRFBHNkZ1WTlNczR4OUJfUVluc1BoV09oWS1scUdsNnB5d1U5M1huX3k4QzAyNldtb2hybktYN2xKZ1NTNWFsaWwzV3pCRVhkaGR5eTNlV1d6ZzFfaFZTT0E4UjRpQ3pKdEZxUlJ6UFZXM3laUndyWEk2NlBXLUpoajVhZzVwQXpWVzUtVjVNZFBwdWdQa3AxZC1KdGdqNnhibjN4dmFYb2cxcEVwc1g5R09zRUdINUZtOE5QRjVUU0dpZy1QVl9odnFtVDNuWFZLSURtMXlSMlhRNTBWSVFJbEdOOWpfVWV0SmdRWDdlUXZZWE8xRUxDN1I0aEN6MHYwNzM1cmpJS0ZpMnBYWkxfb3FsbEV1VnlqWGxqdVJ6SHlwSjAzRlMycTBaQ295NXNnZERpUnJQcjhrUUd3bkI4bDVzRmxQblhkaFJPTTdISnVUQmhET3BOMTM4bjVvUEc2VmZhb2lrR1FyTUl2RWNEeGg0U0dsNnV6eU5zOUxiNDY5SXBxR0hBS00wOTgyWTFnWkQyaEtLVUloT3ZxZGh0RWVGRmJzenFsaUtfZENQM0JzdkVVeTdXR3hUSmJST1NBMUI1NkVFWncwNW5JZVVLX1p1RXdqVnFfQWpvQ08yQjZhN1NkTkpTSnUxOVRXZXE0WFEtZWxhZW1NNXYtQ2sya0VGLURmS01lMkctNVY3c2ZhN0ZGRFgwWHlabTFkeS1hcUZ1dDZ3cnpPQ3hha2IzVE11M0pqbklmU0diczBqTFBNZC1QZGp6VzNTSnJVSjJoWkJUQjVORG4tYUJmMEJtSUNUdVpEaGt6OTM3TjFOdVhXUHItZjRtZ25nU3NhZC1sVTVXNTRDTmxZbnlfeHNsdkpuMXhUYnE1MnpVQ0ZOclRWM1M4eHdXTzRXbFRZZVQtTS1iRVdXVWZMSGotcWg3MUxUYTFnSEEtanBCRHlZRUNIdGdpUFhsYjdYUndCZnRITzhMZVJ1dHFoVlVNb0duVjlxd0U4OGRuQVV3MG90R0hiYW5MWkxWVklzbWFRNzBfSUNrdzc5bVdtTXg0dExEYnRCaDI3c1I4TWFwLXZKR0wxSjRZYjZIV3ZqZjNqTWhFT0RGSDVMc1A1UzY2bDBiMGFSUy1fNVRQRzRJWDVydUpqb1ZfSHNVbldVeUN2YlAxSW5WVDdxVzJ1WHpLeUdmb0xWMDNHN05oQzY3YnhvUUdhS2xaOHNidkVvbTZtSHFlblhOYmwyR3NQdVJDRUdxREhWdF9ZcXhwUWxHc2hyLW5vUGhIUVhJNUNhY0hFU0ptVnI0TFVhZDE1TFBBUEstSkRoZWJ5MHJhUmZrR1ZrRlFtRGpxS1pOMmFMQjBsdjluY3FiYUU4eGJVVXlZVEpuNWdHVVhJMGtwaTdZR2NDbXd2eHpOQ09SeTV6N1BaVUpsR1pQVDBZcElJUUt6VnVpQmxSYnE4Y1BCWV9IRWdVV0p3enBGVHItdnBGN3NyNWFBWmkySnByWThsbDliSlExQmp3LVlBaDIyZXp6UnR6cU9rTzJmTDBlSVpON0tiWllMdm1oME1zTFl2S2ZYYllhQlY2VHNZRGtHUDY4U1lIVExLZTU4VzZxSTZrZHl1ZTBDc0g4SjI4WGYyZHV1bm9wQ3R2Z09ld1ZmUkN5alJGeHZKSHl1bWhQVXpNMzdjblpLcUhfSm02Qlh5S1FVN3lIcHl0NnlRPT0=
# Feature SyncDelta JIRA configuration
Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEbm0yRUJ6VUJKbUwyRW5kMnRaNW4wM2YxMkJUTXVXZUdmdVRCaUZIVHU2TTV2RWZLRmUtZkcwZE4yRUNlNDQ0aUJWYjNfdVg5YjV5c2JwMHhoUUYxZWdkeS11bXR0eGxRLWRVaVU3cUVQZWJlNDRtY1lWUDdqeDVFSlpXS0VFX21WajlRS3lHQjc0bS11akkybWV3QUFlR2hNWUNYLUdiRjZuN2dQODdDSExXWG1Dd2ZGclI2aUhlSWhETVZuY3hYdnhkb2c2LU1JTFBvWFpTNmZtMkNVOTZTejJwbDI2eGE0OS1xUlIwQnlCSmFxRFNCeVJNVzlOMDhTR1VUamx4RDRyV3p6Tk9qVHBrWWdySUM3TVRaYjd3N0JHMFhpdzFhZTNDLTFkRVQ2RVE4U19COXRhRWtNc0NVOHRqUS1CRDFpZ19xQmtFLU9YSDU3TXBZQXpVcld3PT0=
# Teamsbot Browser Bot Service
# For local testing: run the bot locally with `npm run dev` in service-teams-browser-bot
# The bot will connect back to localhost:8000 via WebSocket
TEAMSBOT_BROWSER_BOT_URL = https://cae-poweron-shared.redwater-53d21339.switzerlandnorth.azurecontainerapps.io
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = True
APP_DEBUG_CHAT_WORKFLOW_DIR = D:/Athi/Local/Web/poweron/local/debug
# Manadate Pre-Processing Servers
PREPROCESS_ALTHAUS_CHAT_SECRET = DEV_ENC:Z0FBQUFBQnBudkpGbEphQ3ZUMlFMQ2EwSGpoSE9NNzRJNTJtaGk1N0RGakdIYnVVeVFHZmF5OXB3QTVWLVNaZk9wNkhfQkZWRnVwRGRxem9iRzJIWXdpX1NIN2FwSExfT3c9PQ==
# Preprocessor API Configuration
PP_QUERY_API_KEY=ouho02j0rj2oijroi3rj2oijro23jr0990
PP_QUERY_BASE_URL=https://poweron-althaus-preprocess-prod-e3fegaatc7faency.switzerlandnorth-01.azurewebsites.net/api/v1/dataquery/query
# Azure Communication Services Email Configuration
MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt
MESSAGING_ACS_SENDER_EMAIL = DoNotReply@poweron.swiss
# Zurich WFS Parcels (dynamic map layer). Default: Stadt Zürich OGD. Override for full canton if wfs.zh.ch resolves.
# Connector_ZhWfsParcels_WFS_URL = https://wfs.zh.ch/av
# Connector_ZhWfsParcels_TYPENAMES = av_li_liegenschaften_a

91
env_int.env Normal file
View file

@ -0,0 +1,91 @@
# Integration Environment Configuration
# System Configuration
APP_ENV_TYPE = int
APP_ENV_LABEL = Integration Instance
APP_API_URL = https://gateway-int.poweron-center.net
APP_KEY_SYSVAR = CONFIG_KEY
APP_INIT_PASS_ADMIN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjWm41MWZ4TUZGaVlrX3pWZWNwakJsY3Facm0wLVZDd1VKeTFoZEVZQnItcEdUUnVJS1NXeDBpM2xKbGRsYmxOSmRhc29PZjJSU2txQjdLbUVrTTE1NEJjUXBHbV9NOVJWZUR3QlJkQnJvTEU9
APP_INIT_PASS_EVENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjdmtrakgxa0djekZVNGtTZV8wM2I5UUpCZllveVBMWXROYk5yS3BiV3JEelJSM09VYTRONHpnY3VtMGxDRk5JTEZSRFhtcDZ0RVRmZ1RicTFhb3c5dVZRQ1o4SmlkLVpPTW5MMTU2eTQ0Vkk9
# PostgreSQL DB Host
DB_HOST=gateway-int-server.postgres.database.azure.com
DB_USER=heeshkdlby
DB_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjczYzOUtTa21MMGJVTUQ5UmFfdWc3YlhCbWZOeXFaNEE1QzdJV3BLVjhnalBkLVVCMm5BZzdxdlFXQXc2RHYzLWtPSFZkZE1iWG9rQ1NkVWlpRnF5TURVbnl1cm9iYXlSMGYxd1BGYVc0VDA9
DB_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNUctb2RwU25iR3ZnanBOdHZhWUtIajZ1RnZzTEp4aDR0MktWRjNoeVBrY1Npd1R0VE9YVHp3M2w1cXRzbUxNaU82QUJvaDNFeVQyN05KblRWblBvbWtoT0VXbkNBbDQ5OHhwSUFnaDZGRG10Vmgtdm1YUkRsYUhFMzRVZURmSFlDTFIzVWg4MXNueDZyMGc5aVpFdWRxY3dkTExGM093ZTVUZVl5LUhGWnlRPQ==
APP_TOKEN_EXPIRY=300
# CORS Configuration
APP_ALLOWED_ORIGINS=http://localhost:8080,https://playground.poweron-center.net,https://playground-int.poweron-center.net,http://localhost:5176,https://nyla.poweron-center.net, https://nyla-int.poweron-center.net
# Logging configuration
APP_LOGGING_LOG_LEVEL = DEBUG
APP_LOGGING_LOG_DIR = /home/site/wwwroot/
APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s
APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S
APP_LOGGING_CONSOLE_ENABLED = True
APP_LOGGING_FILE_ENABLED = True
APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5
# OAuth: Auth app (login/JWT) vs Data app (Graph / Google APIs)
Service_MSFT_AUTH_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_AUTH_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm83T29rMDZvcV9qTG5xb1FzUkdqS1llbzRxSEJXbmpONFFtcUtfZXdtZjQybmJSMjBjMEpnRVhiOGRuczZvVFBFdVVTQV80SG9PSnRQTEpLdVViNm5wc2E5aGRLWjZ4TGF1QjVkNmdRSzBpNWNkYXVublFYclVEdEM5TVBBZWVVMW5RVWk=
Service_MSFT_AUTH_REDIRECT_URI = https://gateway-int.poweron-center.net/api/msft/auth/login/callback
Service_MSFT_DATA_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm83T29rMDZvcV9qTG5xb1FzUkdqS1llbzRxSEJXbmpONFFtcUtfZXdtZjQybmJSMjBjMEpnRVhiOGRuczZvVFBFdVVTQV80SG9PSnRQTEpLdVViNm5wc2E5aGRLWjZ4TGF1QjVkNmdRSzBpNWNkYXVublFYclVEdEM5TVBBZWVVMW5RVWk=
Service_MSFT_DATA_REDIRECT_URI = https://gateway-int.poweron-center.net/api/msft/auth/connect/callback
Service_GOOGLE_AUTH_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_AUTH_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNThGeVRNd3hacThtRnE0bzlDa0JPUWQyaEd6QjlFckdsMGZjRlRfUks2bXV3aDdVRTF3LVRlZVY5WjVzSXV4ZGNnX002RDl3dkNYdGFzZkxVUW01My1wTHRCanVCLUozZEx4TlduQlB5MnpvNTR2SGlvbFl1YkhzTEtsSi1SOEo=
Service_GOOGLE_AUTH_REDIRECT_URI = https://gateway-int.poweron-center.net/api/google/auth/login/callback
Service_GOOGLE_DATA_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNThGeVRNd3hacThtRnE0bzlDa0JPUWQyaEd6QjlFckdsMGZjRlRfUks2bXV3aDdVRTF3LVRlZVY5WjVzSXV4ZGNnX002RDl3dkNYdGFzZkxVUW01My1wTHRCanVCLUozZEx4TlduQlB5MnpvNTR2SGlvbFl1YkhzTEtsSi1SOEo=
Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-int.poweron-center.net/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5SE1uVURMNVE3NkM4cHBKa2R2TjBnLWdpSXI5dHpKWGExZVFiUF95TFNnZ1NwLWFLdmh6eWFZTHVHYTBzU2FGRUpLYkVyM1NvZjZkWDZHN21qUER5ZVNOaGpCc3NrUGd3VnFTclF3OW1nUlVuWXQ1UVhDLVpyb1BwRExOeFpDeVhtbEhDVnd4TVdpbzNBNk5QQWFPdjdza0xBWGxFY1E3WFpCSUlNa1l4RDlBPQ==
Service_CLICKUP_OAUTH_REDIRECT_URI = http://localhost:8000/api/clickup/auth/connect/callback
# Stripe Billing (both end with _SECRET for encryption script)
STRIPE_SECRET_KEY_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5ekdBaGNGVUlOQUpncTlzLWlTV0V5OWZzQkpDczhCUGw4U1JpTHZ0d3pfYlFNWElLRlNiNlNsaDRYTGZUTkg2OUFrTW1GZXpOUjBVbmRQWjN6ekhHd2ZSQ195OHlaeWh1TmxrUm10V2R3YmdncmFLbFMzVjdqcWJMSUJPR2xuSEozclNoZG1rZVBTaWg3OFQ1Qzdxb0wyQ2RKazc2dG1aZXBUTXlvbDZqLS1KOVI5M3BGc3NQZkZRbnFpRjIwWmh2ZHlVNlpxZVo2dWNmMjQ5eW02QmtzUT09
STRIPE_WEBHOOK_SECRET = whsec_2agCQEbDPSOn2C40EJcwoPCqlvaPLF7M
STRIPE_API_VERSION = 2026-01-28.clover
# AI configuration
Connector_AiOpenai_API_SECRET = INT_ENC:Z0FBQUFBQnBaSnM4MENkQ2xJVmE5WFZKUkh2SHJFby1YVXN3ZmVxRkptS3ZWRmlwdU93ZEJjSjlMV2NGbU5mS3NCdmFfcmFYTEJNZXFIQ3ozTWE4ZC1pemlQNk9wbjU1d3BPS0ZCTTZfOF8yWmVXMWx0TU1DamlJLVFhSTJXclZsY3hMVWlPcXVqQWtMdER4T252NHZUWEhUOTdIN1VGR3ltazEweXFqQ0lvb0hYWmxQQnpxb0JwcFNhRDNGWXdoRTVJWm9FalZpTUF5b1RqZlRaYnVKYkp0NWR5Vko1WWJ0Wmg2VWJzYXZ0Z3Q4UkpsTldDX2dsekhKMmM4YjRoa2RwemMwYVQwM2cyMFlvaU5mOTVTWGlROU8xY2ZVRXlxZzJqWkxURWlGZGI2STZNb0NpdEtWUnM9
Connector_AiAnthropic_API_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjT1ZlRWVJdVZMT3ljSFJDcFdxRFBRVkZhS204NnN5RDBlQ0tpenhTM0FFVktuWW9mWHNwRWx2dHB0eDBSZ0JFQnZKWlp6c01pVGREWHd1eGpERnU0Q2xhaks1clQ1ZXVsdnd2ZzhpNXNQS1BhY3FjSkdkVEhHalNaRGR4emhpakZncnpDQUVxOHVXQzVUWmtQc0FsYmFwTF9TSG5FOUFtWk5Ick1NcHFvY2s1T1c2WXlRUFFJZnh6TWhuaVpMYmppcDR0QUx0a0R6RXlwbGRYb1R4dzJkUT09
Connector_AiPerplexity_API_SECRET = INT_ENC:Z0FBQUFBQnB5dkd6UkhtU3lhYmZMSlo0bklQZ2s3UTFBSkprZTNwWkg5Q2lVa0wtenhxWXpva21xVDVMRjdKSmhpTmxWS05IUTRoRHdCbktSRVVjcVFnY1RfV0N2S2dyV0dTMlhxQlRFVm41RkFTWVQzQThuVkZwdlNuVC05QlVRVXB6Qjk3akNpYmY1MFR6R1ByMzlIMllRZlRRYVVRN2ZBPT0=
Connector_AiTavily_API_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkdkJMTDY0akhXNzZDWHVYSEt1cDZoOWEzSktneHZEV2JndTNmWlNSMV9KbFNIZmQzeVlrNE5qUEIwcUlBSGM1a0hOZ3J6djIyOVhnZzI3M1dIUkdicl9FVXF3RGktMmlEYmhnaHJfWTdGUkktSXVUSGdQMC1vSEV6VE8zR2F1SVk=
Connector_AiPrivateLlm_API_SECRET = INT_ENC:Z0FBQUFBQnBudkpGSjZ1NWh0aWc1R3Z4MHNaeS1HamtUbndhcUZFZDlqUDhjSmg5eHFfdlVkU0RsVkJ2UVRaMWs3aWhraG5jSlc0YkxNWHVmR2JoSW5ENFFCdkJBM0VienlKSnhzNnBKbTJOUTFKczRfWlQ3bWpmUkRTT1I1OGNUSTlQdExacGRpeXg=
Connector_AiMistral_API_SECRET = INT_ENC:Z0FBQUFBQnBudkpGZTNtZ1E4TWIxSEU1OUlreUpxZkJIR0Vxcm9xRHRUbnBxbTQ1cXlkbnltWkJVdTdMYWZ4c3Fsam42TERWUTVhNzZFMU9xVjdyRGFCYml6bmZsZFd2YmJzemlrSWN6Q3o3X0NXX2xXNUQteTNONHdKYzJ5YVpLLWdhU2JhSTJQZnI=
Service_MSFT_TENANT_ID = common
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkNmVXZ1pWcHcydTF2MXF0ZGJoWHBydF85bTczTktiaEJ3Wk1vMW1mZVhDSG1yd0ZxR2ZuSGJTX0N3MWptWXFJTkNTWjh1SUVVTXI4UDVzcGdLMkU5SHJ2TUpkRlRoRWdnSldtYjNTQkh4UDJHY2xmdTdZQ1ZiMTZZcGZxS3RzaHdjV3dtVkZUcEpJcWx0b2xuQVR6ZmpoVFZPY1hNMTV2SnhDaC1IZEh4UUpLTy1ILXA4RG1zamJTbUJ4X0t2M2NkdzJPbEJxSmFpRzV3WC0wZThoVzlxcmpHZ3ZkLVlVY3REZk1vV19WQ05BOWN6cnJ4MWNYYnNiQ0FQSUVnUlpfM3BhMnlsVlZUOG5wM3pzM1lSN1UzWlZKUXRLczlHbjI1LTFvSUJ4SlVXMy1BNk43bE5Hb0RfTTVlWk9oZnFIaVg0SW5pbm9EcXRTTzU1RFlYY3dTcnpKWWNyNjN5T1BGZ0FmX253cEFncmhvZVRuM05KYzhkOEhFMFJsc2NBSEwzZVZ1R0JMOGxsekVwUE55alZaRXFrdzNWWVNGWXNmbnhKeWhQSFo2VXBTUlRPeHdvdVdncEFuOWgydEtsSUFneUN6cGVaTnBSdjNCdVJseGJFdmlMc203UFhLVlYyTENkaGg2dVN6Z2xwT1ZmTmN5bVZGUkM3ZWcyVkt2ckFUVVd3WFFwYnJjNVRobEh2SkVJbXRwUUpEOFJKQ1NUc0Q4NHNqUFhPSDh5cTV6MEcwSDEwRUJCQ2JiTTJlOE5nd3pMMkJaQ1dVYjMwZVVWWnlETmp2dkZ3aXEtQ29WNkxZTFkzYUkxdTlQUU1OTnhWWU12YU9MVnJQa1d2ZjRtUlhneTNubEMxTmp1eUNPOThSMlB3Y1F0T2tCdFNsNFlKalZPV25yR2QycVBUb096RmZ1V0FTaGsxLV9FWDBmenBIOXpMdGpLcUc0TWRoY2hlMFhYTzlET1ZRekw0ZHNwUVBQdVJBX2h6Q2ZzWVZJWTNybTJiekp3WmhmWF9SUFBXQzlqUjctcVlHWWVMZWVQallzR0JGTVF0WmtnWlg1aTM1bFprNVExZXY5dnNvWF93UjhwbkJ3RzNXaVJ2d2RRU3JJVlBvaVh4eTlBRUtqWkJia3dJQVVBV2Nqdm9FUTRUVW1TaHp2ZUwxT0N2ZndxQ2Nka1RYWXF0LWxIWFE0dTFQcVhncFFPM0hFdUUtYlFnemx3WkF4bjA1aDFULUdrZlVZbEJtRGRCdjJyVkdJSXozd0I0dF9zbWhOeHFqRDA4T1NVaWR5cjBwSVgwbllPU294NjZGTnM1bFhIdGpNQUxFOENWd3FCbGpSRFRmRXotQnU0N2lCVEU5RGF6Qi10S2U2NGdadDlrRjZtVE5oZkw5ZWFjXzhCTmxXQzNFTFgxRXVYY3J3YkxnbnlBSm9PY3h4MlM1NVFQbVNDRW5Ld1dvNWMxSmdoTXJuaE1pT2VFeXYwWXBHZ29MZDVlN2lwUUNIeGNCVVdQVi1rRXdJMWFncUlPTXR0MmZVQ1l0d09mZTdzWGFBWUJMUFd3b0RSOU8zeER2UWpNdzAxS0ZJWnB5S3FJdU9wUDJnTTNwMWw3VFVqVXQ3ZGZnU1RkUktkc0NhUHJ0SGFxZ0lVWDEzYjNtU2JfMGNWM1Y0dHlCTzNESEdENC1jUWF5MVppRzR1QlBNSUJySjFfRi1ENHEwcmJ4S3hQUFpXVHA0TG9DZWdoUlo5WnNSM1lCZm1KbEs2ak1yUUU4Wk9JcVJGUkJwc0NvUkMyTjhoTWxtZmVQeDREZVRKZkhYN2duLVNTeGZzdFdBVnhEandJSXB5QjM0azF0ckI3Tk1wSzFhNGVOUVRrNjU0cG9JQ29pN09xOFkwR1lMTlktaGp4TktxdTVtTnNEcldsV2pEZm5nQWpJc2hxY0hjQnVSWUR5VVdaUXBHWUloTzFZUC1oNzJ4UjZ1dnpLcDJxWEZtQlNIMWkzZ0hXWXdKeC1iLXdZWVJhcU04VFlpMU5pd2ZIdTdCdkVWVFVBdmJuRk16bEFFQTh4alBrcTV2RzliT2hGdTVPOXlRMjFuZktiRTZIamQ1VFVqS0hRTXhxcU1mdkgyQ1NjQmZfcjl4c3NJd0RIeDVMZUFBbHJqdEJxWWl3aWdGUEQxR3ZnMkNGdVB4RUxkZi1xOVlFQXh1NjRfbkFEaEJ5TVZlUGFrWVhSTVRPeGxqNlJDTHNsRWRrei1pYjhnUmZrb3BvWkQ2QXBzYjFHNXZoWU1LSExhLWtlYlJTZlJmYUM5Y1Rhb1pkMVYyWTByM3NTS0VXMG1ybm1BTVN2QXRYaXZqX2dKSkZrajZSS2cyVlNOQnd5Y29zMlVyaWlNbTJEb3FuUFFtbWNTNVpZTktUenFZSl91cVFXZjRkQUZyYmtPczU2S1RKQ19ONGFOTHlwX2hOOEE1UHZEVjhnT0xxRjMxTEE4SHhRbmlmTkZwVXJBdlJDbU5oZS05SzI4QVhEWDZaN2ZiSlFwUGRXSnB5TE9MZV9ia3pYcmZVa1dicG5FMHRXUFZXMWJQVDAwOEdDQzJmZEl0ZDhUOEFpZXZWWXl5Q2xwSmFienNCMldlb2NKb2ZRYV9KbUdHRzNUcjU1VUFhMzk1a2J6dDVuNTl6NTdpM0hGa3k0UWVtbF9pdDVsQVp2cndDLUU5dnNYOF9CLS0ySXhBSFdCSnpqV010bllBb3U0cEZZYVF5R2tSNFM5NlRhdS1fb1NqbDBKMkw0V2N0VEZhNExtQlR3ckZ3cVlCeHVXdXJ6X0s4cEtsaG5rVUxCN2RRbHQxTmcyVFBqYUxyOHJzeFBXVUJaRHpXbUoxdHZzMFBzQk1UTUFvX1pGNFNMNDFvZWdTdEUtMUNKMXNIeVlvQk1CeEdpZVdmN0tsSDVZZHJXSGt5c2o2MHdwSTZIMVBhRzM1eU43Q2FtcVNidExxczNJeUx5U2RuUG5EeHpCTlg2SV9WNk1ET3BRNXFuc0pNWlVvZUYtY21oRGtJSmwxQ09QbHBUV3BuS3B5NE9RVkhfellqZjJUQ0diSV94QlhQWmdaaC1TRWxsMUVWSXB0aE1McFZDZDNwQUVKZ2t5cXRTXzlRZVJwN0pZSnJSV21XMlh0TzFRVEl0c2I4QjBxOGRCYkNxek04a011X1lrb2poQ3h2LUhKTGJiUlhneHp5QWFBcE5nMElkNTVzM3JGOWtUQ19wNVBTaVVHUHFDNFJnNXJaWDNBSkMwbi1WbTdtSnFySkhNQl9ZQjZrR2xDcXhTRExhMmNHcGlyWjR3ZU9SSjRZd1l4ZjVPeHNiYk53SW5SYnZPTzNkd1lnZmFseV9tQ3BxM3lNYVBHT0J0elJnMTByZ3VHemxta0tVQzZZRllmQ2VLZ1ZCNDhUUTc3LWNCZXBMekFwWW1fQkQ1NktzNGFMYUdYTU0xbXprY1FONUNlUHNMY3h2NFJMMmhNa3VNdzF4TVFWQk9odnJUMjFJMVd3Z2N6Sms5aEM2SWlWZFViZ0JWTEpUWWM5NmIzOS1oQmRqdkt1NUUycFlVcUxERUZGbnZqTUxIYnJmMDBHZDEzbnJsWEEzSUo3UmNPUDg1dnRUU1FzcWtjTWZwUG9zM0JTY3RqMDdST2UxcXFTM0d0bGkwdFhnMk5LaUlxNWx3V1pLaVlLUFJXZzBzVl9Ia1V1OHdYUEFWOU50UndycGtCdzM0Q0NQamp2VTNqbFBLaGhsbUk5dUI5MjU5OHVySk1oY0drUWtXUloyVVRvOWJmbUVYRzFVeWNQczh2NXJCeVppRlZiWDNJaDhOSmRmX2lURTNVS3NXQXFZT1QtUmdvMWJoVWYxU3lqUUJhbzEyX3I3TXhwbm9wc1FoQ1ZUTlNBRjMyQTBTY2tzbHZ3RFUtTjVxQ0o1QXRTVks2WENwMGZCRGstNU1jN3FhUFJCQThyaFhhMVRsbnlSRXNGRmt3Yk01X21ldmV3bTItWm1JaGpZQWZROEFtT1d1UUtPQlhYVVFqT2NxLUxQenJHX3JfMEdscDRiMXcyZ1ZmU3NFMzVoelZJaDlvT0ZoRGQ2bmtlM0M5ZHlCd2ZMbnRZRkZUWHVBUEx4czNfTmtMckh5eXZrZFBzOEItOGRYOEhsMzBhZ0xlOWFjZzgteVBsdnpPT1pYdUxnbFNXYnhKaVB6QUxVdUJCOFpvU2x2c1FHZV94MDBOVWJhYkxISkswc0U5UmdPWFJLXzZNYklHTjN1QzRKaldKdEVHb0pOU284N3c2LXZGMGVleEZ5NGZ6OGV1dm1tM0J0aTQ3VFlNOEJrdEh3PT0=
# Feature SyncDelta JIRA configuration
Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkTUNsWm4wX0p6eXFDZmJ4dFdHNEs1MV9MUzdrb3RzeC1jVWVYZ0REWHRyZkFiaGZLcUQtTXFBZzZkNzRmQ0gxbEhGbUNlVVFfR1JEQTc0aldkZkgyWnBOcjdlUlZxR0tDTEdKRExULXAyUEtsVmNTMkRKU1BJNnFiM0hlMXo4YndMcHlRMExtZDQ3Zm9vNFhMcEZCcHpBPT0=
# Teamsbot Browser Bot Service
TEAMSBOT_BROWSER_BOT_URL = https://cae-poweron-shared.redwater-53d21339.switzerlandnorth.azurecontainerapps.io
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = FALSE
APP_DEBUG_CHAT_WORKFLOW_DIR = ./test-chat
# Manadate Pre-Processing Servers
PREPROCESS_ALTHAUS_CHAT_SECRET = INT_ENC:Z0FBQUFBQnBaSnM4UkNBelhvckxCQUVjZm94N3BZUDcxaEMyckE2dm1lRVhqODhrWU1SUjNXZ3dQZlVJOWhveXFkZXpobW5xT0NneGZ2SkNUblFmYXd0WTBYNTl3UmRnSWc9PQ==
# Preprocessor API Configuration
PP_QUERY_API_KEY=ouho02j0rj2oijroi3rj2oijro23jr0990
PP_QUERY_BASE_URL=https://poweron-althaus-preprocess-prod-e3fegaatc7faency.switzerlandnorth-01.azurewebsites.net/api/v1/dataquery/query
# Azure Communication Services Email Configuration
MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt
MESSAGING_ACS_SENDER_EMAIL = DoNotReply@poweron.swiss

91
env_prod.env Normal file
View file

@ -0,0 +1,91 @@
# Production Environment Configuration
# System Configuration
APP_ENV_TYPE = prod
APP_ENV_LABEL = Production Instance
APP_KEY_SYSVAR = CONFIG_KEY
APP_INIT_PASS_ADMIN_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3UnJRV0sySFlDblpXUlREclREaW1WbUt6bGtQYkdrNkZDOXNOLXFua1hqeFF2RHJnRXJ5VlVGV3hOZm41QjZOMlNTb0duYXNxZi05dXVTc2xDVkx0SVBFLUhncVo5T0VUZHE0UTZLWWw3ck09
APP_INIT_PASS_EVENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3QVpIY19DQVZSSzJmc2F0VEZvQlU1cHBhTEgxdHdnR3g4eW01aTEzYTUxc1gxTDR1RVVpSHRXYjV6N1BLZUdCUGlfOW1qdy0xSHFVRkNBcGZvaGlSSkZycXRuUllaWnpyVGRoeFg1dGEyNUk9
APP_API_URL = https://gateway-prod.poweron-center.net
# PostgreSQL DB Host
DB_HOST=gateway-prod-server.postgres.database.azure.com
DB_USER=gzxxmcrdhn
DB_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3Y1JScGxjZG9TdUkwaHRzSHZhRHpNcDV3N1U2TnIwZ21PRG5TWFFfR1k0N3BiRk5WelVadjlnXzVSTDZ6NXFQNFpqbnJ1R3dNVkJocm1zVEgtSk0xaDRiR19zNDBEbVIzSk51ekNlQ0Z3b0U9
DB_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3elhfV0Rnd2pQRjlMdkVwX1FnSmRhSzNZUlV5SVpaWXBNX1hpa2xPZGdMSWpnN2ZINHQxeGZnNHJweU5pZjlyYlY5Qm9zOUZEbl9wUEgtZHZXd1NhR19JSG9kbFU4MnFGQnllbFhRQVphRGQyNHlFVWR5VHQyUUpqN0stUmRuY2QyTi1oalczRHpLTEJqWURjZWs4YjZvT2U5YnFqcXEwdEpxV05fX05QMmtrPQ==
APP_TOKEN_EXPIRY=300
# CORS Configuration
APP_ALLOWED_ORIGINS=http://localhost:8080,https://playground.poweron-center.net,https://playground-int.poweron-center.net,http://localhost:5176,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
# Logging configuration
APP_LOGGING_LOG_LEVEL = DEBUG
APP_LOGGING_LOG_DIR = /home/site/wwwroot/
APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s
APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S
APP_LOGGING_CONSOLE_ENABLED = True
APP_LOGGING_FILE_ENABLED = True
APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5
# OAuth: Auth app (login/JWT) vs Data app (Graph / Google APIs)
Service_MSFT_AUTH_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_AUTH_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBESkk2T25scFU1T1pNd2FENTFRM3kzcEpSXy1HT0trQkR2Wnl3U3RYbExzRy1YUTkxd3lPZE84U2lhX3FZanp5TjhYRGluLXVjU3hjaWRBUnZLbVhtRDItZ3FxNXJ3MUxicUZTXzJWZVNrR0VKN3ZlNEtET1ppOFk0MzNmbkwyRmROUk4=
Service_MSFT_AUTH_REDIRECT_URI = https://gateway-prod.poweron-center.net/api/msft/auth/login/callback
Service_MSFT_DATA_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBESkk2T25scFU1T1pNd2FENTFRM3kzcEpSXy1HT0trQkR2Wnl3U3RYbExzRy1YUTkxd3lPZE84U2lhX3FZanp5TjhYRGluLXVjU3hjaWRBUnZLbVhtRDItZ3FxNXJ3MUxicUZTXzJWZVNrR0VKN3ZlNEtET1ppOFk0MzNmbkwyRmROUk4=
Service_MSFT_DATA_REDIRECT_URI = https://gateway-prod.poweron-center.net/api/msft/auth/connect/callback
Service_GOOGLE_AUTH_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_AUTH_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3eWFwSEZ4YnRJcjU1OW5kcXZKdkt1Z3gzWDFhVW5Eelh3VnpnNlppcWxweHY5UUQzeDIyVk83cW1XNVE4bllVWnR2MjlSQzFrV1UyUVV6OUt5b3Vqa3QzMUIwNFBqc2FVSXRxTlQ1OHVJZVFibnhBQ2puXzBwSXp5NUZhZjM1d1o=
Service_GOOGLE_AUTH_REDIRECT_URI = https://gateway-prod.poweron-center.net/api/google/auth/login/callback
Service_GOOGLE_DATA_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3eWFwSEZ4YnRJcjU1OW5kcXZKdkt1Z3gzWDFhVW5Eelh3VnpnNlppcWxweHY5UUQzeDIyVk83cW1XNVE4bllVWnR2MjlSQzFrV1UyUVV6OUt5b3Vqa3QzMUIwNFBqc2FVSXRxTlQ1OHVJZVFibnhBQ2puXzBwSXp5NUZhZjM1d1o=
Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-prod.poweron-center.net/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnB5dkd6VGw5WDdhdDRsVENSalhSSUV0OFFxbEx0V1l6aktNV0E5Y18xU3JHLUlqMWVJdmxyajAydVZRaDJkZzJOVXhxRV9ROFRZbWxlRjh4c3NtQnRFMmRtZWpzTWVsdngtWldlNXRKTURHQjJCOEt6alMwQlkwOFYyVVJWNURJUGJIZDIxYVlfNnBrMU54M0Q3TVdVbFZqRkJKTUtqa05wUkV4eGZvbXNsVi1nNVdBPQ==
Service_CLICKUP_OAUTH_REDIRECT_URI = http://localhost:8000/api/clickup/auth/connect/callback
# Stripe Billing (both end with _SECRET for encryption script)
STRIPE_SECRET_KEY_SECRET = PROD_ENC:Z0FBQUFBQnB5dkd6aVA3R3VRS3VHMUgzUEVjYkR4eUZKWFhPUzFTTVlHNnBvT3FienNQaUlBWVpPLXJyVGpGMWk4LXktMXphX0J6ZTVESkJxdjNNa3ZJbF9wX2ppYzdjYlF0cmdVamlEWWJDSmJYYkJseHctTlh4dnNoQWs4SG5haVl2TTNDdXpuaFpqeDBtNkFCbUxMa0RaWG14dmxyOEdILTNrZ2licmNpbXVkN2lFSWoxZW1BODNpV0ZTQ0VaeXRmR1d4RjExMlVFS3MtQU9zZXZlZE1mTmY3OWctUXJHdz09
STRIPE_WEBHOOK_SECRET = PROD_ENC:Z0FBQUFBQnBudkpGNUpTWldsakYydFhFelBrR1lSaWxYT3kyMENOMUljZTJUZHBWcEhhdWVCMzYxZXQ5b3VlTFVRalFiTVdsbGxrdUx0RDFwSEpsOC1sTDJRTEJNQlA3S3ZaQzBtV1h6bWp5VnlMZUgwUlF3cXYxcnljZVE5SWdzLVg3V0syOWRYS08=
STRIPE_API_VERSION = 2026-01-28.clover
# AI configuration
Connector_AiOpenai_API_SECRET = PROD_ENC:Z0FBQUFBQnBaSnM4TWJOVm4xVkx6azRlNDdxN3UxLUdwY2hhdGYxRGp4VFJqYXZIcmkxM1ZyOWV2M0Z4MHdFNkVYQ0ROb1d6LUZFUEdvMHhLMEtXYVBCRzM5TlYyY3ROYWtJRk41cDZxd0tYYi00MjVqMTh4QVcyTXl0bmVocEFHbXQwREpwNi1vODdBNmwzazE5bkpNelE2WXpvblIzWlQwbGdEelI2WXFqT1RibXVHcjNWbVhwYzBOM25XTzNmTDAwUjRvYk4yNjIyZHc5c2RSZzREQUFCdUwyb0ZuOXN1dzI2c2FKdXI4NGxEbk92czZWamJXU3ZSbUlLejZjRklRRk4tLV9aVUFZekI2bTU4OHYxNTUybDg3RVo0ZTh6dXNKRW5GNXVackZvcm9laGI0X3R6V3M9
Connector_AiAnthropic_API_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3TnhYdlhSLW5RbXJyMHFXX0V0bHhuTDlTaFJsRDl2dTdIUTFtVFAwTE8tY3hLbzNSMnVTLXd3RUZualN3MGNzc1kwOTIxVUN2WW1rYi1TendFRVVBSVNqRFVjckEzNExyTGNaUkJLMmozazUwemI1cnhrcEtZVXJrWkdaVFFramp3MWZ6RmY2aGlRMXVEYjM2M3ZlbmxMdnNCRDM1QWR0Wmd6MWVnS1I1c01nV3hRLXg3d2NTZXVfTi1Wdm16UnRyNGsyRTZ0bG9TQ1g1OFB5Z002bmQ3QT09
Connector_AiPerplexity_API_SECRET = PROD_ENC:Z0FBQUFBQnB5dkd6NG5CTm9QOFZRV1BIVC0tV2RKTGtCQWFOUXlpRnhEdjN1U2x3VUdDamtIZV9CQzQ5ZmRmcUh3ZUVUa0NxbGhlenVVdWtaYjdpcnhvUlNFLXZfOWh2dWFZai0xUGU5cWpuYmpnRVRWakh0RVNUUTFyX0w5V0NXVWFrQlZuOTd5TkI0eVRoQ0ZBSm9HYUlYamoyY1FCMmlBPT0=
Connector_AiTavily_API_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3NmItcDh6V0JpcE5Jc0NlUWZqcmllRHB5eDlNZmVnUlNVenhNTm5xWExzbjJqdE1GZ0hTSUYtb2dvdWNhTnlQNmVWQ2NGVDgwZ0MwMWZBMlNKWEhzdlF3TlZzTXhCZWM4Z1Uwb18tSTRoU1JBVTVkSkJHOTJwX291b3dPaVphVFg=
Connector_AiPrivateLlm_API_SECRET = PROD_ENC:Z0FBQUFBQnBudkpGanZ6U3pzZWkwXzVPWGtIQ040XzFrTXc5QWRnazdEeEktaUJ0akJmNnEzbWUzNHczLTJfc2dIdzBDY0FTaXZYcDhxNFdNbTNtbEJTb2VRZ0ZYd05hdlNLR1h6SUFzVml2Z1FLY1BjTl90UWozUGxtak1URnhhZmNDRWFTb0dKVUo=
Connector_AiMistral_API_SECRET = PROD_ENC:Z0FBQUFBQnBudkpGc2tQc2lvMk1YZk01Q1dob1U5cnR0dG03WWE3WkpoOWo0SEpvLU9Rc2lCNDExdy1wZExaN3lpT2FEQkxnaHRmWmZUUUZUUUJmblZreGlpaFpOdnFhbzlEd1RsVVJtX216cmhxTm5BcTN2eUZ2T054cDE5bmlEamJ3NGR6MVpFQnA=
Service_MSFT_TENANT_ID = common
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z4NFQxaF9uN3h1cVB6dnZid1c1R1VfNDlSQ1NHMEVDZWtKanpMQ29CLXc1MXBqRm1hQ0YtWVhaejBMY1ZTOEFEVlpWQ3hrYkFza1E2RDNsYkdMMndNR0VGNTMwVDRGdURJY3hyaVFxVjEtSEYwNHJzeWM3WmlpZW9jU2E3NTgycEV2allqQ3dJRTNyRFAzaDJ6dklKeXpNRkJhYjFzUkptN2dpbkNpMklrcGxuZl9vTkt3T0JvNm1YTXd5UlkwZWptUXdWVFpnV2J4X3J2WUhIUlFkSElFVnlqMnlJRnNHTnlpMWs2R1dZc2ROWjNYZG85cndmd1E5cUZnVmZRYnVjTG43dXFmSWd2bGFfVWFWSmtpWkpndWNlSUNwcnFNU2NqZXFaV0xsY3l3SElLRkVHcHZGZERKV1ltcGhTS0dhTko1VTJLYzNoZjRkSGVEX3dTMWVVTmdDczV5cE1JQUdSbUJGUm11eFhTVjJHbkt0SzB4UG1Dc2xmbnp1Y041Y2RTeWRuWGdmQy1sTGx0MGtnM2VJQ3EyLXViRlNhTU9ybzZkR1N1bXE5SXhlZENWRFpWSGlYOWx4SUQ3UlR0ZEVxQkxNakRUVFRiUmFnbklOalphLUZkRFVVaXBRUk5NZW5PaUZydTFmQkNPSTdTVTNZd0plWXllNVFJdmN4MVcyTGlwMGFtVjBzOGRxR1FjbzhfYW5zdTB0ZEZBTTJhakltazh1dktNMUZsOUItdFdTb1pIaUxySllXNkdlY20zUS0wTnpFNTB2SU5acG1VcXhyaHBmME8takw3RDh5T043T2VGOV92TzNya2pWSlpYVjZDdXlZcjM3a0hPTlhkaW9oQmxqQlpGRFYyTTY4WmZmT3k4Tk1tdXRuSGdTUVpNT2NKenhXb05PdXBfSEdhMTNxNjdpNXlKUUI2YUgydFFPX1VvXzVJb0UxWTU2YVNiNDQ0QndZanhMMHR1cGdHWGhvcEg1QXEtSXZJdTdZUE12ZEVVWkF4QmtsQS1GYnY3SFIxSHlsOGVfcEpGS1A4QUVEQWNEOFZYYlljQ3ByTU03YU16Y0UzUnJQZEprSWNjT1ZXVEtDWi03Y3ZzRVdYUTlabXJISEo5THRHVXVuM0xqbzA4bGVlZVpOMk1QMmptb21tV0pTMlVoOXdWVU95UW1iQmttc2w1RG9mMWwxXzg1T2IxYUVmTUJEZkpUdTFDTzZ3RlBFeUFiX01iRTZNWkNaSG45TkFOM2pzbUJRZ2N0VFpoejJUTG1RODY3TzZpSzVkYUQzaEpfY2pSTkRzU0VpanlkdXVQQmJ2WU5peno4QWNLTDVxZTlhSHI3NnNiM0k0Y3JkQ0xaOU05bGtsQl8zQklvaktWSDZ4aVp2MHlYelJuUDJyTU9CZC1OZjJxNFc1dDcwSUlxaVh1LTMyWWFwU0IwUU9kOUFpMWpnOERtLTh1VmJiNGVwcXBMbU5fMjVZc0hFbmxQT2puSFd1ZGpyTkphLU5sVlBZWWxrWEZrWGJQWmVkN19tZFZfZ1l1V3pSWlA0V0ZxM2lrWnl2NU9WeTdCbDROSmhfeENKTFhMVXk1d195S2JMUFJoRXZjcVo4V2g0MTNKRnZhUE1wRkNPM3FZOGdVazJPeW5PSGpuZnFGTTdJMkRnam5rUlV6NFlqODlIelRYaEN5VjdJNnVwbllNODNCTFRHMWlXbmM1VlRxbXB3Wm9LRjVrQUpjYzRNMThUMWwwSVhBMUlyamtPZnE4R0o4bEdHay1zMjR5RDJkZ1lYRHZaNHVHU2otR3ZpN25LZlEySEU0UmdTNzJGVHNWQXMyb0dVMV9WUE13ODhZWUFaakxGOWZieGNXZkNYRnV5djEyWTZLcmdrajRBLU1rS1Z0VVRkOWlDMU9fMGVmYXFhZXJGMUhpNkdmb2hkbzZ1OWV6VlNmVzNISjVYTFh6SjJNdWR5MWZidE8yVEo2dnRrZXhMRXBPczUwTG13OGhNUVpIQm0zQmRKRnJ0Nl8wNW1Ob0dHRDVpU0NWREV3TkY2SjktdVBkMFU1ZXBmSFpHQ3FHNTRZdTJvaExpZVEtLTU4YTVyeFBpNDdEajZtWUc4c1dBeUJqQ3NIY1NLS0FIMUxGZzZxNFNkOG9ORGNHWWJCVnZuNnJVTEtoQi1mRTZyUl81ZWJJMi1KOGdERzBhNVRZeHRYUUlqY2JvMFlaNHhWMU9pWFFiZjdaLUhkaG15TTBPZVlkS2R5UVdENTI4QVFiY1RJV0ZNZnlpVWxfZmlnN1BXbGdrbjFGUkhzYl9qeHBxVVJacUE4bjZETENHVFpSamh0NVpOM2hMYTZjYzBuS3J0a3hhZGxSM1V5UHd2OTU3ZHY0Yy1xWDBkWUk0Ymp0MWVrS3YzSktKODhQZnY3QTZ1Wm1VZkZJbS1jamdreks1ZlhpQjFOUDFiOHJ2Nm9NcmdTdU5LQXV2RkZWZEFNZnVKUjVwcVY3dDdhQnpmRVJ6SmlvVXpDM0ZiYXh5bGE2X04tTE9qZ3BiTnN3TF9ZaFRxSUpjNjB1dXZBcy1TZHRHTjFjSUR3WUl4cE9VNzB5Rkk4U3Z1SVZYTl9sYXlZVk83UnFrMlVmcnBpam9lRUlCY19DdVJwOXl2TVVDV1pMRFZTZk9MY3Z1eXA0MnhGazc5YllQaWtOeTc4NjlOa2lGY05RRzY1cG9nbGpYelc4c3FicWxWRkg0YzRSamFlQ19zOU14YWJreU9pNDREZVJ3a0REMUxGTzF1XzI1bEF3VXVZRjlBeWFiLXJsOXgza3VZem1WckhWSnVNbDBNcldadU8xQ3RwOTl5NGgtVlR0QklCLWl5WkE4V1FlQTBCOVU1RE9sQlRrYUNZOGdfUmEwbEZvUTFGUEFWVmQ4V1FhOU9VNjZqemRpZm1sUDhZQTJ0YVBRbWZldkF5THV4QXpfdUtNZ0tlcGdSRFM3c0lDOTNQbnBxdmxYYWNpTmI3MW9BMlZIdTQ5RldudHpNQWQ5NDNPLVVTLXVVNzdHZXh4UXpZa3dVa2J4dTFDV1RkYjRnWXU2M3lJekRYWGNMcWU5OVh6U2xZWDh6MmpqcnpiOHlnMjA5S3RFQm1NZjNSM21adkVnTUpSYVhkTzNkNnJCTmljY0x1cl9kMkx3UHhySjZEdHREanZERzNEUTFlTkR0NWlBczAtdmFGTjdZNVpTMlkxV2czYW5RN2lqemg4eUViZDV6RjdKNXdFcUlvcVhoNkJ6eVJkR1pua1hnNzQwOEs2TXJYSlpGcW9qRDU2QjBOWFFtdXBJRkRKbmdZUF9ZSmRPVEtvUjVhLTV1NjdXQjRhS0duaEtJb2FrQnNjUTRvdFMxdkdTNk1NYlFHUFhhYTJ1eUN3WHN4UlJ4UjdrZjY0SzFGYWVFN1k0cGJnc1RjNmFUenR4NHljbVhablZSWHZmUVN3cXRHNjhsX1BSZWEzdTJUZFA0S2pTaU9YMnZIQ1ZPcGhWMFJqZkVEMWRMR1h3SnU0Z2FzZ3VGM3puNzdhVjhaQXNIWHFsbjB0TDVYSFdSNV9rdWhUUUhSZHBGYkJIVDB5SDdlMC13QTVnS0g5Qkg5RGNxSGJlelVndUhPcEQ0QkRKMTJTZUM1OXJhVm0zYjU0OVY2dk9MQVBheklIQXpVNW9Yc0ROVjEzaFZTWmVxYlBWMlNlSzladzJ6TmNuMG5FVVZkN1VZN1pfS2ZHa0lQcE80S24wSnQtVlJVV09OVWJ3M09YMkZpV2ktVF9ENHhKU2dfYUQ2aUVyamk0VHJHQmVfVHU4clpUTFoteW5aSWRPV1M0RDRMTms4NGRoYmJfVE82aUl2X3VieVJOdDhBQmRwdzdnRTVBNzZwaW93dUlZb3ZRYUtOeG9ULWxvNVp5a0haSjdkcUhRb3d6UGIxRUpCVkVYX2d6TkRqQVozUWxkNGFoc1FXYVd2YWNkME9Qclo0bjYxMFRWTy1nbnI5NTBJNzRMMDluUXRKYTFqQUN4d0d5aHVlamN3Tkk3NWJXeXR0TW9BeUg5Vnp4Q2RnZUY3b3AtMDlrNmlrSGR0eGRtbUdUd2lFRWg4MklEeWJHN2wwZEpVSXMxNDNOWjRFS0tPdWxhMmFCckhfRENIY184aEFDZXNrRDl2dHQtQW12UnRuQXJjaDJoTUpiYkNWQUtfRG9GMUZoNWM4UnBYZ29RWWs2NHcyUm5kdTF3Vk1GeFpiRUJLaVZ2UGFjbi1jV3lMV0N2ZDl4VERPN295X01NNG56ZjZkRzZoYUtmY1E5NlVXemx2SnVfb19iSXg0R2M3Mjd1a2JRPT0=
# Feature SyncDelta JIRA configuration
Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z4d3Z4d2x6N1FhUktMU0RKbkxfY2pTQkRzXzJ6UXVEbDNCaFM3UHMtQVFGYzNmYWs4N0lMM1R2SFJuZTVFVmx6MGVEbXc5U3NOTnY1TWN0ZDNaamlHQWloalM3VldmREJNSHQ1TlVkSVFJMTVhQWVGSVRMTGw4UTBqNGlQZFVuaHp4WUlKemR5UnBXZlh0REJFLXJ4ejR3PT0=
# Teamsbot Browser Bot Service
TEAMSBOT_BROWSER_BOT_URL = https://cae-poweron-shared.redwater-53d21339.switzerlandnorth.azurecontainerapps.io
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = FALSE
APP_DEBUG_CHAT_WORKFLOW_DIR = ./test-chat
# Manadate Pre-Processing Servers
PREPROCESS_ALTHAUS_CHAT_SECRET = PROD_ENC:Z0FBQUFBQnBaSnM4RVRmYW5IelNIbklTUDZIMEoycEN4ZFF0YUJoWWlUTUh2M0dhSXpYRXcwVkRGd1VieDNsYkdCRlpxMUR5Rjk1RDhPRkE5bmVtc2VDMURfLW9QNkxMVHN0M1JhbU9sa3JHWmdDZnlHS3BQRVBGTERVMHhXOVdDOWVqNkhfSUQyOHo=
# Preprocessor API Configuration
PP_QUERY_API_KEY=ouho02j0rj2oijroi3rj2oijro23jr0990
PP_QUERY_BASE_URL=https://poweron-althaus-preprocess-prod-e3fegaatc7faency.switzerlandnorth-01.azurewebsites.net/api/v1/dataquery/query
# Azure Communication Services Email Configuration
MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt
MESSAGING_ACS_SENDER_EMAIL = DoNotReply@poweron.swiss

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Base connector interface for AI connectors. Base connector interface for AI connectors.
@ -11,15 +11,15 @@ IMPORTANT: Model Registration Requirements
- If duplicate displayNames are detected during registration, an error will be raised - If duplicate displayNames are detected during registration, an error will be raised
""" """
import re import re as _re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional, AsyncGenerator, Union from typing import List, Dict, Any, Optional, AsyncGenerator, Union
from modules.datamodels.datamodelAi import AiModel, AiModelCall, AiModelResponse from modules.datamodels.datamodelAi import AiModel, AiModelCall, AiModelResponse
_RETRY_AFTER_PATTERN = re.compile( _RETRY_AFTER_PATTERN = _re.compile(
r"(?:try again in|retry after)\s+(\d+(?:\.\d+)?)\s*s", re.IGNORECASE r"(?:try again in|retry after)\s+(\d+(?:\.\d+)?)\s*s", _re.IGNORECASE
) )

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Dynamic model registry that collects models from all AI connectors. Dynamic model registry that collects models from all AI connectors.
@ -9,12 +9,12 @@ import logging
import importlib import importlib
import os import os
import time import time
import threading
from typing import Dict, List, Optional, Any, Tuple from typing import Dict, List, Optional, Any, Tuple
from modules.datamodels.datamodelAi import AiModel from modules.datamodels.datamodelAi import AiModel
from modules.datamodels.datamodelRbac import AccessRuleContext, RbacProtocol
from .aicoreBase import BaseConnectorAi from .aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelUam import User
from modules.security.rbacHelpers import checkResourceAccess
from modules.security.rbac import RbacClass
from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.connectors.connectorDbPostgre import DatabaseConnector
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,37 +31,11 @@ class ModelRegistry:
self._connectors: Dict[str, BaseConnectorAi] = {} self._connectors: Dict[str, BaseConnectorAi] = {}
self._lastRefresh: Optional[float] = None self._lastRefresh: Optional[float] = None
self._refreshInterval: float = 300.0 # 5 minutes self._refreshInterval: float = 300.0 # 5 minutes
self._refreshLock = threading.Lock()
self._connectorsInitialized: bool = False self._connectorsInitialized: bool = False
self._discoveredConnectorsCache: Optional[List[BaseConnectorAi]] = None # Avoid re-instantiating on every discoverConnectors() call self._discoveredConnectorsCache: Optional[List[BaseConnectorAi]] = None # Avoid re-instantiating on every discoverConnectors() call
self._getAvailableModelsCache: Dict[Tuple[str, int], Tuple[List[AiModel], float]] = {} # (user_id, rbac_id) -> (models, ts) self._getAvailableModelsCache: Dict[Tuple[str, int], Tuple[List[AiModel], float]] = {} # (user_id, rbac_id) -> (models, ts)
self._getAvailableModelsCacheTtl: float = 30.0 # seconds self._getAvailableModelsCacheTtl: float = 30.0 # seconds
def _addModelToDict(self, model: AiModel, connectorType: str, target: Dict[str, AiModel]):
"""Add model to a dict, tolerating benign re-adds from the same connector."""
if model.displayName in target:
existing = target[model.displayName]
if existing.name == model.name and existing.connectorType == model.connectorType:
logger.debug(f"Skipping duplicate model '{model.displayName}' from same connector {connectorType}")
return
raise ValueError(
f"displayName conflict '{model.displayName}': "
f"existing name='{existing.name}' (connector: {existing.connectorType}), "
f"new name='{model.name}' (connector: {connectorType})"
)
if TESTING_MAX_TOKENS_OVERRIDE is not None and model.maxTokens > TESTING_MAX_TOKENS_OVERRIDE:
originalMaxTokens = model.maxTokens
model.maxTokens = TESTING_MAX_TOKENS_OVERRIDE
logger.debug(f"TESTING: Overrode maxTokens for {model.displayName}: {originalMaxTokens} -> {TESTING_MAX_TOKENS_OVERRIDE}")
target[model.displayName] = model
logger.debug(f"Registered model: {model.displayName} (name: {model.name}) from {connectorType}")
def _addModel(self, model: AiModel, connectorType: str):
"""Convenience wrapper for adding to self._models."""
self._addModelToDict(model, connectorType, self._models)
def registerConnector(self, connector: BaseConnectorAi): def registerConnector(self, connector: BaseConnectorAi):
"""Register a connector and collect its models.""" """Register a connector and collect its models."""
connectorType = connector.getConnectorType() connectorType = connector.getConnectorType()
@ -73,10 +47,26 @@ class ModelRegistry:
self._connectors[connectorType] = connector self._connectors[connectorType] = connector
# Collect models from this connector
try: try:
models = connector.getCachedModels() models = connector.getCachedModels()
for model in models: for model in models:
self._addModel(model, connectorType) # Validate displayName uniqueness
if model.displayName in self._models:
existingModel = self._models[model.displayName]
errorMsg = f"Duplicate displayName '{model.displayName}' detected! Existing model: displayName='{existingModel.displayName}', name='{existingModel.name}' (connector: {existingModel.connectorType}), New model: displayName='{model.displayName}', name='{model.name}' (connector: {connectorType}). displayName must be unique."
logger.error(errorMsg)
raise ValueError(errorMsg)
# TODO TESTING: Override maxTokens if testing override is enabled
if TESTING_MAX_TOKENS_OVERRIDE is not None and model.maxTokens > TESTING_MAX_TOKENS_OVERRIDE:
originalMaxTokens = model.maxTokens
model.maxTokens = TESTING_MAX_TOKENS_OVERRIDE
logger.debug(f"TESTING: Overrode maxTokens for {model.displayName}: {originalMaxTokens} -> {TESTING_MAX_TOKENS_OVERRIDE}")
# Use displayName as the key (must be unique)
self._models[model.displayName] = model
logger.debug(f"Registered model: {model.displayName} (name: {model.name}) from {connectorType}")
except Exception as e: except Exception as e:
logger.error(f"Failed to register models from {connectorType}: {e}") logger.error(f"Failed to register models from {connectorType}: {e}")
raise raise
@ -126,40 +116,51 @@ class ModelRegistry:
self._connectorsInitialized = True self._connectorsInitialized = True
def refreshModels(self, force: bool = False): def refreshModels(self, force: bool = False):
"""Refresh models from all registered connectors. Thread-safe via _refreshLock.""" """Refresh models from all registered connectors."""
import time
self.ensureConnectorsRegistered() self.ensureConnectorsRegistered()
currentTime = time.time() currentTime = time.time()
if (not force and # Check if refresh is needed
self._lastRefresh is not None and if (not force and
self._lastRefresh is not None and
currentTime - self._lastRefresh < self._refreshInterval): currentTime - self._lastRefresh < self._refreshInterval):
return return
if not self._refreshLock.acquire(blocking=False): logger.info("Refreshing model registry...")
logger.debug("refreshModels already running in another thread, skipping")
return # Clear existing models
self._models.clear()
try:
logger.info("Refreshing model registry...") # Re-register all connectors
newModels: Dict[str, AiModel] = {} for connector in self._connectors.values():
try:
for connector in self._connectors.values(): connector.clearCache() # Clear connector cache
connectorType = connector.getConnectorType() models = connector.getCachedModels()
try: for model in models:
connector.clearCache() # Validate displayName uniqueness
models = connector.getCachedModels() if model.displayName in self._models:
for model in models: existingModel = self._models[model.displayName]
self._addModelToDict(model, connectorType, newModels) errorMsg = f"Duplicate displayName '{model.displayName}' detected! Existing model: displayName='{existingModel.displayName}', name='{existingModel.name}' (connector: {existingModel.connectorType}), New model: displayName='{model.displayName}', name='{model.name}' (connector: {connector.getConnectorType()}). displayName must be unique."
except Exception as e: logger.error(errorMsg)
logger.error(f"Failed to refresh models from {connectorType}: {e}") raise ValueError(errorMsg)
raise
# TODO TESTING: Override maxTokens if testing override is enabled
self._models = newModels if TESTING_MAX_TOKENS_OVERRIDE is not None and model.maxTokens > TESTING_MAX_TOKENS_OVERRIDE:
self._lastRefresh = time.time() originalMaxTokens = model.maxTokens
logger.info(f"Model registry refreshed: {len(self._models)} models available") model.maxTokens = TESTING_MAX_TOKENS_OVERRIDE
finally: logger.debug(f"TESTING: Overrode maxTokens for {model.displayName}: {originalMaxTokens} -> {TESTING_MAX_TOKENS_OVERRIDE}")
self._refreshLock.release()
# Use displayName as the key (must be unique)
self._models[model.displayName] = model
except Exception as e:
logger.error(f"Failed to refresh models from {connector.getConnectorType()}: {e}")
raise
self._lastRefresh = currentTime
logger.info(f"Model registry refreshed: {len(self._models)} models available")
def getModel(self, displayName: str) -> Optional[AiModel]: def getModel(self, displayName: str) -> Optional[AiModel]:
"""Get a specific model by displayName (displayName must be unique).""" """Get a specific model by displayName (displayName must be unique)."""
@ -185,7 +186,7 @@ class ModelRegistry:
def getAvailableModels( def getAvailableModels(
self, self,
currentUser: Optional[User] = None, currentUser: Optional[User] = None,
rbacInstance: Optional[RbacProtocol] = None, rbacInstance: Optional[RbacClass] = None,
mandateId: Optional[str] = None, mandateId: Optional[str] = None,
featureInstanceId: Optional[str] = None featureInstanceId: Optional[str] = None
) -> List[AiModel]: ) -> List[AiModel]:
@ -236,7 +237,7 @@ class ModelRegistry:
self, self,
models: List[AiModel], models: List[AiModel],
currentUser: User, currentUser: User,
rbacInstance: RbacProtocol, rbacInstance: RbacClass,
mandateId: Optional[str] = None, mandateId: Optional[str] = None,
featureInstanceId: Optional[str] = None featureInstanceId: Optional[str] = None
) -> List[AiModel]: ) -> List[AiModel]:
@ -261,7 +262,7 @@ class ModelRegistry:
logger.debug(f"User {currentUser.username} does not have access to model {model.displayName} (connector: {model.connectorType})") logger.debug(f"User {currentUser.username} does not have access to model {model.displayName} (connector: {model.connectorType})")
return filteredModels return filteredModels
def getModel(self, displayName: str, currentUser: Optional[User] = None, rbacInstance: Optional[RbacProtocol] = None) -> Optional[AiModel]: def getModel(self, displayName: str, currentUser: Optional[User] = None, rbacInstance: Optional[RbacClass] = None) -> Optional[AiModel]:
"""Get a specific model by displayName, optionally checking RBAC permissions. """Get a specific model by displayName, optionally checking RBAC permissions.
Args: Args:
@ -283,15 +284,8 @@ class ModelRegistry:
connectorResourcePath = f"ai.model.{model.connectorType}" connectorResourcePath = f"ai.model.{model.connectorType}"
modelResourcePath = f"ai.model.{model.connectorType}.{model.displayName}" modelResourcePath = f"ai.model.{model.connectorType}.{model.displayName}"
try: hasConnectorAccess = checkResourceAccess(rbacInstance, currentUser, connectorResourcePath)
connPerms = rbacInstance.getUserPermissions(currentUser, AccessRuleContext.RESOURCE, connectorResourcePath) hasModelAccess = checkResourceAccess(rbacInstance, currentUser, modelResourcePath)
modelPerms = rbacInstance.getUserPermissions(currentUser, AccessRuleContext.RESOURCE, modelResourcePath)
hasConnectorAccess = connPerms.view if connPerms else False
hasModelAccess = modelPerms.view if modelPerms else False
except Exception as e:
logger.error(f"Error checking resource access for {modelResourcePath}: {e}")
hasConnectorAccess = False
hasModelAccess = False
if not (hasConnectorAccess or hasModelAccess): if not (hasConnectorAccess or hasModelAccess):
logger.warning(f"User {currentUser.username} does not have access to model {displayName}") logger.warning(f"User {currentUser.username} does not have access to model {displayName}")
@ -347,8 +341,8 @@ class ModelRegistry:
modelRegistry = ModelRegistry() modelRegistry = ModelRegistry()
# Eager pre-warm on first import: ensures connectors are ready in this process. # Eager pre-warm on first import: ensures connectors are ready in this process.
# Critical for AI/agent performance — avoids 48 s latency on first request. # Critical for chatbot performance — avoids 48 s latency on first request.
# Runs when this module is first imported (lifespan or first AI request). # Runs when this module is first imported (lifespan or first chatbot request).
def _eager_prewarm() -> None: def _eager_prewarm() -> None:
try: try:
modelRegistry.ensureConnectorsRegistered() modelRegistry.ensureConnectorsRegistered()

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Simplified model selection based on model properties and priority-based sorting. Simplified model selection based on model properties and priority-based sorting.
@ -140,10 +140,11 @@ class ModelSelector:
promptFiltered.append(model) promptFiltered.append(model)
else: else:
maxAllowedTokens = model.contextLength * 0.8 maxAllowedTokens = model.contextLength * 0.8
if totalTokens <= maxAllowedTokens: # Compare prompt tokens (not bytes) with model's token limit
if promptTokens <= maxAllowedTokens:
promptFiltered.append(model) promptFiltered.append(model)
else: else:
logger.debug(f"Model {model.name} filtered out: totalTokens={totalTokens:.0f} > maxAllowed={maxAllowedTokens:.0f} tokens (80% of {model.contextLength} tokens)") logger.debug(f"Model {model.name} filtered out: promptSize={promptTokens:.0f} tokens > maxAllowed={maxAllowedTokens:.0f} tokens (80% of {model.contextLength} tokens)")
logger.debug(f"After prompt size filtering: {len(promptFiltered)} models") logger.debug(f"After prompt size filtering: {len(promptFiltered)} models")
@ -271,9 +272,7 @@ class ModelSelector:
return 1.0 return 1.0
elif requestedPriority == PriorityEnum.SPEED: elif requestedPriority == PriorityEnum.SPEED:
# Scale to same magnitude as operation type (x1000) so speed return model.speedRating / 10.0
# can meaningfully influence model ranking across tiers.
return model.speedRating * 100.0
elif requestedPriority == PriorityEnum.QUALITY: elif requestedPriority == PriorityEnum.QUALITY:
return model.qualityRating / 10.0 return model.qualityRating / 10.0
@ -323,4 +322,4 @@ class ModelSelector:
# Global model selector instance # Global model selector instance
modelSelector = ModelSelector() modelSelector = ModelSelector()

View file

@ -1,6 +1,5 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
import base64
import json import json
import logging import logging
import httpx import httpx
@ -14,37 +13,6 @@ from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingMode
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _supportsCustomTemperature(modelName: str) -> bool:
"""Check whether an Anthropic model accepts a custom ``temperature``.
Anthropic's Extended-Thinking models (Claude 4.7 Opus and the
upcoming 4.7 Sonnet/Haiku, plus all 5.x and beyond) reject every
``temperature`` value with HTTP 400
``{"error": "`temperature` is deprecated for this model."}`` --
only the model's internal default is accepted. Older Claude 4.5 /
4.6 models still accept any value in [0, 1].
Returns:
True if ``temperature`` may be sent; False if it must be omitted.
"""
if not modelName:
return True
name = modelName.lower()
if name.startswith("claude-opus-4-8"):
return False
if name.startswith("claude-opus-4-7"):
return False
if name.startswith("claude-sonnet-4-7"):
return False
if name.startswith("claude-haiku-4-7"):
return False
# 5.x and beyond: same Extended-Thinking family, no custom temperature.
if name.startswith("claude-opus-5") or name.startswith("claude-sonnet-5") or name.startswith("claude-haiku-5"):
return False
return True
def loadConfigData(): def loadConfigData():
"""Load configuration data for Anthropic connector""" """Load configuration data for Anthropic connector"""
return { return {
@ -81,150 +49,6 @@ class AiAnthropic(BaseConnectorAi):
def getModels(self) -> List[AiModel]: def getModels(self) -> List[AiModel]:
# Get all available Anthropic models. # Get all available Anthropic models.
return [ return [
AiModel(
name="claude-opus-4-8",
displayName="Anthropic Claude Opus 4.8",
connectorType="anthropic",
apiUrl="https://api.anthropic.com/v1/messages",
temperature=0.2,
maxTokens=128000,
contextLength=1000000,
costPer1kTokensInput=0.005, # $5/M tokens (Anthropic API, 2026-05)
costPer1kTokensOutput=0.025, # $25/M tokens
speedRating=5,
qualityRating=10,
functionCall=self.callAiBasic,
functionCallStream=self.callAiBasicStream,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 10),
(OperationTypeEnum.DATA_ANALYSE, 9),
(OperationTypeEnum.DATA_GENERATE, 10),
(OperationTypeEnum.DATA_EXTRACT, 9),
(OperationTypeEnum.AGENT, 10),
(OperationTypeEnum.DATA_QUERY, 3),
),
version="claude-opus-4-8",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.025
),
AiModel(
name="claude-opus-4-8",
displayName="Anthropic Claude Opus 4.8 Vision",
connectorType="anthropic",
apiUrl="https://api.anthropic.com/v1/messages",
temperature=0.2,
maxTokens=128000,
contextLength=1000000,
costPer1kTokensInput=0.005,
costPer1kTokensOutput=0.025,
speedRating=5,
qualityRating=10,
functionCall=self.callAiImage,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.IMAGE_ANALYSE, 10)
),
version="claude-opus-4-8",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.025
),
AiModel(
name="claude-opus-4-7",
displayName="Anthropic Claude Opus 4.7",
connectorType="anthropic",
apiUrl="https://api.anthropic.com/v1/messages",
temperature=0.2,
maxTokens=128000,
contextLength=1000000,
costPer1kTokensInput=0.005, # $5/M tokens (Anthropic API, 2026-04)
costPer1kTokensOutput=0.025, # $25/M tokens
speedRating=5,
qualityRating=10,
functionCall=self.callAiBasic,
functionCallStream=self.callAiBasicStream,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 10),
(OperationTypeEnum.DATA_ANALYSE, 9),
(OperationTypeEnum.DATA_GENERATE, 10),
(OperationTypeEnum.DATA_EXTRACT, 9),
(OperationTypeEnum.AGENT, 10),
(OperationTypeEnum.DATA_QUERY, 3),
),
version="claude-opus-4-7",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.025
),
AiModel(
name="claude-sonnet-4-6",
displayName="Anthropic Claude Sonnet 4.6",
connectorType="anthropic",
apiUrl="https://api.anthropic.com/v1/messages",
temperature=0.2,
maxTokens=64000,
contextLength=1000000,
costPer1kTokensInput=0.003, # $3/M tokens
costPer1kTokensOutput=0.015, # $15/M tokens
speedRating=7,
qualityRating=10,
functionCall=self.callAiBasic,
functionCallStream=self.callAiBasicStream,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.ADVANCED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 9),
(OperationTypeEnum.DATA_ANALYSE, 9),
(OperationTypeEnum.DATA_GENERATE, 9),
(OperationTypeEnum.DATA_EXTRACT, 8),
(OperationTypeEnum.AGENT, 9),
(OperationTypeEnum.DATA_QUERY, 9),
),
version="claude-sonnet-4-6",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.003 + (bytesReceived / 4 / 1000) * 0.015
),
AiModel(
name="claude-opus-4-7",
displayName="Anthropic Claude Opus 4.7 Vision",
connectorType="anthropic",
apiUrl="https://api.anthropic.com/v1/messages",
temperature=0.2,
maxTokens=128000,
contextLength=1000000,
costPer1kTokensInput=0.005,
costPer1kTokensOutput=0.025,
speedRating=5,
qualityRating=10,
functionCall=self.callAiImage,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.IMAGE_ANALYSE, 10)
),
version="claude-opus-4-7",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.025
),
AiModel(
name="claude-sonnet-4-6",
displayName="Anthropic Claude Sonnet 4.6 Vision",
connectorType="anthropic",
apiUrl="https://api.anthropic.com/v1/messages",
temperature=0.2,
maxTokens=64000,
contextLength=1000000,
costPer1kTokensInput=0.003,
costPer1kTokensOutput=0.015,
speedRating=6,
qualityRating=10,
functionCall=self.callAiImage,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.IMAGE_ANALYSE, 10)
),
version="claude-sonnet-4-6",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.003 + (bytesReceived / 4 / 1000) * 0.015
),
AiModel( AiModel(
name="claude-sonnet-4-5-20250929", name="claude-sonnet-4-5-20250929",
displayName="Anthropic Claude Sonnet 4.5", displayName="Anthropic Claude Sonnet 4.5",
@ -356,12 +180,9 @@ class AiAnthropic(BaseConnectorAi):
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
"model": model.name, "model": model.name,
"messages": converted_messages, "messages": converted_messages,
"temperature": temperature,
} }
# Extended-Thinking models (claude-opus-4-7 etc.) reject any
# `temperature` value -- only the model default is accepted.
if _supportsCustomTemperature(model.name):
payload["temperature"] = temperature
# Anthropic requires max_tokens - use provided value or throw error # Anthropic requires max_tokens - use provided value or throw error
if maxTokens is None: if maxTokens is None:
raise ValueError("maxTokens must be provided for Anthropic API calls") raise ValueError("maxTokens must be provided for Anthropic API calls")
@ -402,7 +223,6 @@ class AiAnthropic(BaseConnectorAi):
# Parse response # Parse response
anthropicResponse = response.json() anthropicResponse = response.json()
stop_reason = anthropicResponse.get("stop_reason")
# Extract content and tool_use blocks from response # Extract content and tool_use blocks from response
content = "" content = ""
@ -426,25 +246,9 @@ class AiAnthropic(BaseConnectorAi):
if not content and not toolCalls: if not content and not toolCalls:
logger.warning(f"Anthropic API returned empty content. Full response: {anthropicResponse}") logger.warning(f"Anthropic API returned empty content. Full response: {anthropicResponse}")
err = ( content = "[Anthropic API returned empty response]"
"Anthropic refused the request (content policy) — try another model or adjust the prompt."
if stop_reason == "refusal"
else f"Anthropic returned no assistant text (stop_reason={stop_reason or 'unknown'})."
)
return AiModelResponse(
content="",
success=False,
error=err,
modelId=model.name,
metadata={
"response_id": anthropicResponse.get("id", ""),
"stop_reason": stop_reason,
},
)
metadata = {"response_id": anthropicResponse.get("id", "")} metadata = {"response_id": anthropicResponse.get("id", "")}
if stop_reason:
metadata["stop_reason"] = stop_reason
if toolCalls: if toolCalls:
metadata["toolCalls"] = toolCalls metadata["toolCalls"] = toolCalls
@ -481,11 +285,10 @@ class AiAnthropic(BaseConnectorAi):
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
"model": model.name, "model": model.name,
"messages": converted, "messages": converted,
"temperature": temperature,
"max_tokens": model.maxTokens, "max_tokens": model.maxTokens,
"stream": True, "stream": True,
} }
if _supportsCustomTemperature(model.name):
payload["temperature"] = temperature
if system_prompt: if system_prompt:
payload["system"] = system_prompt payload["system"] = system_prompt
if modelCall.tools: if modelCall.tools:
@ -560,19 +363,6 @@ class AiAnthropic(BaseConnectorAi):
f"Anthropic stream returned empty response: model={model.name}, " f"Anthropic stream returned empty response: model={model.name}, "
f"stopReason={stopReason}" f"stopReason={stopReason}"
) )
err = (
"Anthropic refused the request (content policy) — try another model or adjust the prompt."
if stopReason == "refusal"
else f"Anthropic returned no assistant text (stop_reason={stopReason or 'unknown'})."
)
yield AiModelResponse(
content="",
success=False,
error=err,
modelId=model.name,
metadata={"stopReason": stopReason} if stopReason else {},
)
return
metadata: Dict[str, Any] = {} metadata: Dict[str, Any] = {}
if stopReason: if stopReason:
@ -655,9 +445,9 @@ class AiAnthropic(BaseConnectorAi):
mimeType = parts[0].replace("data:", "") mimeType = parts[0].replace("data:", "")
base64Data = parts[1] base64Data = parts[1]
_SUPPORTED = {"image/jpeg", "image/png", "image/gif", "image/webp"} import base64 as _b64
try: try:
rawHead = base64.b64decode(base64Data[:32]) rawHead = _b64.b64decode(base64Data[:32])
if rawHead[:3] == b"\xff\xd8\xff": if rawHead[:3] == b"\xff\xd8\xff":
mimeType = "image/jpeg" mimeType = "image/jpeg"
elif rawHead[:8] == b"\x89PNG\r\n\x1a\n": elif rawHead[:8] == b"\x89PNG\r\n\x1a\n":
@ -668,9 +458,6 @@ class AiAnthropic(BaseConnectorAi):
mimeType = "image/webp" mimeType = "image/webp"
except Exception: except Exception:
pass pass
if mimeType not in _SUPPORTED:
raise ValueError(f"Unsupported image media_type '{mimeType}' for Anthropic (supported: {', '.join(sorted(_SUPPORTED))})")
# Convert to Anthropic's vision format # Convert to Anthropic's vision format
anthropicMessages = [{ anthropicMessages = [{
@ -725,10 +512,10 @@ class AiAnthropic(BaseConnectorAi):
if systemPrompt: if systemPrompt:
payload["system"] = systemPrompt payload["system"] = systemPrompt
if _supportsCustomTemperature(model.name): # Set temperature from model
payload["temperature"] = temperature payload["temperature"] = temperature
# Make API call with headers from httpClient (which includes anthropic-version) # Make API call with headers from httpClient (which includes anthropic-version)
response = await self.httpClient.post( response = await self.httpClient.post(
"https://api.anthropic.com/v1/messages", "https://api.anthropic.com/v1/messages",
@ -862,4 +649,4 @@ def _convertToolsToAnthropicFormat(openaiTools: List[Dict[str, Any]]) -> List[Di
"description": fn.get("description", ""), "description": fn.get("description", ""),
"input_schema": fn.get("parameters", {"type": "object", "properties": {}}) "input_schema": fn.get("parameters", {"type": "object", "properties": {}})
}) })
return anthropicTools return anthropicTools

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
import logging import logging
from typing import List from typing import List

View file

@ -1,7 +1,7 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
import logging import logging
import json import json as _json
import httpx import httpx
from typing import List, Dict, Any, AsyncGenerator, Union from typing import List, Dict, Any, AsyncGenerator, Union
from fastapi import HTTPException from fastapi import HTTPException
@ -274,7 +274,7 @@ class AiMistral(BaseConnectorAi):
bodyStr = body.decode() bodyStr = body.decode()
if response.status_code == 429: if response.status_code == 429:
try: try:
errorMsg = json.loads(bodyStr).get("error", {}).get("message", "Rate limit exceeded") errorMsg = _json.loads(bodyStr).get("error", {}).get("message", "Rate limit exceeded")
except (ValueError, KeyError): except (ValueError, KeyError):
errorMsg = f"Rate limit exceeded for {model.name}" errorMsg = f"Rate limit exceeded for {model.name}"
raise RateLimitExceededException(f"Rate limit exceeded for {model.name}: {errorMsg}") raise RateLimitExceededException(f"Rate limit exceeded for {model.name}: {errorMsg}")
@ -287,8 +287,8 @@ class AiMistral(BaseConnectorAi):
if data.strip() == "[DONE]": if data.strip() == "[DONE]":
break break
try: try:
chunk = json.loads(data) chunk = _json.loads(data)
except json.JSONDecodeError: except _json.JSONDecodeError:
continue continue
delta = chunk.get("choices", [{}])[0].get("delta", {}) delta = chunk.get("choices", [{}])[0].get("delta", {})

View file

@ -1,7 +1,7 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
import logging import logging
import json import json as _json
import httpx import httpx
from typing import List, Dict, Any, AsyncGenerator, Union from typing import List, Dict, Any, AsyncGenerator, Union
from fastapi import HTTPException from fastapi import HTTPException
@ -11,30 +11,6 @@ from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _supportsCustomTemperature(modelName: str) -> bool:
"""Check whether an OpenAI model accepts a custom `temperature` value.
GPT-5.x and the o-series (o1/o3/o4) reasoning models reject every
`temperature` value other than the default (1) with HTTP 400
`unsupported_value`. For these models we must omit `temperature`
from the payload entirely. Older chat-completions models
(gpt-4o, gpt-4o-mini, gpt-4.1, gpt-3.5-*) still accept any value
in [0, 2].
Returns:
True if `temperature` may be sent; False if it must be omitted.
"""
if not modelName:
return True
name = modelName.lower()
if name.startswith("gpt-5"):
return False
if name.startswith("o1") or name.startswith("o3") or name.startswith("o4"):
return False
return True
def loadConfigData(): def loadConfigData():
"""Load configuration data for OpenAI connector""" """Load configuration data for OpenAI connector"""
return { return {
@ -147,135 +123,6 @@ class AiOpenai(BaseConnectorAi):
version="gpt-4o", version="gpt-4o",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0025 + (bytesReceived / 4 / 1000) * 0.01 calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0025 + (bytesReceived / 4 / 1000) * 0.01
), ),
AiModel(
name="gpt-5.5",
displayName="OpenAI GPT-5.5",
connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2,
maxTokens=128000,
contextLength=1050000,
costPer1kTokensInput=0.005, # $5/M tokens (OpenAI API, 2026-04)
costPer1kTokensOutput=0.03, # $30/M tokens
speedRating=8,
qualityRating=10,
functionCall=self.callAiBasic,
functionCallStream=self.callAiBasicStream,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 10),
(OperationTypeEnum.DATA_ANALYSE, 10),
(OperationTypeEnum.DATA_GENERATE, 10),
(OperationTypeEnum.DATA_EXTRACT, 8),
(OperationTypeEnum.AGENT, 10),
(OperationTypeEnum.DATA_QUERY, 8),
),
version="gpt-5.5",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.03
),
AiModel(
name="gpt-5.4",
displayName="OpenAI GPT-5.4",
connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2,
maxTokens=128000,
contextLength=1050000,
costPer1kTokensInput=0.0025, # $2.50/M tokens
costPer1kTokensOutput=0.015, # $15/M tokens
speedRating=8,
qualityRating=10,
functionCall=self.callAiBasic,
functionCallStream=self.callAiBasicStream,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.ADVANCED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 9),
(OperationTypeEnum.DATA_ANALYSE, 10),
(OperationTypeEnum.DATA_GENERATE, 10),
(OperationTypeEnum.DATA_EXTRACT, 8),
(OperationTypeEnum.AGENT, 9),
(OperationTypeEnum.DATA_QUERY, 8),
),
version="gpt-5.4",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0025 + (bytesReceived / 4 / 1000) * 0.015
),
AiModel(
name="gpt-5.4-mini",
displayName="OpenAI GPT-5.4 Mini",
connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2,
maxTokens=128000,
contextLength=400000,
costPer1kTokensInput=0.00075, # $0.75/M tokens
costPer1kTokensOutput=0.0045, # $4.50/M tokens
speedRating=9,
qualityRating=9,
functionCall=self.callAiBasic,
functionCallStream=self.callAiBasicStream,
priority=PriorityEnum.SPEED,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 8),
(OperationTypeEnum.DATA_ANALYSE, 9),
(OperationTypeEnum.DATA_GENERATE, 9),
(OperationTypeEnum.DATA_EXTRACT, 8),
(OperationTypeEnum.AGENT, 8),
(OperationTypeEnum.DATA_QUERY, 10),
),
version="gpt-5.4-mini",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.00075 + (bytesReceived / 4 / 1000) * 0.0045
),
AiModel(
name="gpt-5.4-nano",
displayName="OpenAI GPT-5.4 Nano",
connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2,
maxTokens=128000,
contextLength=400000,
costPer1kTokensInput=0.0002, # $0.20/M tokens
costPer1kTokensOutput=0.00125, # $1.25/M tokens
speedRating=10,
qualityRating=7,
functionCall=self.callAiBasic,
functionCallStream=self.callAiBasicStream,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 7),
(OperationTypeEnum.DATA_ANALYSE, 7),
(OperationTypeEnum.DATA_GENERATE, 8),
(OperationTypeEnum.DATA_EXTRACT, 9),
(OperationTypeEnum.AGENT, 7),
(OperationTypeEnum.DATA_QUERY, 10),
),
version="gpt-5.4-nano",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0002 + (bytesReceived / 4 / 1000) * 0.00125
),
AiModel(
name="gpt-5.5",
displayName="OpenAI GPT-5.5 Vision",
connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2,
maxTokens=128000,
contextLength=1050000,
costPer1kTokensInput=0.005,
costPer1kTokensOutput=0.03,
speedRating=6,
qualityRating=10,
functionCall=self.callAiImage,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.IMAGE_ANALYSE, 10)
),
version="gpt-5.5",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.03
),
AiModel( AiModel(
name="text-embedding-3-small", name="text-embedding-3-small",
displayName="OpenAI Embedding Small", displayName="OpenAI Embedding Small",
@ -319,24 +166,25 @@ class AiOpenai(BaseConnectorAi):
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.00013 calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.00013
), ),
AiModel( AiModel(
name="gpt-image-1", name="dall-e-3",
displayName="OpenAI GPT Image", displayName="OpenAI DALL-E 3",
connectorType="openai", connectorType="openai",
apiUrl="https://api.openai.com/v1/images/generations", apiUrl="https://api.openai.com/v1/images/generations",
temperature=0.0, temperature=0.0, # Image generation doesn't use temperature
maxTokens=0, maxTokens=0, # Image generation doesn't use tokens
contextLength=0, contextLength=0,
costPer1kTokensInput=0.04, costPer1kTokensInput=0.04,
costPer1kTokensOutput=0.0, costPer1kTokensOutput=0.0,
speedRating=5, speedRating=5, # Slow for image generation
qualityRating=9, qualityRating=9, # High quality art generation
# capabilities removed (not used in business logic)
functionCall=self.generateImage, functionCall=self.generateImage,
priority=PriorityEnum.QUALITY, priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED, processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings( operationTypes=createOperationTypeRatings(
(OperationTypeEnum.IMAGE_GENERATE, 10) (OperationTypeEnum.IMAGE_GENERATE, 10)
), ),
version="gpt-image-1", version="dall-e-3",
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04 calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
) )
] ]
@ -367,18 +215,10 @@ class AiOpenai(BaseConnectorAi):
payload = { payload = {
"model": model.name, "model": model.name,
"messages": messages, "messages": messages,
# Universal output-length cap. `max_tokens` is deprecated and "temperature": temperature,
# rejected outright by gpt-5.x / o-series; `max_completion_tokens` "max_tokens": maxTokens
# is accepted by every current chat-completions model (legacy
# gpt-4o, gpt-4.1, gpt-5.x, o1/o3/o4) per OpenAI API reference.
"max_completion_tokens": maxTokens
} }
# gpt-5.x and o-series only accept the default temperature (1) and
# return HTTP 400 `unsupported_value` for anything else - omit the
# field entirely for those models.
if _supportsCustomTemperature(model.name):
payload["temperature"] = temperature
if modelCall.tools: if modelCall.tools:
payload["tools"] = modelCall.tools payload["tools"] = modelCall.tools
payload["tool_choice"] = modelCall.toolChoice or "auto" payload["tool_choice"] = modelCall.toolChoice or "auto"
@ -455,15 +295,10 @@ class AiOpenai(BaseConnectorAi):
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
"model": model.name, "model": model.name,
"messages": messages, "messages": messages,
# See callAiBasic for the rationale: `max_completion_tokens` "temperature": temperature,
# is the universal output-length parameter; `max_tokens` is "max_tokens": model.maxTokens,
# deprecated and rejected by gpt-5.x / o-series.
"max_completion_tokens": model.maxTokens,
"stream": True, "stream": True,
} }
if _supportsCustomTemperature(model.name):
payload["temperature"] = temperature
if modelCall.tools: if modelCall.tools:
payload["tools"] = modelCall.tools payload["tools"] = modelCall.tools
payload["tool_choice"] = modelCall.toolChoice or "auto" payload["tool_choice"] = modelCall.toolChoice or "auto"
@ -477,7 +312,7 @@ class AiOpenai(BaseConnectorAi):
bodyStr = body.decode() bodyStr = body.decode()
if response.status_code == 429: if response.status_code == 429:
try: try:
errorMsg = json.loads(bodyStr).get("error", {}).get("message", "Rate limit exceeded") errorMsg = _json.loads(bodyStr).get("error", {}).get("message", "Rate limit exceeded")
except (ValueError, KeyError): except (ValueError, KeyError):
errorMsg = f"Rate limit exceeded for {model.name}" errorMsg = f"Rate limit exceeded for {model.name}"
raise RateLimitExceededException(f"Rate limit exceeded for {model.name}: {errorMsg}") raise RateLimitExceededException(f"Rate limit exceeded for {model.name}: {errorMsg}")
@ -490,8 +325,8 @@ class AiOpenai(BaseConnectorAi):
if data.strip() == "[DONE]": if data.strip() == "[DONE]":
break break
try: try:
chunk = json.loads(data) chunk = _json.loads(data)
except json.JSONDecodeError: except _json.JSONDecodeError:
continue continue
delta = chunk.get("choices", [{}])[0].get("delta", {}) delta = chunk.get("choices", [{}])[0].get("delta", {})
@ -614,15 +449,15 @@ class AiOpenai(BaseConnectorAi):
# Use the messages directly - they should already contain the image data # Use the messages directly - they should already contain the image data
# in the format: {"type": "image_url", "image_url": {"url": "data:...base64,..."}} # in the format: {"type": "image_url", "image_url": {"url": "data:...base64,..."}}
# Use parameters from model
temperature = model.temperature temperature = model.temperature
# Don't set maxTokens - let the model use its full context length # Don't set maxTokens - let the model use its full context length
payload = { payload = {
"model": model.name, "model": model.name,
"messages": messages, "messages": messages,
"temperature": temperature
} }
if _supportsCustomTemperature(model.name):
payload["temperature"] = temperature
response = await self.httpClient.post( response = await self.httpClient.post(
model.apiUrl, model.apiUrl,
@ -652,82 +487,105 @@ class AiOpenai(BaseConnectorAi):
) )
async def generateImage(self, modelCall: AiModelCall) -> AiModelResponse: async def generateImage(self, modelCall: AiModelCall) -> AiModelResponse:
"""Generate an image using GPT Image model (gpt-image-1).""" """
Generate an image using DALL-E 3 using standardized pattern.
Args:
modelCall: AiModelCall with messages and generation options
Returns:
AiModelResponse with generated image data
"""
try: try:
import json # Extract parameters from modelCall
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model
options = modelCall.options options = modelCall.options
# Get prompt from messages
promptContent = messages[0]["content"] if messages else "" promptContent = messages[0]["content"] if messages else ""
# Parse prompt using AiCallPromptImage model
import json
try: try:
# Try to parse as JSON
promptData = json.loads(promptContent) promptData = json.loads(promptContent)
promptModel = AiCallPromptImage(**promptData) promptModel = AiCallPromptImage(**promptData)
except Exception: except:
# If not JSON, use plain text prompt
promptModel = AiCallPromptImage( promptModel = AiCallPromptImage(
prompt=promptContent, prompt=promptContent,
size=options.size if options and hasattr(options, "size") else "1024x1024", size=options.size if options and hasattr(options, 'size') else "1024x1024",
quality=options.quality if options and hasattr(options, "quality") else "auto", quality=options.quality if options and hasattr(options, 'quality') else "standard",
style=options.style if options and hasattr(options, 'style') else "vivid"
) )
# Extract parameters from Pydantic model
prompt = promptModel.prompt prompt = promptModel.prompt
size = promptModel.size or "1024x1024" size = promptModel.size or "1024x1024"
rawQuality = promptModel.quality or "auto" quality = promptModel.quality or "standard"
quality = {"standard": "auto", "hd": "high"}.get(rawQuality, rawQuality) style = promptModel.style or "vivid"
logger.debug(f"Starting image generation with prompt: '{prompt[:100]}...'") logger.debug(f"Starting image generation with prompt: '{prompt[:100]}...'")
# DALL-E 3 API endpoint
dalle_url = "https://api.openai.com/v1/images/generations"
payload = { payload = {
"model": "gpt-image-1", "model": "dall-e-3",
"prompt": prompt, "prompt": prompt,
"size": size, "size": size,
"quality": quality, "quality": quality,
"style": style,
"n": 1, "n": 1,
"response_format": "b64_json" # Get base64 data directly instead of URLs
} }
# Use existing httpClient to benefit from connection pooling
# This avoids TLS connection issues that can occur with fresh clients
response = await self.httpClient.post( response = await self.httpClient.post(
"https://api.openai.com/v1/images/generations", dalle_url,
json=payload, json=payload
) )
if response.status_code != 200: if response.status_code != 200:
logger.error(f"Image generation API error: {response.status_code} - {response.text}") logger.error(f"DALL-E API error: {response.status_code} - {response.text}")
return AiModelResponse( return AiModelResponse(
content="", content="",
success=False, success=False,
error=f"Image generation API error: {response.status_code} - {response.text}", error=f"DALL-E API error: {response.status_code} - {response.text}"
) )
responseJson = response.json() responseJson = response.json()
if "data" in responseJson and len(responseJson["data"]) > 0: if "data" in responseJson and len(responseJson["data"]) > 0:
imageData = responseJson["data"][0].get("b64_json", "") image_data = responseJson["data"][0]["b64_json"]
if not imageData:
imageData = responseJson["data"][0].get("url", "") logger.info(f"Successfully generated image: {len(image_data)} characters")
logger.info(f"Successfully generated image: {len(imageData)} characters")
return AiModelResponse( return AiModelResponse(
content=imageData, content=image_data,
success=True, success=True,
modelId="gpt-image-1", modelId="dall-e-3",
metadata={ metadata={
"size": size, "size": size,
"quality": quality, "quality": quality,
"response_id": responseJson.get("id", ""), "style": style,
}, "response_id": responseJson.get("id", "")
}
) )
else: else:
logger.error("No image data in generation response") logger.error("No image data in DALL-E response")
return AiModelResponse( return AiModelResponse(
content="", content="",
success=False, success=False,
error="No image data in generation response", error="No image data in DALL-E response"
) )
except Exception as e: except Exception as e:
logger.error(f"Error during image generation: {str(e)}", exc_info=True) logger.error(f"Error during image generation: {str(e)}", exc_info=True)
return AiModelResponse( return AiModelResponse(
content="", content="",
success=False, success=False,
error=f"Error during image generation: {str(e)}", error=f"Error during image generation: {str(e)}"
) )

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
import logging import logging
import httpx import httpx

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
AI Connector for PowerOn Private-LLM Service. AI Connector for PowerOn Private-LLM Service.
@ -6,17 +6,14 @@ AI Connector for PowerOn Private-LLM Service.
Connects to the private-llm service running on-premise with Ollama backend. Connects to the private-llm service running on-premise with Ollama backend.
Provides OCR and Vision capabilities via local AI models. Provides OCR and Vision capabilities via local AI models.
Models (current L4 24 GB): Models:
- poweron-text-general: Text (qwen2.5:7b); NEUTRALIZATION_TEXT + data/plan ops - poweron-text-general: Text (qwen2.5); NEUTRALIZATION_TEXT + data/plan ops
- poweron-vision-general: Vision (qwen2.5vl:7b); IMAGE_ANALYSE + NEUTRALIZATION_IMAGE - poweron-vision-general: Vision (qwen2.5vl); IMAGE_ANALYSE + NEUTRALIZATION_IMAGE
- poweron-vision-deep: Vision (granite3.2); IMAGE_ANALYSE + NEUTRALIZATION_IMAGE - poweron-vision-deep: Vision (granite3.2); IMAGE_ANALYSE + NEUTRALIZATION_IMAGE
Models (next-gen RTX PRO 6000 96 GB, auto-activated when pulled in Ollama): Pricing (CHF per call):
- poweron-text-reasoning: Reasoning (deepseek-r1:70b); complex logic, math, planning - Text models: CHF 0.010
- poweron-vision-general: Vision (llama4:scout); multimodal, long-context documents - Vision models: CHF 0.100
- poweron-embed: Embedding (nomic-embed-text); local RAG embedding
Pricing: byte-based (~per-token via bytes/4), configured via the PRICE_* constants below.
""" """
import logging import logging
@ -39,20 +36,9 @@ from modules.datamodels.datamodelAi import (
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Pricing constants (CHF per 1k tokens; billed byte-based via bytes/4 ~ 1 token) # Pricing constants (CHF)
PRICE_INPUT_PER_1K = 0.0075 PRICE_TEXT_PER_CALL = 0.01 # CHF 0.010 per text model call
PRICE_OUTPUT_PER_1K = 0.0375 PRICE_VISION_PER_CALL = 0.10 # CHF 0.100 per vision model call
PRICE_EMBED_PER_1K = 0.0005
def _calcPrivatePriceCHF(processingTime, bytesSent, bytesReceived):
"""Byte-based price for private text/vision/reasoning models."""
return (bytesSent / 4 / 1000) * PRICE_INPUT_PER_1K + (bytesReceived / 4 / 1000) * PRICE_OUTPUT_PER_1K
def _calcPrivateEmbedPriceCHF(processingTime, bytesSent, bytesReceived):
"""Byte-based price for private embedding (input only)."""
return (bytesSent / 4 / 1000) * PRICE_EMBED_PER_1K
# Private-LLM Service URL (fix, nicht via env konfigurierbar) # Private-LLM Service URL (fix, nicht via env konfigurierbar)
@ -247,8 +233,8 @@ class AiPrivateLlm(BaseConnectorAi):
temperature=0.1, temperature=0.1,
maxTokens=4096, maxTokens=4096,
contextLength=8192, # Reduced for RAM constraints contextLength=8192, # Reduced for RAM constraints
costPer1kTokensInput=PRICE_INPUT_PER_1K, costPer1kTokensInput=0.0, # Flat rate pricing
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K, costPer1kTokensOutput=0.0, # Flat rate pricing
speedRating=8, # Fast and efficient speedRating=8, # Fast and efficient
qualityRating=9, # High quality text model qualityRating=9, # High quality text model
functionCall=self.callAiText, functionCall=self.callAiText,
@ -260,11 +246,9 @@ class AiPrivateLlm(BaseConnectorAi):
(OperationTypeEnum.DATA_GENERATE, 8), (OperationTypeEnum.DATA_GENERATE, 8),
(OperationTypeEnum.DATA_EXTRACT, 8), (OperationTypeEnum.DATA_EXTRACT, 8),
(OperationTypeEnum.NEUTRALIZATION_TEXT, 9), (OperationTypeEnum.NEUTRALIZATION_TEXT, 9),
# Agent loop (workspace etc.) selects models by OperationTypeEnum.AGENT for streaming.
(OperationTypeEnum.AGENT, 8),
), ),
version="qwen2.5:7b", version="qwen2.5:7b",
calculatepriceCHF=_calcPrivatePriceCHF calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: PRICE_TEXT_PER_CALL
), ),
"ollamaModel": "qwen2.5:7b" "ollamaModel": "qwen2.5:7b"
}, },
@ -278,8 +262,8 @@ class AiPrivateLlm(BaseConnectorAi):
temperature=0.2, temperature=0.2,
maxTokens=2048, maxTokens=2048,
contextLength=4096, # Reduced for RAM constraints (vision needs more) contextLength=4096, # Reduced for RAM constraints (vision needs more)
costPer1kTokensInput=PRICE_INPUT_PER_1K, costPer1kTokensInput=0.0, # Flat rate pricing
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K, costPer1kTokensOutput=0.0, # Flat rate pricing
speedRating=7, speedRating=7,
qualityRating=9, qualityRating=9,
functionCall=self.callAiVision, functionCall=self.callAiVision,
@ -290,7 +274,7 @@ class AiPrivateLlm(BaseConnectorAi):
(OperationTypeEnum.NEUTRALIZATION_IMAGE, 9), (OperationTypeEnum.NEUTRALIZATION_IMAGE, 9),
), ),
version="qwen2.5vl:7b", version="qwen2.5vl:7b",
calculatepriceCHF=_calcPrivatePriceCHF calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: PRICE_VISION_PER_CALL
), ),
"ollamaModel": "qwen2.5vl:7b" "ollamaModel": "qwen2.5vl:7b"
}, },
@ -304,8 +288,8 @@ class AiPrivateLlm(BaseConnectorAi):
temperature=0.1, temperature=0.1,
maxTokens=2048, maxTokens=2048,
contextLength=4096, # Reduced for RAM constraints contextLength=4096, # Reduced for RAM constraints
costPer1kTokensInput=PRICE_INPUT_PER_1K, costPer1kTokensInput=0.0, # Flat rate pricing
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K, costPer1kTokensOutput=0.0, # Flat rate pricing
speedRating=9, # Fast due to small 2B model speedRating=9, # Fast due to small 2B model
qualityRating=8, # Good for document understanding qualityRating=8, # Good for document understanding
functionCall=self.callAiVision, functionCall=self.callAiVision,
@ -316,92 +300,10 @@ class AiPrivateLlm(BaseConnectorAi):
(OperationTypeEnum.NEUTRALIZATION_IMAGE, 9), (OperationTypeEnum.NEUTRALIZATION_IMAGE, 9),
), ),
version="granite3.2-vision", version="granite3.2-vision",
calculatepriceCHF=_calcPrivatePriceCHF calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: PRICE_VISION_PER_CALL
), ),
"ollamaModel": "granite3.2-vision" "ollamaModel": "granite3.2-vision"
}, },
# --- Next-gen models (auto-activated when available in Ollama) ---
# Reasoning Model (deepseek-r1:70b — chain-of-thought, math, logic)
{
"model": AiModel(
name="poweron-text-reasoning",
displayName="PowerOn Reasoning",
connectorType="privatellm",
apiUrl=f"{self.baseUrl}/api/analyze",
temperature=0.1,
maxTokens=8192,
contextLength=65536,
costPer1kTokensInput=PRICE_INPUT_PER_1K,
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K,
speedRating=5,
qualityRating=10,
functionCall=self.callAiText,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 10),
(OperationTypeEnum.DATA_ANALYSE, 10),
(OperationTypeEnum.DATA_GENERATE, 9),
(OperationTypeEnum.DATA_EXTRACT, 9),
(OperationTypeEnum.NEUTRALIZATION_TEXT, 10),
(OperationTypeEnum.AGENT, 9),
),
version="deepseek-r1:70b",
calculatepriceCHF=_calcPrivatePriceCHF
),
"ollamaModel": "deepseek-r1:70b"
},
# Vision Multimodal (llama4:scout — native vision, 10M context)
{
"model": AiModel(
name="poweron-vision-multimodal",
displayName="PowerOn Vision Multimodal",
connectorType="privatellm",
apiUrl=f"{self.baseUrl}/api/analyze",
temperature=0.2,
maxTokens=4096,
contextLength=131072,
costPer1kTokensInput=PRICE_INPUT_PER_1K,
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K,
speedRating=7,
qualityRating=10,
functionCall=self.callAiVision,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.IMAGE_ANALYSE, 10),
(OperationTypeEnum.NEUTRALIZATION_IMAGE, 10),
),
version="llama4:scout",
calculatepriceCHF=_calcPrivatePriceCHF
),
"ollamaModel": "llama4:scout"
},
# Local Embedding (nomic-embed-text — replaces OpenAI text-embedding-3-small)
{
"model": AiModel(
name="poweron-embed",
displayName="PowerOn Embedding",
connectorType="privatellm",
apiUrl=f"{self.baseUrl}/v1/embeddings",
temperature=0.0,
maxTokens=0,
contextLength=8192,
costPer1kTokensInput=PRICE_EMBED_PER_1K,
costPer1kTokensOutput=0.0,
speedRating=10,
qualityRating=8,
functionCall=self.callAiText,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.EMBEDDING, 9),
),
version="nomic-embed-text",
calculatepriceCHF=_calcPrivateEmbedPriceCHF
),
"ollamaModel": "nomic-embed-text"
},
] ]
# Filter models by Ollama availability # Filter models by Ollama availability
@ -416,7 +318,7 @@ class AiPrivateLlm(BaseConnectorAi):
unavailableModels.append(modelDef["model"].name) unavailableModels.append(modelDef["model"].name)
if unavailableModels: if unavailableModels:
logger.info( logger.warning(
f"Private-LLM: {len(unavailableModels)} models not available in Ollama: {', '.join(unavailableModels)}. " f"Private-LLM: {len(unavailableModels)} models not available in Ollama: {', '.join(unavailableModels)}. "
f"Install with: ollama pull <model-name>" f"Install with: ollama pull <model-name>"
) )

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""Tavily web search class. """Tavily web search class.
""" """

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Authentication and authorization modules for routes and services. Authentication and authorization modules for routes and services.

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Authentication module for backend API. Authentication module for backend API.
@ -437,7 +437,7 @@ def requireSysAdmin(currentUser: User = Depends(getCurrentUser)) -> User:
# Audit for all SysAdmin actions # Audit for all SysAdmin actions
try: try:
from modules.dbHelpers.auditLogger import audit_logger from modules.shared.auditLogger import audit_logger
audit_logger.logSecurityEvent( audit_logger.logSecurityEvent(
userId=str(currentUser.id), userId=str(currentUser.id),
mandateId="system", mandateId="system",
@ -483,7 +483,7 @@ def requirePlatformAdmin(currentUser: User = Depends(getCurrentUser)) -> User:
# Audit for all Platform-Admin actions # Audit for all Platform-Admin actions
try: try:
from modules.dbHelpers.auditLogger import audit_logger from modules.shared.auditLogger import audit_logger
audit_logger.logSecurityEvent( audit_logger.logSecurityEvent(
userId=str(currentUser.id), userId=str(currentUser.id),
mandateId="system", mandateId="system",

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
CSRF Protection Middleware for PowerOn Gateway CSRF Protection Middleware for PowerOn Gateway

View file

@ -1,11 +1,11 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
JWT Service JWT Service
Centralizes local JWT creation and cookie helpers. Centralizes local JWT creation and cookie helpers.
""" """
from datetime import datetime, timedelta from datetime import timedelta
from typing import Optional, Tuple from typing import Optional, Tuple
from fastapi import Response from fastapi import Response
from jose import jwt from jose import jwt
@ -19,28 +19,10 @@ ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY")) ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY"))
REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7")) REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7"))
def _cookiePolicy() -> Tuple[bool, str, str]: # Cookie security settings - use secure cookies based on whether API uses HTTPS
""" # Cookies must have secure=True on HTTPS sites, secure=False on HTTP sites
Return (useSecure, samesiteStarlette, samesiteSetCookieHeader). APP_API_URL = APP_CONFIG.get("APP_API_URL", "http://localhost:8000")
USE_SECURE_COOKIES = APP_API_URL.startswith("https://") if APP_API_URL else False
Evaluated on each Set-Cookie so policy is not frozen at module import (config refresh / load order).
Cross-origin SPA + API: SameSite=None and Secure=True so credentialed fetch sends cookies.
HTTP dev: Lax + Secure=False.
APP_COOKIE_SECURE: explicit true/false (1/0, yes/no) overrides the APP_API_URL heuristic.
"""
explicit = (APP_CONFIG.get("APP_COOKIE_SECURE") or "").strip().lower()
if explicit in ("1", "true", "yes"):
useSecure = True
elif explicit in ("0", "false", "no"):
useSecure = False
else:
apiUrl = (APP_CONFIG.get("APP_API_URL") or "").strip()
useSecure = apiUrl.startswith("https://")
samesite = "none" if useSecure else "lax"
samesiteHeader = "None" if useSecure else "Lax"
return useSecure, samesite, samesiteHeader
def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> Tuple[str, "datetime"]: def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> Tuple[str, "datetime"]:
@ -72,14 +54,13 @@ def createRefreshToken(data: dict) -> Tuple[str, "datetime"]:
def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[timedelta] = None) -> None: def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[timedelta] = None) -> None:
"""Set access token as httpOnly cookie.""" """Set access token as httpOnly cookie."""
useSecure, samesite, _ = _cookiePolicy()
maxAge = int(expiresDelta.total_seconds()) if expiresDelta else ACCESS_TOKEN_EXPIRE_MINUTES * 60 maxAge = int(expiresDelta.total_seconds()) if expiresDelta else ACCESS_TOKEN_EXPIRE_MINUTES * 60
response.set_cookie( response.set_cookie(
key="auth_token", key="auth_token",
value=token, value=token,
httponly=True, httponly=True,
secure=useSecure, secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
samesite=samesite, samesite="strict",
path="/", path="/",
max_age=maxAge max_age=maxAge
) )
@ -87,13 +68,12 @@ def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[
def setRefreshTokenCookie(response: Response, token: str) -> None: def setRefreshTokenCookie(response: Response, token: str) -> None:
"""Set refresh token as httpOnly cookie.""" """Set refresh token as httpOnly cookie."""
useSecure, samesite, _ = _cookiePolicy()
response.set_cookie( response.set_cookie(
key="refresh_token", key="refresh_token",
value=token, value=token,
httponly=True, httponly=True,
secure=useSecure, secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
samesite=samesite, samesite="strict",
path="/", path="/",
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60 max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
) )
@ -104,23 +84,17 @@ def clearAccessTokenCookie(response: Response) -> None:
Clear access token cookie by setting it to expire immediately. Clear access token cookie by setting it to expire immediately.
Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility. Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility.
""" """
useSecure, samesite, samesiteHeader = _cookiePolicy() # Build secure flag based on environment
secure_flag = "; Secure" if useSecure else "" secure_flag = "; Secure" if USE_SECURE_COOKIES else ""
# Primary method: Raw Set-Cookie header for guaranteed deletion # Primary method: Raw Set-Cookie header for guaranteed deletion
response.headers.append( response.headers.append(
"Set-Cookie", "Set-Cookie",
f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={samesiteHeader}" f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite=Strict"
)
# Fallback: Also use FastAPI's built-in method (match SameSite/Secure for invalidation)
response.delete_cookie(
key="auth_token",
path="/",
secure=useSecure,
httponly=True,
samesite=samesite,
) )
# Fallback: Also use FastAPI's built-in method
response.delete_cookie(key="auth_token", path="/")
def clearRefreshTokenCookie(response: Response) -> None: def clearRefreshTokenCookie(response: Response) -> None:
@ -128,22 +102,16 @@ def clearRefreshTokenCookie(response: Response) -> None:
Clear refresh token cookie by setting it to expire immediately. Clear refresh token cookie by setting it to expire immediately.
Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility. Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility.
""" """
useSecure, samesite, samesiteHeader = _cookiePolicy() # Build secure flag based on environment
secure_flag = "; Secure" if useSecure else "" secure_flag = "; Secure" if USE_SECURE_COOKIES else ""
# Primary method: Raw Set-Cookie header for guaranteed deletion # Primary method: Raw Set-Cookie header for guaranteed deletion
response.headers.append( response.headers.append(
"Set-Cookie", "Set-Cookie",
f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={samesiteHeader}" f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite=Strict"
)
# Fallback: Also use FastAPI's built-in method (match SameSite/Secure for invalidation)
response.delete_cookie(
key="refresh_token",
path="/",
secure=useSecure,
httponly=True,
samesite=samesite,
) )
# Fallback: Also use FastAPI's built-in method
response.delete_cookie(key="refresh_token", path="/")

View file

@ -1,132 +0,0 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""
MFA (Multi-Factor Authentication) Service.
TOTP-based MFA using pyotp. Secrets are encrypted at rest via
encryptValue/decryptValue from the configuration module.
MFA obligation is resolved by three OR-linked rules:
1. Any mandate the user belongs to has ``mfaRequired=True``.
2. User is sysAdmin OR platformAdmin AND config key ``MFA_REQUIRE_ADMINS``
is truthy.
3. User has opted in (``mfaEnabled=True`` without any mandate/admin rule).
"""
import logging
from typing import Optional
import pyotp
from modules.shared.configuration import APP_CONFIG, encryptValue, decryptValue
logger = logging.getLogger(__name__)
_MFA_DIGITS = 6
_MFA_INTERVAL = 30
_MFA_VALID_WINDOW = 1
def getMfaIssuer() -> str:
"""Build the TOTP issuer name, e.g. 'PowerOn' or 'PowerOn (Dev)'."""
envType = (APP_CONFIG.get("APP_ENV_TYPE") or "").strip().lower()
if envType in ("prod", ""):
return "PowerOn"
return f"PowerOn ({envType.upper()})"
def _generateSecret() -> str:
"""Generate a fresh base32-encoded TOTP secret."""
return pyotp.random_base32()
def _encryptSecret(plainSecret: str, userId: str = "system") -> str:
return encryptValue(plainSecret, userId=userId, keyName="mfa_secret")
def decryptSecret(encryptedSecret: str, userId: str = "system") -> str:
return decryptValue(encryptedSecret, userId=userId, keyName="mfa_secret")
def buildTotp(plainSecret: str) -> pyotp.TOTP:
return pyotp.TOTP(plainSecret, digits=_MFA_DIGITS, interval=_MFA_INTERVAL)
def generateSetup(userId: str, username: str) -> dict:
"""Start MFA enrolment: return secret + provisioning URI (for QR code).
Returns dict with keys ``secret`` (encrypted for DB storage) and
``provisioningUri`` (otpauth:// URI the frontend renders as QR).
The plaintext secret is NOT returned -- the URI already contains it.
"""
plain = _generateSecret()
encrypted = _encryptSecret(plain, userId=userId)
totp = buildTotp(plain)
uri = totp.provisioning_uri(name=username, issuer_name=getMfaIssuer())
return {
"encryptedSecret": encrypted,
"provisioningUri": uri,
}
def confirmSetup(encryptedSecret: str, code: str, userId: str = "system") -> bool:
"""Verify a TOTP code against an encrypted secret (enrolment confirmation)."""
try:
plain = decryptSecret(encryptedSecret, userId=userId)
totp = buildTotp(plain)
return totp.verify(code, valid_window=_MFA_VALID_WINDOW)
except Exception:
logger.exception("MFA confirmSetup failed for userId=%s", userId)
return False
def verifyCode(encryptedSecret: str, code: str, userId: str = "system") -> bool:
"""Verify a TOTP code during login."""
try:
plain = decryptSecret(encryptedSecret, userId=userId)
totp = buildTotp(plain)
return totp.verify(code, valid_window=_MFA_VALID_WINDOW)
except Exception:
logger.exception("MFA verifyCode failed for userId=%s", userId)
return False
def _isMfaRequireAdminsEnabled() -> bool:
"""Read ``MFA_REQUIRE_ADMINS`` from config / env."""
raw = (APP_CONFIG.get("MFA_REQUIRE_ADMINS") or "").strip().lower()
return raw in ("1", "true", "yes")
def isMfaRequired(user, userMandates=None, mandates=None) -> bool:
"""Resolve whether MFA is mandatory for *user*.
Rules (OR):
1. At least one of the user's mandates has ``mfaRequired=True``.
2. User is sysAdmin or platformAdmin AND ``MFA_REQUIRE_ADMINS`` config
key is truthy.
3. User already opted in (``mfaEnabled=True``).
Parameters
----------
user : User | UserInDB
The user object.
userMandates : list | None
List of UserMandate records for the user (each has ``mandateId``).
mandates : list | None
List of Mandate objects the user has access to. If provided directly
this avoids a second lookup.
"""
if getattr(user, "mfaEnabled", False):
return True
isSys = getattr(user, "isSysAdmin", False)
isPlat = getattr(user, "isPlatformAdmin", False)
if (isSys or isPlat) and _isMfaRequireAdminsEnabled():
return True
if mandates:
for m in mandates:
if getattr(m, "mfaRequired", False):
return True
return False

View file

@ -1,101 +0,0 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""
Short-lived signed tickets for OAuth data-connection popups.
The UI authenticates API calls with a Bearer token in localStorage, but
``window.open(authUrl)`` cannot send that header. Cross-origin httpOnly cookies
are unreliable in cross-origin setups (UI and API on different subdomains).
Login popups work without a session because ``/auth/login`` is public; connect
popups hit ``/auth/connect``, which used to require ``getCurrentUser``.
Flow: POST ``/api/connections/{id}/connect`` (Bearer-authenticated) issues a
ticket; the popup opens ``/auth/connect?connectTicket=...`` which validates the
ticket instead of cookies.
"""
import time
from typing import Any, Dict, Tuple
from fastapi import HTTPException, status
from jose import JWTError, jwt as jose_jwt
from modules.auth.jwtService import ALGORITHM, SECRET_KEY
from modules.datamodels.datamodelUam import AuthAuthority, User, UserConnection
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.shared.i18nRegistry import apiRouteContext
_msg = apiRouteContext("oauthConnectTicket")
_CONNECT_TICKET_TTL_SEC = 600
def issue_connect_ticket(flow: str, connection_id: str, user_id: str) -> str:
"""Issue a short-lived JWT for starting a data-connection OAuth popup."""
body = {
"flow": flow,
"connectionId": connection_id,
"userId": str(user_id),
"exp": int(time.time()) + _CONNECT_TICKET_TTL_SEC,
}
return jose_jwt.encode(body, SECRET_KEY, algorithm=ALGORITHM)
def parse_connect_ticket(ticket: str, expected_flow: str) -> Dict[str, Any]:
"""Validate connect ticket signature, expiry, and flow."""
try:
data = jose_jwt.decode(ticket, SECRET_KEY, algorithms=[ALGORITHM])
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=_msg("Invalid or expired connect ticket"),
) from e
if data.get("flow") != expected_flow:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=_msg("Invalid connect ticket flow"),
)
connection_id = data.get("connectionId")
user_id = data.get("userId")
if not connection_id or not user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=_msg("Incomplete connect ticket"),
)
return data
def resolve_connect_context(
connect_ticket: str,
connection_id: str,
expected_flow: str,
authority: AuthAuthority,
) -> Tuple[User, UserConnection]:
"""Validate ticket and return the user + connection for OAuth redirect."""
state = parse_connect_ticket(connect_ticket, expected_flow)
if state.get("connectionId") != connection_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=_msg("Connection ID does not match connect ticket"),
)
root = getRootInterface()
user = root.getUser(state["userId"])
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=_msg("User not found"),
)
interface = getInterface(user)
connection = None
for conn in interface.getUserConnections(user.id):
if conn.id == connection_id and conn.authority == authority:
connection = conn
break
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=_msg("Connection not found"),
)
return user, connection

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""OAuth scope sets for split Auth- vs Data-apps (Google / Microsoft).""" """OAuth scope sets for split Auth- vs Data-apps (Google / Microsoft)."""
@ -9,15 +9,13 @@ googleAuthScopes = [
"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.profile",
] ]
# Google — Data app (Gmail + Drive + Calendar + Contacts + identity for token responses) # Google — Data app (Gmail + Drive + identity for token responses)
googleDataScopes = [ googleDataScopes = [
"openid", "openid",
"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/drive.readonly", "https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/contacts.readonly",
] ]
# Microsoft — Auth app: Graph profile only (MSAL adds openid, profile, offline_access, …) # Microsoft — Auth app: Graph profile only (MSAL adds openid, profile, offline_access, …)
@ -36,18 +34,9 @@ msftDataScopes = [
"OnlineMeetings.Read", "OnlineMeetings.Read",
"Chat.ReadWrite", "Chat.ReadWrite",
"ChatMessage.Send", "ChatMessage.Send",
"Calendars.Read",
"Contacts.Read",
] ]
def msftDataScopesForRefresh() -> str: def msftDataScopesForRefresh() -> str:
"""Space-separated scope string identical to authorization request (Token v2 refresh).""" """Space-separated scope string identical to authorization request (Token v2 refresh)."""
return " ".join(msftDataScopes) return " ".join(msftDataScopes)
# Infomaniak intentionally has no OAuth scope set: the kDrive + Mail data APIs
# are only reachable with manually issued Personal Access Tokens (see
# wiki/d-guides/infomaniak-token-setup.md). The OAuth /authorize endpoint at
# login.infomaniak.com only accepts identity scopes (openid/profile/email/phone)
# and does not return tokens that work against /1/* data routes.

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Token Manager Service Token Manager Service
@ -29,7 +29,6 @@ class TokenManager:
# Google Data-app OAuth # Google Data-app OAuth
self.google_client_id = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_ID") self.google_client_id = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_ID")
self.google_client_secret = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_SECRET") self.google_client_secret = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_SECRET")
def refreshMicrosoftToken(self, refreshToken: str, userId: str, oldToken: Token) -> Optional[Token]: def refreshMicrosoftToken(self, refreshToken: str, userId: str, oldToken: Token) -> Optional[Token]:
"""Refresh Microsoft OAuth token using refresh token""" """Refresh Microsoft OAuth token using refresh token"""
@ -162,7 +161,7 @@ class TokenManager:
except Exception as e: except Exception as e:
logger.error(f"Error refreshing Google token: {str(e)}") logger.error(f"Error refreshing Google token: {str(e)}")
return None return None
def refreshToken(self, oldToken: Token) -> Optional[Token]: def refreshToken(self, oldToken: Token) -> Optional[Token]:
"""Refresh an expired token using the appropriate OAuth service""" """Refresh an expired token using the appropriate OAuth service"""
try: try:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Token Refresh Middleware for PowerOn Gateway Token Refresh Middleware for PowerOn Gateway

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Token Refresh Service for PowerOn Gateway Token Refresh Service for PowerOn Gateway
@ -12,7 +12,7 @@ import logging
from typing import Dict, Any from typing import Dict, Any
from modules.datamodels.datamodelUam import UserConnection, AuthAuthority from modules.datamodels.datamodelUam import UserConnection, AuthAuthority
from modules.shared.timeUtils import getUtcTimestamp from modules.shared.timeUtils import getUtcTimestamp
from modules.dbHelpers.auditLogger import audit_logger from modules.shared.auditLogger import audit_logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -143,7 +143,7 @@ class TokenRefreshService:
except Exception as e: except Exception as e:
logger.error(f"Error refreshing Microsoft token for connection {connection.id}: {str(e)}") logger.error(f"Error refreshing Microsoft token for connection {connection.id}: {str(e)}")
return False return False
async def refresh_expired_tokens(self, user_id: str) -> Dict[str, Any]: async def refresh_expired_tokens(self, user_id: str) -> Dict[str, Any]:
""" """
Refresh expired OAuth tokens for a user Refresh expired OAuth tokens for a user

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Azure Communication Services Email Connector Azure Communication Services Email Connector

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Twilio SMS Connector Twilio SMS Connector

View file

@ -1,5 +1,3 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
""" """
ÖREB WFS Connector ÖREB WFS Connector

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Preprocessor connector for executing SQL queries via HTTP API. Preprocessor connector for executing SQL queries via HTTP API.

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""Abstract base classes for the Provider-Connector architecture (1:n). """Abstract base classes for the Provider-Connector architecture (1:n).
@ -24,21 +24,8 @@ class ServiceAdapter(ABC):
"""Standardized operations for a single service of a provider.""" """Standardized operations for a single service of a provider."""
@abstractmethod @abstractmethod
async def browse( async def browse(self, path: str, filter: Optional[str] = None) -> list:
self, """List items (files/folders) at the given path."""
path: str,
filter: Optional[str] = None,
limit: Optional[int] = None,
) -> list:
"""List items (files/folders) at the given path.
``limit`` is an optional upper bound for the number of returned entries.
Adapters that talk to paginated APIs should keep paging until either
the API is exhausted OR ``limit`` is reached. ``None`` means "use the
adapter's sensible default" (NOT "unlimited") so an over-eager caller
cannot accidentally pull millions of records. Adapters that have no
pagination (single page result) may ignore this parameter.
"""
... ...
@abstractmethod @abstractmethod
@ -52,16 +39,8 @@ class ServiceAdapter(ABC):
... ...
@abstractmethod @abstractmethod
async def search( async def search(self, query: str, path: Optional[str] = None) -> list:
self, """Search for items matching the query."""
query: str,
path: Optional[str] = None,
limit: Optional[int] = None,
) -> list:
"""Search for items matching the query.
See :meth:`browse` for the semantics of ``limit``.
"""
... ...

View file

@ -1,940 +0,0 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""Google ProviderConnector -- Drive and Gmail via Google OAuth."""
import asyncio
import base64
import logging
import re
import urllib.parse
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
import aiohttp
from modules.connectors.connectorProviderBase import ProviderConnector, ServiceAdapter, DownloadResult
from modules.shared.httpResilience import ResilientHttp
from modules.datamodels.datamodelDataSource import ExternalEntry
logger = logging.getLogger(__name__)
_http = ResilientHttp("Google", maxConcurrent=8, defaultTimeoutS=20)
_DRIVE_BASE = "https://www.googleapis.com/drive/v3"
_GMAIL_BASE = "https://gmail.googleapis.com/gmail/v1"
_CALENDAR_BASE = "https://www.googleapis.com/calendar/v3"
_PEOPLE_BASE = "https://people.googleapis.com/v1"
def _parseGoogleDateRange(text: Optional[str]) -> tuple:
"""Parse a date range from a filter/query string for Calendar timeMin/timeMax.
Supports two ISO dates, a single ISO date (~31 day window) or a YYYY-MM
month pattern. Returns RFC3339 UTC strings (timeMin, timeMax) or (None, None).
"""
if not text:
return (None, None)
def _toRfc3339(value: str) -> str:
value = value.strip().rstrip("Z")
if "T" not in value:
value = f"{value}T00:00:00"
return f"{value}Z"
isoMatch = re.findall(r'\d{4}-\d{2}-\d{2}(?:T[\d:]+)?', text)
if len(isoMatch) >= 2:
return (_toRfc3339(isoMatch[0]), _toRfc3339(isoMatch[1]))
if len(isoMatch) == 1:
try:
dt = datetime.fromisoformat(isoMatch[0])
return (_toRfc3339(isoMatch[0]), _toRfc3339((dt + timedelta(days=31)).strftime('%Y-%m-%dT00:00:00')))
except ValueError:
pass
monthMatch = re.match(r'^(\d{4})-(\d{2})$', text.strip())
if monthMatch:
year, month = int(monthMatch.group(1)), int(monthMatch.group(2))
start = f"{year}-{month:02d}-01T00:00:00"
end = f"{year + 1}-01-01T00:00:00" if month == 12 else f"{year}-{month + 1:02d}-01T00:00:00"
return (_toRfc3339(start), _toRfc3339(end))
return (None, None)
async def googleGet(token: str, url: str) -> Dict[str, Any]:
headers = {"Authorization": f"Bearer {token}"}
return await _http.getJson(url, headers=headers)
def _raiseGoogleError(result: Dict[str, Any], ctx: str) -> None:
"""Raise a clear error for a failed Google API response.
Browse/search must NOT swallow API failures into an empty result list, which
masks a real error as 'empty'. Callers wrap these in try/except.
"""
err = result.get("error") if isinstance(result, dict) else None
logger.warning("Google error (%s): %s", ctx, err or result)
raise RuntimeError(f"Google error ({ctx}): {err or result}")
class DriveAdapter(ServiceAdapter):
"""Google Drive ServiceAdapter -- browse files and folders."""
def __init__(self, accessToken: str):
self._token = accessToken
async def browse(
self,
path: str,
filter: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
folderId = (path or "").strip("/") or "root"
query = f"'{folderId}' in parents and trashed=false"
fields = "files(id,name,mimeType,size,modifiedTime,parents)"
pageSize = max(1, min(int(limit or 100), 1000))
url = f"{_DRIVE_BASE}/files?q={query}&fields={fields}&pageSize={pageSize}&orderBy=folder,name"
result = await googleGet(self._token, url)
if "error" in result:
_raiseGoogleError(result, "Google Drive browse")
entries = []
for f in result.get("files", []):
isFolder = f.get("mimeType") == "application/vnd.google-apps.folder"
entries.append(ExternalEntry(
name=f.get("name", ""),
path=f"/{f.get('id', '')}",
isFolder=isFolder,
size=int(f.get("size", 0)) if f.get("size") else None,
mimeType=f.get("mimeType") if not isFolder else None,
metadata={"id": f.get("id"), "modifiedTime": f.get("modifiedTime")},
))
return entries
_EXPORT_MIME_MAP = {
"application/vnd.google-apps.document": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.google-apps.spreadsheet": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.google-apps.presentation": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.google-apps.drawing": "application/pdf",
}
async def download(self, path: str) -> bytes:
fileId = (path or "").strip("/")
if not fileId:
return b""
headers = {"Authorization": f"Bearer {self._token}"}
dlTimeout = aiohttp.ClientTimeout(total=60)
try:
url = f"{_DRIVE_BASE}/files/{fileId}?alt=media"
data = await _http.getBytes(url, headers=headers, timeout=dlTimeout)
if data is not None:
return data
logger.debug(f"Google Drive direct download returned None for {fileId}")
metaUrl = f"{_DRIVE_BASE}/files/{fileId}?fields=mimeType,name"
meta = await _http.getJson(metaUrl, headers=headers)
if "error" in meta:
logger.warning(f"Google Drive metadata fetch failed for {fileId}: {meta['error']}")
return b""
fileMime = meta.get("mimeType", "")
fileName = meta.get("name", fileId)
exportMime = self._EXPORT_MIME_MAP.get(fileMime)
if not exportMime:
logger.warning(f"Google Drive: unsupported mimeType '{fileMime}' for file '{fileName}' ({fileId})")
return b""
exportUrl = f"{_DRIVE_BASE}/files/{fileId}/export?mimeType={exportMime}"
logger.info(f"Google Drive: exporting '{fileName}' as {exportMime}")
exported = await _http.getBytes(exportUrl, headers=headers, timeout=dlTimeout)
if exported is not None:
return exported
logger.warning(f"Google Drive export failed for '{fileName}'")
except Exception as e:
logger.error(f"Google Drive download failed for {fileId}: {e}")
return b""
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "Google Drive upload not yet implemented"}
async def search(
self,
query: str,
path: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
safeQuery = query.replace("\\", "\\\\").replace("'", "\\'")
folderId = (path or "").strip("/")
# `fullText contains` matches file name AND content (and some metadata),
# which is what users expect from a search -- not just the file name.
qParts = [f"fullText contains '{safeQuery}'", "trashed=false"]
if folderId:
qParts.append(f"'{folderId}' in parents")
qStr = " and ".join(qParts)
effectiveLimit = max(1, int(limit)) if limit is not None else None
pageSize = min(effectiveLimit or 100, 1000)
logger.debug(f"Google Drive search: q={qStr}")
entries: List[ExternalEntry] = []
pageToken: Optional[str] = None
hardCap = effectiveLimit or 1000
while len(entries) < hardCap:
params = {
"q": qStr,
"fields": "nextPageToken,files(id,name,mimeType,size,modifiedTime)",
"pageSize": str(pageSize),
}
if pageToken:
params["pageToken"] = pageToken
url = f"{_DRIVE_BASE}/files?{urllib.parse.urlencode(params)}"
result = await googleGet(self._token, url)
if "error" in result:
if not entries:
_raiseGoogleError(result, "Google Drive search")
break
for f in result.get("files", []):
entries.append(ExternalEntry(
name=f.get("name", ""),
path=f"/{f.get('id', '')}",
isFolder=f.get("mimeType") == "application/vnd.google-apps.folder",
size=int(f.get("size", 0)) if f.get("size") else None,
mimeType=f.get("mimeType"),
metadata={"id": f.get("id"), "modifiedTime": f.get("modifiedTime")},
))
if len(entries) >= hardCap:
break
pageToken = result.get("nextPageToken")
if not pageToken:
break
if effectiveLimit is not None:
entries = entries[:effectiveLimit]
return entries
class GmailAdapter(ServiceAdapter):
"""Gmail ServiceAdapter -- browse labels and messages."""
def __init__(self, accessToken: str):
self._token = accessToken
_DEFAULT_MESSAGE_LIMIT = 100
_MAX_MESSAGE_LIMIT = 1000
_METADATA_FETCH_CAP = 200
async def browse(
self,
path: str,
filter: Optional[str] = None,
limit: Optional[int] = None,
) -> list:
cleanPath = (path or "").strip("/")
if not cleanPath:
url = f"{_GMAIL_BASE}/users/me/labels"
result = await googleGet(self._token, url)
if "error" in result:
_raiseGoogleError(result, "Gmail labels")
_SYSTEM_LABELS = {"INBOX", "SENT", "DRAFT", "TRASH", "SPAM", "STARRED", "IMPORTANT"}
labels = []
for lbl in result.get("labels", []):
labelId = lbl.get("id", "")
labelName = lbl.get("name", labelId)
if lbl.get("type") == "system" and labelId not in _SYSTEM_LABELS:
continue
labels.append(ExternalEntry(
name=labelName,
path=f"/{labelId}",
isFolder=True,
metadata={"id": labelId, "type": lbl.get("type", "")},
))
labels.sort(key=lambda e: (0 if e.metadata.get("type") == "system" else 1, e.name))
return labels
effectiveLimit = self._DEFAULT_MESSAGE_LIMIT if limit is None else max(1, min(int(limit), self._MAX_MESSAGE_LIMIT))
labelId = await self._resolveLabelId(cleanPath)
if not labelId:
raise ValueError(
f"Gmail label not found: '{cleanPath}'. Browse the mailbox root ('/') "
f"to list available labels."
)
msgIds, totalEstimate = await self._listMessageIds(
params={"labelIds": labelId}, limit=effectiveLimit,
)
entries = await self._fetchMessageEntries(
msgIds[:self._METADATA_FETCH_CAP], labelPath=labelId,
)
if totalEstimate and totalEstimate > len(msgIds):
entries.append(ExternalEntry(
name=f"(~{totalEstimate} total messages estimated, {len(msgIds)} listed)",
path=f"/{labelId}/_count", isFolder=False,
metadata={"totalEstimate": totalEstimate, "listed": len(msgIds)},
))
elif len(msgIds) > self._METADATA_FETCH_CAP:
entries.append(ExternalEntry(
name=f"({len(msgIds)} messages listed, metadata shown for first {self._METADATA_FETCH_CAP})",
path=f"/{labelId}/_count", isFolder=False,
metadata={"listed": len(msgIds), "metadataShown": self._METADATA_FETCH_CAP},
))
return entries
async def _resolveLabelId(self, ref: str) -> Optional[str]:
"""Resolve a Gmail label reference (display name / system name / id) to a
label id. Returns None if nothing matches so the caller can raise a clear
error instead of querying with an invalid label."""
if not ref:
return None
r = ref.strip()
result = await googleGet(self._token, f"{_GMAIL_BASE}/users/me/labels")
if "error" in result:
_raiseGoogleError(result, "Gmail labels")
labels = result.get("labels", [])
# 1) exact id match (already-resolved id passes through)
for lbl in labels:
if lbl.get("id") == r:
return r
# 2) case-insensitive display-name match
for lbl in labels:
if (lbl.get("name") or "").strip().lower() == r.lower():
return lbl.get("id")
# 3) system label by uppercased name (INBOX, SENT, ...)
up = r.upper()
for lbl in labels:
if lbl.get("id") == up:
return up
return None
async def _listMessageIds(
self, params: Dict[str, str], limit: int,
) -> tuple:
"""Page through ``messages.list`` and return (msgIds, totalEstimate).
Gmail's ``maxResults`` caps at 500 per page, so we follow
``nextPageToken`` until we have ``limit`` ids or there are no more pages.
``resultSizeEstimate`` from the first page gives the agent an approximate
total count without having to download every message.
"""
msgIds: List[str] = []
totalEstimate: Optional[int] = None
pageToken: Optional[str] = None
pageSize = min(limit, 500)
while len(msgIds) < limit:
p = {**params, "maxResults": str(pageSize)}
if pageToken:
p["pageToken"] = pageToken
url = f"{_GMAIL_BASE}/users/me/messages?{urllib.parse.urlencode(p)}"
result = await googleGet(self._token, url)
if "error" in result:
if not msgIds:
_raiseGoogleError(result, "Gmail list messages")
break
if totalEstimate is None:
totalEstimate = result.get("resultSizeEstimate")
for m in result.get("messages", []):
mid = m.get("id", "")
if mid:
msgIds.append(mid)
if len(msgIds) >= limit:
break
pageToken = result.get("nextPageToken")
if not pageToken:
break
return msgIds, totalEstimate
async def _fetchMessageEntries(self, msgIds: List[str], labelPath: str = "") -> List[ExternalEntry]:
"""Resolve a list of Gmail message ids into ExternalEntries with
Subject/From/Date metadata. Detail fetches run concurrently to avoid a
slow sequential N+1 round-trip per message."""
if not msgIds:
return []
pathPrefix = f"/{labelPath}" if labelPath else ""
async def _one(msgId: str) -> ExternalEntry:
detailUrl = (
f"{_GMAIL_BASE}/users/me/messages/{msgId}"
f"?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date"
)
detail = await googleGet(self._token, detailUrl)
if "error" in detail:
return ExternalEntry(name=f"Message {msgId}", path=f"{pathPrefix}/{msgId}", isFolder=False,
metadata={"id": msgId})
headers = {h.get("name", ""): h.get("value", "") for h in detail.get("payload", {}).get("headers", [])}
return ExternalEntry(
name=headers.get("Subject", "(no subject)"),
path=f"{pathPrefix}/{msgId}",
isFolder=False,
metadata={
"id": msgId,
"from": headers.get("From", ""),
"date": headers.get("Date", ""),
"snippet": detail.get("snippet", ""),
},
)
return list(await asyncio.gather(*[_one(mid) for mid in msgIds]))
async def download(self, path: str) -> DownloadResult:
"""Download a Gmail message as RFC 822 EML via format=raw."""
cleanPath = (path or "").strip("/")
msgId = cleanPath.split("/")[-1] if cleanPath else ""
if not msgId:
return DownloadResult()
url = f"{_GMAIL_BASE}/users/me/messages/{msgId}?format=raw"
result = await googleGet(self._token, url)
if "error" in result:
return DownloadResult()
rawB64 = result.get("raw", "")
if not rawB64:
return DownloadResult()
emlBytes = base64.urlsafe_b64decode(rawB64)
metaUrl = f"{_GMAIL_BASE}/users/me/messages/{msgId}?format=metadata&metadataHeaders=Subject"
meta = await googleGet(self._token, metaUrl)
subject = msgId
if "error" not in meta:
for h in meta.get("payload", {}).get("headers", []):
if h.get("name", "").lower() == "subject":
subject = h.get("value", msgId)
break
safeName = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", subject)[:80].strip(". ") or "email"
return DownloadResult(
data=emlBytes,
fileName=f"{safeName}.eml",
mimeType="message/rfc822",
)
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "Gmail upload not applicable"}
async def search(
self,
query: str,
path: Optional[str] = None,
limit: Optional[int] = None,
) -> list:
effectiveLimit = self._DEFAULT_MESSAGE_LIMIT if limit is None else max(1, min(int(limit), self._MAX_MESSAGE_LIMIT))
params: Dict[str, str] = {"q": query}
labelPath = (path or "").strip("/")
if labelPath:
labelId = await self._resolveLabelId(labelPath)
if not labelId:
raise ValueError(
f"Gmail label not found: '{labelPath}'. Browse the mailbox root ('/') "
f"to list available labels, or search without a label scope."
)
labelPath = labelId
params["labelIds"] = labelId
msgIds, totalEstimate = await self._listMessageIds(params, limit=effectiveLimit)
entries = await self._fetchMessageEntries(
msgIds[:self._METADATA_FETCH_CAP], labelPath=labelPath,
)
if totalEstimate and totalEstimate > len(msgIds):
entries.append(ExternalEntry(
name=f"(~{totalEstimate} total results estimated, {len(msgIds)} listed)",
path=f"/{labelPath or 'search'}/_count", isFolder=False,
metadata={"totalEstimate": totalEstimate, "listed": len(msgIds)},
))
elif len(msgIds) > self._METADATA_FETCH_CAP:
entries.append(ExternalEntry(
name=f"({len(msgIds)} results listed, metadata shown for first {self._METADATA_FETCH_CAP})",
path=f"/{labelPath or 'search'}/_count", isFolder=False,
metadata={"listed": len(msgIds), "metadataShown": self._METADATA_FETCH_CAP},
))
return entries
class CalendarAdapter(ServiceAdapter):
"""Google Calendar ServiceAdapter -- browse calendars, list events, .ics download.
Path conventions:
``""`` / ``"/"`` -> list calendars from ``calendarList``
``"/<calendarId>"`` -> list upcoming events in that calendar
``"/<calendarId>/<eventId>"`` -> reserved for future event detail browse
"""
_DEFAULT_EVENT_LIMIT = 100
_MAX_EVENT_LIMIT = 2500
def __init__(self, accessToken: str):
self._token = accessToken
async def browse(
self,
path: str,
filter: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
cleanPath = (path or "").strip("/")
if not cleanPath:
url = f"{_CALENDAR_BASE}/users/me/calendarList?maxResults=250"
result = await googleGet(self._token, url)
if "error" in result:
_raiseGoogleError(result, "Google Calendar list")
calendars = result.get("items", [])
if filter:
f = filter.lower()
calendars = [c for c in calendars if f in (c.get("summary") or "").lower()]
return [
ExternalEntry(
name=c.get("summaryOverride") or c.get("summary", ""),
path=f"/{c.get('id', '')}",
isFolder=True,
metadata={
"id": c.get("id"),
"primary": c.get("primary", False),
"accessRole": c.get("accessRole"),
"backgroundColor": c.get("backgroundColor"),
"timeZone": c.get("timeZone"),
},
)
for c in calendars
]
from urllib.parse import quote
calendarId = cleanPath.split("/", 1)[0]
effectiveLimit = self._DEFAULT_EVENT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_EVENT_LIMIT))
url = (
f"{_CALENDAR_BASE}/calendars/{quote(calendarId, safe='')}/events"
f"?maxResults={effectiveLimit}&orderBy=startTime&singleEvents=true"
)
# Restrict to a date window when the filter is a date range, so large
# multi-year calendars only return the relevant period.
timeMin, timeMax = _parseGoogleDateRange(filter)
if timeMin and timeMax:
url += f"&timeMin={quote(timeMin, safe='')}&timeMax={quote(timeMax, safe='')}"
result = await googleGet(self._token, url)
if "error" in result:
_raiseGoogleError(result, "Google Calendar events")
events = result.get("items", [])
return [
ExternalEntry(
name=ev.get("summary", "(no title)"),
path=f"/{calendarId}/{ev.get('id', '')}",
isFolder=False,
mimeType="text/calendar",
metadata={
"id": ev.get("id"),
"start": (ev.get("start") or {}).get("dateTime") or (ev.get("start") or {}).get("date"),
"end": (ev.get("end") or {}).get("dateTime") or (ev.get("end") or {}).get("date"),
"location": ev.get("location"),
"organizer": (ev.get("organizer") or {}).get("email"),
"htmlLink": ev.get("htmlLink"),
"status": ev.get("status"),
},
)
for ev in events
]
async def download(self, path: str) -> DownloadResult:
from urllib.parse import quote
cleanPath = (path or "").strip("/")
if "/" not in cleanPath:
return DownloadResult()
calendarId, eventId = cleanPath.split("/", 1)
url = f"{_CALENDAR_BASE}/calendars/{quote(calendarId, safe='')}/events/{quote(eventId, safe='')}"
ev = await googleGet(self._token, url)
if "error" in ev:
logger.warning(f"Google Calendar event fetch failed: {ev['error']}")
return DownloadResult()
icsBytes = _googleEventToIcs(ev)
summary = ev.get("summary") or eventId
safeName = _googleSafeFileName(summary) or "event"
return DownloadResult(
data=icsBytes,
fileName=f"{safeName}.ics",
mimeType="text/calendar",
)
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "Google Calendar upload not supported"}
async def search(
self,
query: str,
path: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
from urllib.parse import quote
calendarId = (path or "").strip("/").split("/", 1)[0] or "primary"
effectiveLimit = self._DEFAULT_EVENT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_EVENT_LIMIT))
# A date-range query maps to timeMin/timeMax (efficient window fetch);
# otherwise fall back to the free-text q parameter.
timeMin, timeMax = _parseGoogleDateRange(query)
if timeMin and timeMax:
url = (
f"{_CALENDAR_BASE}/calendars/{quote(calendarId, safe='')}/events"
f"?timeMin={quote(timeMin, safe='')}&timeMax={quote(timeMax, safe='')}"
f"&maxResults={effectiveLimit}&orderBy=startTime&singleEvents=true"
)
else:
url = (
f"{_CALENDAR_BASE}/calendars/{quote(calendarId, safe='')}/events"
f"?q={quote(query, safe='')}&maxResults={effectiveLimit}&singleEvents=true"
)
result = await googleGet(self._token, url)
if "error" in result:
_raiseGoogleError(result, "Google Calendar search")
return [
ExternalEntry(
name=ev.get("summary", "(no title)"),
path=f"/{calendarId}/{ev.get('id', '')}",
isFolder=False,
mimeType="text/calendar",
metadata={
"id": ev.get("id"),
"start": (ev.get("start") or {}).get("dateTime") or (ev.get("start") or {}).get("date"),
"end": (ev.get("end") or {}).get("dateTime") or (ev.get("end") or {}).get("date"),
},
)
for ev in result.get("items", [])
]
class ContactsAdapter(ServiceAdapter):
"""Google Contacts ServiceAdapter -- People API (read-only).
Path conventions:
``""`` / ``"/"`` -> list contact groups (incl. virtual ``all`` for the user's connections)
``"/all"`` -> list all ``people/me/connections``
``"/<groupResourceName>"`` -> list members of that contact group (e.g. ``contactGroups/myFriends``)
``"/<group>/<personId>"`` -> reserved for future detail browse;
``personId`` is the suffix after ``people/``
"""
_DEFAULT_CONTACT_LIMIT = 200
_MAX_CONTACT_LIMIT = 1000
_PERSON_FIELDS = (
"names,emailAddresses,phoneNumbers,organizations,addresses,biographies,memberships"
)
def __init__(self, accessToken: str):
self._token = accessToken
async def browse(
self,
path: str,
filter: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
cleanPath = (path or "").strip("/")
if not cleanPath:
entries: List[ExternalEntry] = [
ExternalEntry(
name="Alle Kontakte",
path="/all",
isFolder=True,
metadata={"id": "all", "isVirtual": True},
),
]
url = f"{_PEOPLE_BASE}/contactGroups?pageSize=200"
result = await googleGet(self._token, url)
if "error" not in result:
for grp in result.get("contactGroups", []):
name = grp.get("formattedName") or grp.get("name") or ""
if not name:
continue
entries.append(
ExternalEntry(
name=name,
path=f"/{grp.get('resourceName', '')}",
isFolder=True,
metadata={
"id": grp.get("resourceName"),
"memberCount": grp.get("memberCount", 0),
"groupType": grp.get("groupType"),
},
)
)
else:
logger.warning(f"Google contactGroups list failed: {result['error']}")
return entries
from urllib.parse import quote
effectiveLimit = self._DEFAULT_CONTACT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_CONTACT_LIMIT))
groupRef = cleanPath.split("/", 1)[0]
if groupRef == "all":
url = (
f"{_PEOPLE_BASE}/people/me/connections"
f"?pageSize={min(effectiveLimit, 1000)}&personFields={self._PERSON_FIELDS}"
)
result = await googleGet(self._token, url)
if "error" in result:
_raiseGoogleError(result, "Google People connections")
people = result.get("connections", [])
else:
groupResource = groupRef
grpUrl = (
f"{_PEOPLE_BASE}/{quote(groupResource, safe='/')}"
f"?maxMembers={min(effectiveLimit, 1000)}"
)
grpResult = await googleGet(self._token, grpUrl)
if "error" in grpResult:
_raiseGoogleError(grpResult, "Google contactGroup detail")
memberResourceNames = grpResult.get("memberResourceNames") or []
if not memberResourceNames:
return []
chunkSize = 200
people: List[Dict[str, Any]] = []
for i in range(0, min(len(memberResourceNames), effectiveLimit), chunkSize):
chunk = memberResourceNames[i : i + chunkSize]
params = "&".join(f"resourceNames={quote(rn, safe='/')}" for rn in chunk)
batchUrl = f"{_PEOPLE_BASE}/people:batchGet?{params}&personFields={self._PERSON_FIELDS}"
batchResult = await googleGet(self._token, batchUrl)
if "error" in batchResult:
logger.warning(f"Google People batchGet failed: {batchResult['error']}")
continue
for resp in batchResult.get("responses", []):
person = resp.get("person")
if person:
people.append(person)
if len(people) >= effectiveLimit:
break
return [
ExternalEntry(
name=_googlePersonLabel(p) or "(no name)",
path=f"/{groupRef}/{(p.get('resourceName', '') or '').split('/')[-1]}",
isFolder=False,
mimeType="text/vcard",
metadata={
"id": p.get("resourceName"),
"emails": [e.get("value") for e in (p.get("emailAddresses") or []) if e.get("value")],
"phones": [pn.get("value") for pn in (p.get("phoneNumbers") or []) if pn.get("value")],
"organization": (p.get("organizations") or [{}])[0].get("name") if p.get("organizations") else None,
},
)
for p in people[:effectiveLimit]
]
async def download(self, path: str) -> DownloadResult:
from urllib.parse import quote
cleanPath = (path or "").strip("/")
if "/" not in cleanPath:
return DownloadResult()
personSuffix = cleanPath.split("/")[-1]
if not personSuffix:
return DownloadResult()
url = f"{_PEOPLE_BASE}/people/{quote(personSuffix, safe='')}?personFields={self._PERSON_FIELDS}"
person = await googleGet(self._token, url)
if "error" in person:
logger.warning(f"Google People fetch failed: {person['error']}")
return DownloadResult()
vcfBytes = _googlePersonToVcard(person)
label = _googlePersonLabel(person) or personSuffix
safeName = _googleSafeFileName(label) or "contact"
return DownloadResult(
data=vcfBytes,
fileName=f"{safeName}.vcf",
mimeType="text/vcard",
)
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "Google Contacts upload not supported"}
async def search(
self,
query: str,
path: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
from urllib.parse import quote
effectiveLimit = self._DEFAULT_CONTACT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_CONTACT_LIMIT))
url = (
f"{_PEOPLE_BASE}/people:searchContacts"
f"?query={quote(query, safe='')}&pageSize={min(effectiveLimit, 30)}"
f"&readMask={self._PERSON_FIELDS}"
)
result = await googleGet(self._token, url)
if "error" in result:
_raiseGoogleError(result, "Google Contacts search")
entries: List[ExternalEntry] = []
for r in result.get("results", []):
p = r.get("person") or {}
entries.append(
ExternalEntry(
name=_googlePersonLabel(p) or "(no name)",
path=f"/search/{(p.get('resourceName', '') or '').split('/')[-1]}",
isFolder=False,
mimeType="text/vcard",
metadata={
"id": p.get("resourceName"),
"emails": [e.get("value") for e in (p.get("emailAddresses") or []) if e.get("value")],
"phones": [pn.get("value") for pn in (p.get("phoneNumbers") or []) if pn.get("value")],
"organization": (p.get("organizations") or [{}])[0].get("name") if p.get("organizations") else None,
},
)
)
return entries
def _googleSafeFileName(name: str) -> str:
return re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", name or "")[:80].strip(". ")
def _googleIcsEscape(value: str) -> str:
if value is None:
return ""
return (
value.replace("\\", "\\\\")
.replace(";", "\\;")
.replace(",", "\\,")
.replace("\r\n", "\\n")
.replace("\n", "\\n")
)
def _googleIcsDateTime(value: Optional[str]) -> Optional[str]:
"""Convert a Google Calendar dateTime/date string to RFC 5545 format (UTC)."""
if not value:
return None
try:
if "T" not in value:
dt = datetime.strptime(value, "%Y-%m-%d")
return dt.strftime("%Y%m%d")
normalized = value.replace("Z", "+00:00") if value.endswith("Z") else value
dt = datetime.fromisoformat(normalized)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
except (TypeError, ValueError):
return None
def _googleEventToIcs(event: Dict[str, Any]) -> bytes:
"""Build a minimal RFC 5545 VCALENDAR/VEVENT for a Google Calendar event."""
uid = event.get("iCalUID") or event.get("id") or "unknown@poweron"
summary = _googleIcsEscape(event.get("summary") or "")
location = _googleIcsEscape(event.get("location") or "")
description = _googleIcsEscape(event.get("description") or "")
rawStart = (event.get("start") or {}).get("dateTime") or (event.get("start") or {}).get("date")
rawEnd = (event.get("end") or {}).get("dateTime") or (event.get("end") or {}).get("date")
isAllDay = bool((event.get("start") or {}).get("date") and not (event.get("start") or {}).get("dateTime"))
dtstart = _googleIcsDateTime(rawStart)
dtend = _googleIcsDateTime(rawEnd)
dtstamp = _googleIcsDateTime(event.get("updated")) or datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//PowerOn//Google-Calendar-Adapter//EN",
"CALSCALE:GREGORIAN",
"BEGIN:VEVENT",
f"UID:{uid}",
f"DTSTAMP:{dtstamp}",
]
if dtstart:
lines.append(f"DTSTART;VALUE=DATE:{dtstart}" if isAllDay else f"DTSTART:{dtstart}")
if dtend:
lines.append(f"DTEND;VALUE=DATE:{dtend}" if isAllDay else f"DTEND:{dtend}")
if summary:
lines.append(f"SUMMARY:{summary}")
if location:
lines.append(f"LOCATION:{location}")
if description:
lines.append(f"DESCRIPTION:{description}")
organizer = (event.get("organizer") or {}).get("email")
if organizer:
lines.append(f"ORGANIZER:mailto:{organizer}")
for att in (event.get("attendees") or []):
addr = att.get("email")
if addr:
lines.append(f"ATTENDEE:mailto:{addr}")
lines.append("END:VEVENT")
lines.append("END:VCALENDAR")
return ("\r\n".join(lines) + "\r\n").encode("utf-8")
def _googlePersonLabel(person: Dict[str, Any]) -> str:
names = person.get("names") or []
if names:
primary = names[0]
display = primary.get("displayName") or ""
if display:
return display
given = primary.get("givenName") or ""
family = primary.get("familyName") or ""
full = f"{given} {family}".strip()
if full:
return full
orgs = person.get("organizations") or []
if orgs and orgs[0].get("name"):
return orgs[0]["name"]
emails = person.get("emailAddresses") or []
if emails and emails[0].get("value"):
return emails[0]["value"]
return ""
def _googlePersonToVcard(person: Dict[str, Any]) -> bytes:
"""Build a vCard 3.0 from a Google People API person payload."""
names = person.get("names") or []
primaryName = names[0] if names else {}
given = primaryName.get("givenName") or ""
family = primaryName.get("familyName") or ""
middle = primaryName.get("middleName") or ""
fn = primaryName.get("displayName") or _googlePersonLabel(person) or ""
lines = [
"BEGIN:VCARD",
"VERSION:3.0",
f"N:{family};{given};{middle};;",
f"FN:{fn}",
]
orgs = person.get("organizations") or []
if orgs:
org = orgs[0]
orgVal = org.get("name") or ""
if org.get("department"):
orgVal = f"{orgVal};{org['department']}"
if orgVal:
lines.append(f"ORG:{orgVal}")
if org.get("title"):
lines.append(f"TITLE:{org['title']}")
for em in (person.get("emailAddresses") or []):
addr = em.get("value")
if not addr:
continue
emailType = (em.get("type") or "INTERNET").upper()
lines.append(f"EMAIL;TYPE={emailType}:{addr}")
for ph in (person.get("phoneNumbers") or []):
val = ph.get("value")
if not val:
continue
phType = (ph.get("type") or "VOICE").upper()
lines.append(f"TEL;TYPE={phType}:{val}")
for addr in (person.get("addresses") or []):
street = addr.get("streetAddress") or ""
city = addr.get("city") or ""
region = addr.get("region") or ""
postal = addr.get("postalCode") or ""
country = addr.get("country") or ""
if any([street, city, region, postal, country]):
adrType = (addr.get("type") or "OTHER").upper()
lines.append(f"ADR;TYPE={adrType}:;;{street};{city};{region};{postal};{country}")
bios = person.get("biographies") or []
if bios and bios[0].get("value"):
lines.append(f"NOTE:{_googleIcsEscape(bios[0]['value'])}")
lines.append(f"UID:{person.get('resourceName', '')}")
lines.append("END:VCARD")
return ("\r\n".join(lines) + "\r\n").encode("utf-8")
class GoogleConnector(ProviderConnector):
"""Google ProviderConnector -- 1 connection -> Drive + Gmail + Calendar + Contacts."""
_SERVICE_MAP = {
"drive": DriveAdapter,
"gmail": GmailAdapter,
"calendar": CalendarAdapter,
"contact": ContactsAdapter,
}
def getAvailableServices(self) -> List[str]:
return list(self._SERVICE_MAP.keys())
def getServiceAdapter(self, service: str) -> ServiceAdapter:
adapterClass = self._SERVICE_MAP.get(service)
if not adapterClass:
raise ValueError(f"Unknown Google service: {service}. Available: {list(self._SERVICE_MAP.keys())}")
return adapterClass(self.accessToken)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""ConnectorResolver -- resolves a connectionId to the correct ProviderConnector and ServiceAdapter. """ConnectorResolver -- resolves a connectionId to the correct ProviderConnector and ServiceAdapter.
@ -15,15 +15,6 @@ from modules.connectors.connectorProviderBase import ProviderConnector, ServiceA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _connection_uuid(connection: Any) -> str:
"""Resolve UserConnection primary key (tokens are stored by UUID, not reference string)."""
if connection is None:
return ""
if isinstance(connection, dict):
return str(connection.get("id") or "").strip()
return str(getattr(connection, "id", None) or "").strip()
class ConnectorResolver: class ConnectorResolver:
"""Resolves connectionId → ProviderConnector (with fresh token) → ServiceAdapter.""" """Resolves connectionId → ProviderConnector (with fresh token) → ServiceAdapter."""
@ -44,35 +35,29 @@ class ConnectorResolver:
if ConnectorResolver._providerRegistry: if ConnectorResolver._providerRegistry:
return return
try: try:
from modules.connectors.connectorProviderMsft import MsftConnector from modules.connectors.providerMsft.connectorMsft import MsftConnector
ConnectorResolver._providerRegistry["msft"] = MsftConnector ConnectorResolver._providerRegistry["msft"] = MsftConnector
except ImportError: except ImportError:
logger.warning("MsftConnector not available") logger.warning("MsftConnector not available")
try: try:
from modules.connectors.connectorProviderGoogle import GoogleConnector from modules.connectors.providerGoogle.connectorGoogle import GoogleConnector
ConnectorResolver._providerRegistry["google"] = GoogleConnector ConnectorResolver._providerRegistry["google"] = GoogleConnector
except ImportError: except ImportError:
logger.debug("GoogleConnector not available (stub)") logger.debug("GoogleConnector not available (stub)")
try: try:
from modules.connectors.connectorProviderFtp import FtpConnector from modules.connectors.providerFtp.connectorFtp import FtpConnector
ConnectorResolver._providerRegistry["local:ftp"] = FtpConnector ConnectorResolver._providerRegistry["local:ftp"] = FtpConnector
except ImportError: except ImportError:
logger.debug("FtpConnector not available (stub)") logger.debug("FtpConnector not available (stub)")
try: try:
from modules.connectors.connectorProviderClickup import ClickupConnector from modules.connectors.providerClickup.connectorClickup import ClickupConnector
ConnectorResolver._providerRegistry["clickup"] = ClickupConnector ConnectorResolver._providerRegistry["clickup"] = ClickupConnector
except ImportError: except ImportError:
logger.warning("ClickupConnector not available") logger.warning("ClickupConnector not available")
try:
from modules.connectors.connectorProviderInfomaniak import InfomaniakConnector
ConnectorResolver._providerRegistry["infomaniak"] = InfomaniakConnector
except ImportError:
logger.warning("InfomaniakConnector not available")
async def resolve(self, connectionId: str) -> ProviderConnector: async def resolve(self, connectionId: str) -> ProviderConnector:
"""Resolve connectionId to a ProviderConnector with a fresh access token.""" """Resolve connectionId to a ProviderConnector with a fresh access token."""
connection = await self._loadConnection(connectionId) connection = await self._loadConnection(connectionId)
@ -88,16 +73,9 @@ class ConnectorResolver:
if not providerClass: if not providerClass:
raise ValueError(f"No ProviderConnector registered for authority: {authorityStr}") raise ValueError(f"No ProviderConnector registered for authority: {authorityStr}")
resolved_id = _connection_uuid(connection) token = self._security.getFreshToken(connectionId)
if not resolved_id:
raise ValueError(f"Connection {connectionId} has no id")
token = self._security.getFreshToken(resolved_id)
if not token or not token.tokenAccess: if not token or not token.tokenAccess:
raise ValueError( raise ValueError(f"No valid token for connection {connectionId}")
f"No valid token for connection {resolved_id}"
+ (f" (ref: {connectionId})" if connectionId != resolved_id else "")
)
return providerClass(connection, token.tokenAccess) return providerClass(connection, token.tokenAccess)

View file

@ -1,5 +1,3 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
""" """
Swiss Topo MapServer Connector (Simplified) Swiss Topo MapServer Connector (Simplified)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""ClickUp connector for CRUD operations (compatible with TicketInterface). """ClickUp connector for CRUD operations (compatible with TicketInterface).
@ -9,7 +9,7 @@ from typing import Optional
import logging import logging
import aiohttp import aiohttp
from modules.datamodels.datamodelTickets import TicketBase, TicketFieldAttribute from modules.datamodels.datamodelTickets import TicketBase, TicketFieldAttribute
from modules.connectors.connectorProviderClickup import clickupAuthorizationHeader from modules.serviceCenter.services.serviceClickup.mainServiceClickup import clickup_authorization_header
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,7 +31,7 @@ class ConnectorTicketClickup(TicketBase):
def _headers(self) -> dict: def _headers(self) -> dict:
return { return {
"Authorization": clickupAuthorizationHeader(self.apiToken), "Authorization": clickup_authorization_header(self.apiToken),
"Content-Type": "application/json", "Content-Type": "application/json",
} }

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""Jira connector for CRUD operations (neutralized to generic ticket interface). """Jira connector for CRUD operations (neutralized to generic ticket interface).

View file

@ -1,419 +0,0 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""Redmine REST connector.
Async / aiohttp port of the SSS pilot client
(``pamocreate/projects/valueon/sss/project_mars/redmine-sync/code/_redmineClient.py``)
plus the read-side helpers required by ``serviceRedmine`` and
``serviceRedmineStats``.
Auth: ``X-Redmine-API-Key`` header. The key is *never* logged.
Idempotency / safety:
- ``DELETE /issues/{id}`` is often forbidden in Redmine (HTTP 403).
``deleteIssue`` returns ``False`` instead of raising in that case so
the higher layer can fall back to status-based archival.
- A small ``_throttleSeconds`` delay (default 150 ms) is awaited after
every write call to keep the SSS server happy.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlencode
import aiohttp
from modules.datamodels.datamodelTickets import TicketBase, TicketFieldAttribute
logger = logging.getLogger(__name__)
class RedmineApiError(RuntimeError):
"""Raised when the Redmine API returns a non-success status."""
def __init__(self, status: int, body: str, method: str, path: str):
self.status = status
self.body = body
self.method = method
self.path = path
super().__init__(f"Redmine {method} {path} failed: HTTP {status} {body[:300]}")
class ConnectorTicketsRedmine(TicketBase):
"""Async Redmine connector. One instance per (baseUrl, apiKey, projectId)."""
def __init__(
self,
*,
baseUrl: str,
apiKey: str,
projectId: str,
throttleSeconds: float = 0.15,
timeoutSeconds: float = 30.0,
) -> None:
if not baseUrl:
raise ValueError("Redmine baseUrl is required")
if not apiKey:
raise ValueError("Redmine apiKey is required")
self._baseUrl = baseUrl.rstrip("/")
self._apiKey = apiKey
self._projectId = str(projectId) if projectId is not None else ""
self._throttleSeconds = max(0.0, float(throttleSeconds))
self._timeoutSeconds = float(timeoutSeconds)
# ------------------------------------------------------------------
# Low-level
# ------------------------------------------------------------------
def _headers(self) -> Dict[str, str]:
return {
"X-Redmine-API-Key": self._apiKey,
"Content-Type": "application/json",
"Accept": "application/json",
}
async def _call(
self,
method: str,
path: str,
*,
payload: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
) -> Tuple[int, Optional[Dict[str, Any]], str]:
"""Single REST call. Returns ``(status, json_or_none, raw_body)``.
Does *not* raise -- the caller decides whether a non-2xx is fatal
(e.g. 403 on DELETE is expected and handled).
"""
url = f"{self._baseUrl}{path}"
if params:
url = f"{url}?{urlencode(params)}"
timeout = aiohttp.ClientTimeout(total=self._timeoutSeconds)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.request(method, url, headers=self._headers(), json=payload) as resp:
raw = await resp.text()
parsed: Optional[Dict[str, Any]] = None
if raw:
try:
parsed = await resp.json(content_type=None)
except Exception:
parsed = None
return resp.status, parsed, raw
except aiohttp.ClientError as e:
logger.warning(f"Redmine {method} {path} client error: {e}")
return -1, None, f"ClientError: {e}"
except asyncio.TimeoutError:
logger.warning(f"Redmine {method} {path} timeout after {self._timeoutSeconds}s")
return -1, None, "Timeout"
@staticmethod
def _isOk(status: int) -> bool:
return 200 <= status < 300
async def _gentle(self) -> None:
if self._throttleSeconds > 0:
await asyncio.sleep(self._throttleSeconds)
# ------------------------------------------------------------------
# Identity / health
# ------------------------------------------------------------------
async def whoAmI(self) -> Dict[str, Any]:
status, body, raw = await self._call("GET", "/users/current.json")
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "GET", "/users/current.json")
return body.get("user", {})
# ------------------------------------------------------------------
# Project meta -- trackers, statuses, priorities, custom fields, users
# ------------------------------------------------------------------
async def getTrackers(self) -> List[Dict[str, Any]]:
status, body, raw = await self._call("GET", "/trackers.json")
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "GET", "/trackers.json")
return body.get("trackers", []) or []
async def getStatuses(self) -> List[Dict[str, Any]]:
status, body, raw = await self._call("GET", "/issue_statuses.json")
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "GET", "/issue_statuses.json")
return body.get("issue_statuses", []) or []
async def getPriorities(self) -> List[Dict[str, Any]]:
status, body, raw = await self._call(
"GET", "/enumerations/issue_priorities.json"
)
if not self._isOk(status) or not body:
return []
return body.get("issue_priorities", []) or []
async def getCustomFields(self) -> List[Dict[str, Any]]:
"""Requires admin privileges in Redmine. Returns ``[]`` if forbidden."""
status, body, raw = await self._call("GET", "/custom_fields.json")
if status == 403 or status == 401:
logger.info("Redmine /custom_fields.json forbidden -- using per-issue field discovery")
return []
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "GET", "/custom_fields.json")
return body.get("custom_fields", []) or []
async def getProjectUsers(self) -> List[Dict[str, Any]]:
status, body, raw = await self._call(
"GET", f"/projects/{self._projectId}/memberships.json", params={"limit": 100}
)
if not self._isOk(status) or not body:
return []
members = body.get("memberships", []) or []
users: List[Dict[str, Any]] = []
seen: set[int] = set()
for m in members:
user = m.get("user")
if not user:
continue
uid = user.get("id")
if uid in seen:
continue
seen.add(uid)
users.append(user)
return users
async def getProjectInfo(self) -> Dict[str, Any]:
status, body, raw = await self._call("GET", f"/projects/{self._projectId}.json")
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "GET", f"/projects/{self._projectId}.json")
return body.get("project", {})
async def getIssueCategories(self) -> List[Dict[str, Any]]:
"""Per-project issue categories. Returns ``[]`` if the endpoint
is forbidden or the project has no categories defined."""
path = f"/projects/{self._projectId}/issue_categories.json"
status, body, raw = await self._call("GET", path)
if status in (401, 403, 404) or not self._isOk(status) or not body:
return []
return body.get("issue_categories", []) or []
# ------------------------------------------------------------------
# Issues -- read
# ------------------------------------------------------------------
async def getIssue(
self, issueId: int, *, includeRelations: bool = True, includeChildren: bool = False
) -> Dict[str, Any]:
includes = ["custom_fields", "journals"]
if includeRelations:
includes.append("relations")
if includeChildren:
includes.append("children")
params = {"include": ",".join(includes)}
status, body, raw = await self._call("GET", f"/issues/{issueId}.json", params=params)
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "GET", f"/issues/{issueId}.json")
return body.get("issue", {})
async def listIssues(
self,
*,
trackerId: Optional[int] = None,
statusId: Optional[str] = "*",
updatedOnFrom: Optional[str] = None,
updatedOnTo: Optional[str] = None,
createdOnFrom: Optional[str] = None,
createdOnTo: Optional[str] = None,
assignedToId: Optional[int] = None,
subjectContains: Optional[str] = None,
limit: int = 100,
offset: int = 0,
include: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Single-page list. Returns the raw envelope ``{issues, total_count, offset, limit}``."""
params: Dict[str, Any] = {
"project_id": self._projectId,
"limit": str(limit),
"offset": str(offset),
}
if statusId is not None:
params["status_id"] = str(statusId)
if trackerId is not None:
params["tracker_id"] = str(trackerId)
if assignedToId is not None:
params["assigned_to_id"] = str(assignedToId)
if subjectContains:
params["subject"] = f"~{subjectContains}"
if updatedOnFrom and updatedOnTo:
params["updated_on"] = f"><{updatedOnFrom}|{updatedOnTo}"
elif updatedOnFrom:
params["updated_on"] = f">={updatedOnFrom}"
elif updatedOnTo:
params["updated_on"] = f"<={updatedOnTo}"
if createdOnFrom and createdOnTo:
params["created_on"] = f"><{createdOnFrom}|{createdOnTo}"
elif createdOnFrom:
params["created_on"] = f">={createdOnFrom}"
elif createdOnTo:
params["created_on"] = f"<={createdOnTo}"
if include:
params["include"] = ",".join(include)
status, body, raw = await self._call("GET", "/issues.json", params=params)
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "GET", "/issues.json")
return body
async def listAllIssues(
self,
*,
trackerId: Optional[int] = None,
statusId: Optional[str] = "*",
updatedOnFrom: Optional[str] = None,
updatedOnTo: Optional[str] = None,
createdOnFrom: Optional[str] = None,
createdOnTo: Optional[str] = None,
assignedToId: Optional[int] = None,
pageSize: int = 100,
maxPages: int = 50,
include: Optional[List[str]] = None,
) -> List[Dict[str, Any]]:
"""Paginate ``listIssues`` and return all matching raw issues."""
all_issues: List[Dict[str, Any]] = []
offset = 0
for _page in range(maxPages):
envelope = await self.listIssues(
trackerId=trackerId,
statusId=statusId,
updatedOnFrom=updatedOnFrom,
updatedOnTo=updatedOnTo,
createdOnFrom=createdOnFrom,
createdOnTo=createdOnTo,
assignedToId=assignedToId,
limit=pageSize,
offset=offset,
include=include,
)
page_issues = envelope.get("issues", []) or []
all_issues.extend(page_issues)
total = int(envelope.get("total_count") or 0)
offset += len(page_issues)
if not page_issues or offset >= total:
break
return all_issues
async def listRelations(self, issueId: int) -> List[Dict[str, Any]]:
issue = await self.getIssue(issueId, includeRelations=True)
return issue.get("relations", []) or []
# ------------------------------------------------------------------
# Issues -- write
# ------------------------------------------------------------------
async def createIssue(self, fields: Dict[str, Any]) -> Dict[str, Any]:
body_in = {"issue": dict(fields)}
body_in["issue"].setdefault("project_id", self._projectId)
status, body, raw = await self._call("POST", "/issues.json", payload=body_in)
await self._gentle()
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "POST", "/issues.json")
return body.get("issue", {})
async def updateIssue(
self, issueId: int, fields: Dict[str, Any], *, notes: Optional[str] = None
) -> bool:
body_in: Dict[str, Any] = {"issue": dict(fields)}
if notes:
body_in["issue"]["notes"] = notes
status, body, raw = await self._call("PUT", f"/issues/{issueId}.json", payload=body_in)
await self._gentle()
if status == 204:
return True
if not self._isOk(status):
raise RedmineApiError(status, raw, "PUT", f"/issues/{issueId}.json")
return True
async def deleteIssue(self, issueId: int) -> bool:
"""Returns ``False`` if Redmine forbids deletion (HTTP 403/401)."""
status, body, raw = await self._call("DELETE", f"/issues/{issueId}.json")
await self._gentle()
if status in (200, 204):
return True
if status in (401, 403):
logger.info(f"Redmine DELETE issue {issueId} forbidden ({status}) -- caller should fall back")
return False
raise RedmineApiError(status, raw, "DELETE", f"/issues/{issueId}.json")
# ------------------------------------------------------------------
# Relations -- write
# ------------------------------------------------------------------
async def addRelation(
self, fromId: int, toId: int, *, relationType: str = "relates", delay: Optional[int] = None
) -> Dict[str, Any]:
rel: Dict[str, Any] = {"issue_to_id": toId, "relation_type": relationType}
if delay is not None:
rel["delay"] = int(delay)
status, body, raw = await self._call(
"POST", f"/issues/{fromId}/relations.json", payload={"relation": rel}
)
await self._gentle()
if not self._isOk(status) or not body:
raise RedmineApiError(status, raw, "POST", f"/issues/{fromId}/relations.json")
return body.get("relation", {})
async def deleteRelation(self, relationId: int) -> bool:
status, body, raw = await self._call("DELETE", f"/relations/{relationId}.json")
await self._gentle()
if status in (200, 204):
return True
if status in (401, 403):
return False
raise RedmineApiError(status, raw, "DELETE", f"/relations/{relationId}.json")
# ------------------------------------------------------------------
# TicketBase compliance (used by AI-tool path)
# ------------------------------------------------------------------
async def readAttributes(self) -> List[TicketFieldAttribute]:
"""Static base attributes + project custom fields (best-effort)."""
attrs: List[TicketFieldAttribute] = [
TicketFieldAttribute(fieldName="Subject", field="subject"),
TicketFieldAttribute(fieldName="Description", field="description"),
TicketFieldAttribute(fieldName="Tracker", field="tracker_id"),
TicketFieldAttribute(fieldName="Status", field="status_id"),
TicketFieldAttribute(fieldName="Priority", field="priority_id"),
TicketFieldAttribute(fieldName="Assignee", field="assigned_to_id"),
TicketFieldAttribute(fieldName="Parent", field="parent_issue_id"),
TicketFieldAttribute(fieldName="Target Version", field="fixed_version_id"),
]
try:
cfs = await self.getCustomFields()
except Exception:
cfs = []
for cf in cfs:
try:
attrs.append(
TicketFieldAttribute(
fieldName=str(cf.get("name", f"cf_{cf.get('id')}")),
field=f"cf_{cf.get('id')}",
)
)
except Exception:
continue
return attrs
async def readTasks(self, *, limit: int = 0) -> List[Dict[str, Any]]:
if limit and limit > 0:
envelope = await self.listIssues(limit=limit)
return envelope.get("issues", []) or []
return await self.listAllIssues()
async def writeTasks(self, tasklist: List[Dict[str, Any]]) -> None:
for task in tasklist:
issue_id = task.get("id")
fields = {k: v for k, v in task.items() if k != "id"}
if issue_id:
await self.updateIssue(int(issue_id), fields)
else:
await self.createIssue(fields)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Google Cloud Speech-to-Text and Translation Connector Google Cloud Speech-to-Text and Translation Connector
@ -15,34 +15,10 @@ from google.cloud import speech
from google.cloud import translate_v2 as translate from google.cloud import translate_v2 as translate
from google.cloud import texttospeech from google.cloud import texttospeech
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.shared.voiceCatalog import getDefaultVoice from modules.shared.voiceCatalog import getDefaultVoice as _catalogDefaultVoice
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _buildPrimarySttRecognitionFields(
*,
model: str,
lightweight: bool,
) -> Dict[str, Any]:
"""Shared fields for batch + streaming primary RecognitionConfig."""
base: Dict[str, Any] = {
"enable_automatic_punctuation": True,
"model": model,
}
if lightweight:
base["enable_word_time_offsets"] = False
base["enable_word_confidence"] = False
base["max_alternatives"] = 1
base["use_enhanced"] = False
else:
base["enable_word_time_offsets"] = True
base["enable_word_confidence"] = True
base["max_alternatives"] = 3
base["use_enhanced"] = True
return base
# Gemini-TTS speaker IDs from voices.list use short names (e.g. "Kore") and require # Gemini-TTS speaker IDs from voices.list use short names (e.g. "Kore") and require
# SynthesisInput.prompt + VoiceSelectionParams.model_name (google-cloud-texttospeech >= 2.24.0). # SynthesisInput.prompt + VoiceSelectionParams.model_name (google-cloud-texttospeech >= 2.24.0).
_GEMINI_TTS_DEFAULT_MODEL = "gemini-2.5-flash-tts" _GEMINI_TTS_DEFAULT_MODEL = "gemini-2.5-flash-tts"
@ -97,10 +73,7 @@ class ConnectorGoogleSpeech:
sampleRate: int = None, channels: int = None, sampleRate: int = None, channels: int = None,
skipFallbacks: bool = False, skipFallbacks: bool = False,
phraseHints: Optional[list] = None, phraseHints: Optional[list] = None,
alternativeLanguages: Optional[list] = None, alternativeLanguages: Optional[list] = None) -> Dict:
model: str = "latest_long",
lightweight: bool = False,
audioFormat: Optional[str] = None) -> Dict:
""" """
Convert speech to text using Google Cloud Speech-to-Text API. Convert speech to text using Google Cloud Speech-to-Text API.
@ -109,9 +82,6 @@ class ConnectorGoogleSpeech:
language: Language code (e.g., 'de-DE', 'en-US') language: Language code (e.g., 'de-DE', 'en-US')
sample_rate: Audio sample rate (auto-detected if None) sample_rate: Audio sample rate (auto-detected if None)
channels: Number of audio channels (auto-detected if None) channels: Number of audio channels (auto-detected if None)
model: Google recognition model (e.g. latest_long, latest_short)
lightweight: If True, omit word timings/confidence, single alternative, no enhanced model
audioFormat: If set (webm_opus, linear16, mp3, flac, wav), skip auto-detection
Returns: Returns:
Dict containing transcribed text, confidence, and metadata Dict containing transcribed text, confidence, and metadata
@ -122,24 +92,8 @@ class ConnectorGoogleSpeech:
logger.warning(f"Invalid sampleRate={sampleRate}, treating as unknown for auto-detection") logger.warning(f"Invalid sampleRate={sampleRate}, treating as unknown for auto-detection")
sampleRate = None sampleRate = None
explicitFormat = (audioFormat or "").strip().lower() or None # Auto-detect audio format if not provided
if explicitFormat: if sampleRate is None or channels is None:
if channels is None:
channels = 1
if sampleRate is None:
if explicitFormat == "webm_opus":
sampleRate = 48000
elif explicitFormat == "linear16":
sampleRate = 16000
elif explicitFormat in ("mp3", "flac"):
sampleRate = 44100
elif explicitFormat == "wav":
sampleRate = 16000
else:
sampleRate = 16000
audioFormat = explicitFormat
logger.info(f"STT explicit format: {audioFormat}, {sampleRate}Hz, {channels}ch")
elif sampleRate is None or channels is None:
validation = self.validateAudioFormat(audioContent) validation = self.validateAudioFormat(audioContent)
if not validation["valid"]: if not validation["valid"]:
return { return {
@ -202,7 +156,12 @@ class ConnectorGoogleSpeech:
"encoding": encoding, "encoding": encoding,
"audio_channel_count": channels, "audio_channel_count": channels,
"language_code": language, "language_code": language,
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight), "enable_automatic_punctuation": True,
"model": "latest_long",
"enable_word_time_offsets": True,
"enable_word_confidence": True,
"max_alternatives": 3,
"use_enhanced": True,
} }
if phraseHints: if phraseHints:
@ -246,7 +205,8 @@ class ConnectorGoogleSpeech:
sample_rate_hertz=16000, sample_rate_hertz=16000,
audio_channel_count=1, audio_channel_count=1,
language_code=language, language_code=language,
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight), enable_automatic_punctuation=True,
model="latest_long"
) )
try: try:
response = await asyncio.to_thread( response = await asyncio.to_thread(
@ -383,7 +343,7 @@ class ConnectorGoogleSpeech:
"error": "No recognition results (silence or unclear audio)" "error": "No recognition results (silence or unclear audio)"
} }
models = list(dict.fromkeys([model, "latest_long", "phone_call", "latest_short"])) models = ["latest_long", "phone_call", "latest_short"]
for fallback_config in fallback_configs: for fallback_config in fallback_configs:
for model in models: for model in models:
@ -459,9 +419,6 @@ class ConnectorGoogleSpeech:
audioQueue: asyncio.Queue, audioQueue: asyncio.Queue,
language: str = "de-DE", language: str = "de-DE",
phraseHints: Optional[list] = None, phraseHints: Optional[list] = None,
model: str = "latest_long",
lightweight: bool = False,
singleUtterance: bool = False,
) -> AsyncGenerator[Dict[str, Any], None]: ) -> AsyncGenerator[Dict[str, Any], None]:
""" """
Stream audio chunks to Google Cloud Speech-to-Text Streaming API. Stream audio chunks to Google Cloud Speech-to-Text Streaming API.
@ -472,13 +429,9 @@ class ConnectorGoogleSpeech:
Send (b"", True) to signal end of stream. Send (b"", True) to signal end of stream.
language: Language code language: Language code
phraseHints: Optional boost phrases phraseHints: Optional boost phrases
model: Google recognition model (e.g. latest_long, latest_short)
lightweight: If True, use non-enhanced primary config (lower latency)
singleUtterance: If True, end stream after first utterance (client should reconnect)
Yields: Yields:
Dicts with keys: isFinal, transcript, confidence, stabilityScore, audioDurationSec; Dicts with keys: isFinal, transcript, confidence, stabilityScore, audioDurationSec
optionally endOfSingleUtterance, reconnectRequired
""" """
STREAM_LIMIT_SEC = 290 STREAM_LIMIT_SEC = 290
streamStartTs = time.time() streamStartTs = time.time()
@ -489,7 +442,9 @@ class ConnectorGoogleSpeech:
"sample_rate_hertz": 48000, "sample_rate_hertz": 48000,
"audio_channel_count": 1, "audio_channel_count": 1,
"language_code": language, "language_code": language,
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight), "enable_automatic_punctuation": True,
"model": "latest_long",
"use_enhanced": True,
} }
if phraseHints: if phraseHints:
configParams["speech_contexts"] = [speech.SpeechContext(phrases=phraseHints, boost=15.0)] configParams["speech_contexts"] = [speech.SpeechContext(phrases=phraseHints, boost=15.0)]
@ -498,7 +453,7 @@ class ConnectorGoogleSpeech:
streamingConfig = speech.StreamingRecognitionConfig( streamingConfig = speech.StreamingRecognitionConfig(
config=recognitionConfig, config=recognitionConfig,
interim_results=True, interim_results=True,
single_utterance=singleUtterance, single_utterance=False,
) )
import queue as threadQueue import queue as threadQueue
@ -535,22 +490,7 @@ class ConnectorGoogleSpeech:
) )
for response in responseStream: for response in responseStream:
elapsed = time.time() - streamStartTs elapsed = time.time() - streamStartTs
estimatedDurationSec = totalAudioBytes / (48000 * 1 * 2) if totalAudioBytes else 0
durationFromResults = 0.0
for result in response.results:
rt = getattr(result, "result_end_time", None)
if rt is None:
continue
if hasattr(rt, "total_seconds"):
durationFromResults = max(durationFromResults, float(rt.total_seconds()))
else:
durationFromResults = max(
durationFromResults,
float(getattr(rt, "seconds", 0)) + float(getattr(rt, "nanos", 0)) * 1e-9,
)
estimatedDurationSec = durationFromResults if durationFromResults > 0 else (
totalAudioBytes / (48000 * 1 * 2) if totalAudioBytes else 0.0
)
finalTexts = [] finalTexts = []
interimTexts = [] interimTexts = []
@ -584,13 +524,6 @@ class ConnectorGoogleSpeech:
"stabilityScore": 0.0, "stabilityScore": 0.0,
"audioDurationSec": estimatedDurationSec, "audioDurationSec": estimatedDurationSec,
}), loop) }), loop)
speechEvt = getattr(response, "speech_event_type", None)
if speechEvt and "END_OF_SINGLE_UTTERANCE" in str(speechEvt):
asyncio.run_coroutine_threadsafe(resultOutQ.put({
"endOfSingleUtterance": True,
"audioDurationSec": estimatedDurationSec,
}), loop)
if elapsed >= STREAM_LIMIT_SEC: if elapsed >= STREAM_LIMIT_SEC:
logger.info("Streaming STT approaching 5-min limit, client should reconnect") logger.info("Streaming STT approaching 5-min limit, client should reconnect")
asyncio.run_coroutine_threadsafe(resultOutQ.put({ asyncio.run_coroutine_threadsafe(resultOutQ.put({
@ -1097,7 +1030,7 @@ class ConnectorGoogleSpeech:
voice exists, in which case the caller omits `name` and Google voice exists, in which case the caller omits `name` and Google
auto-selects based on languageCode + ssml_gender. auto-selects based on languageCode + ssml_gender.
""" """
return getDefaultVoice(languageCode) return _catalogDefaultVoice(languageCode)
async def getAvailableVoices(self, languageCode: Optional[str] = None) -> Dict[str, Any]: async def getAvailableVoices(self, languageCode: Optional[str] = None) -> Dict[str, Any]:
""" """

View file

@ -1,5 +1,3 @@
# Copyright (c) 2026 PowerOn AG
# All rights reserved.
""" """
Swiss Parcel (Liegenschaften) Connector Swiss Parcel (Liegenschaften) Connector

View file

@ -0,0 +1,7 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""ClickUp provider connector."""
from .connectorClickup import ClickupConnector
__all__ = ["ClickupConnector"]

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""ClickUp ProviderConnector — virtual paths for teams → lists → tasks (table rows). """ClickUp ProviderConnector — virtual paths for teams → lists → tasks (table rows).
@ -13,13 +13,10 @@ Path convention (leading slash, no trailing slash except root):
from __future__ import annotations from __future__ import annotations
import asyncio
import json import json
import logging import logging
import re import re
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional
import aiohttp
from modules.connectors.connectorProviderBase import ( from modules.connectors.connectorProviderBase import (
ProviderConnector, ProviderConnector,
@ -27,11 +24,11 @@ from modules.connectors.connectorProviderBase import (
DownloadResult, DownloadResult,
) )
from modules.datamodels.datamodelDataSource import ExternalEntry from modules.datamodels.datamodelDataSource import ExternalEntry
from modules.serviceCenter.services.serviceClickup.mainServiceClickup import ClickupService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_CLICKUP_API_BASE = "https://api.clickup.com/api/v2" # type metadata for ExternalEntry.metadata["cuType"]
_CU_TEAM = "team" _CU_TEAM = "team"
_CU_SPACE = "space" _CU_SPACE = "space"
_CU_FOLDER = "folder" _CU_FOLDER = "folder"
@ -48,125 +45,16 @@ def _norm(path: str) -> str:
return p return p
def clickupAuthorizationHeader(token: str) -> str:
"""ClickUp: personal tokens are `pk_...` without Bearer; OAuth uses Bearer."""
t = (token or "").strip()
if t.startswith("pk_"):
return t
return f"Bearer {t}"
class ClickupApiClient:
"""Low-level ClickUp REST API v2 client. Pure HTTP — no service dependencies."""
def __init__(self, accessToken: str):
self.accessToken = accessToken
async def _request(
self,
method: str,
path: str,
*,
params: Optional[Dict[str, Any]] = None,
json_body: Optional[Dict[str, Any]] = None,
data: Optional[aiohttp.FormData] = None,
) -> Union[Dict[str, Any], List[Any], bytes, None]:
if not self.accessToken:
return {"error": "Access token is not set."}
url = f"{_CLICKUP_API_BASE}/{path.lstrip('/')}"
headers: Dict[str, str] = {
"Authorization": clickupAuthorizationHeader(self.accessToken),
}
if json_body is not None:
headers["Content-Type"] = "application/json"
timeout = aiohttp.ClientTimeout(total=60)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
kwargs: Dict[str, Any] = {"headers": headers, "params": params}
if json_body is not None:
kwargs["json"] = json_body
if data is not None:
kwargs["data"] = data
async with session.request(method.upper(), url, **kwargs) as resp:
if resp.status == 204:
return {}
text = await resp.text()
if resp.status >= 400:
log = logger.warning if resp.status == 404 else logger.error
log(f"ClickUp API {method} {url} -> {resp.status}: {text[:500]}")
return {"error": f"HTTP {resp.status}", "body": text}
if not text:
return {}
try:
return json.loads(text)
except Exception:
return {"raw": text}
except asyncio.TimeoutError:
return {"error": f"ClickUp API timeout: {path}"}
except Exception as e:
logger.error(f"ClickUp API error: {e}")
return {"error": str(e)}
async def getAuthorizedTeams(self) -> Dict[str, Any]:
return await self._request("GET", "/team")
async def getSpaces(self, teamId: str) -> Dict[str, Any]:
return await self._request("GET", f"/team/{teamId}/space")
async def getFolders(self, spaceId: str) -> Dict[str, Any]:
return await self._request("GET", f"/space/{spaceId}/folder")
async def getFolderlessLists(self, spaceId: str) -> Dict[str, Any]:
return await self._request("GET", f"/space/{spaceId}/list")
async def getListsInFolder(self, folderId: str) -> Dict[str, Any]:
return await self._request("GET", f"/folder/{folderId}/list")
async def getTasksInList(self, listId: str, *, page: int = 0) -> Dict[str, Any]:
params: Dict[str, Any] = {"page": page, "subtasks": "true", "include_closed": "false"}
return await self._request("GET", f"/list/{listId}/task", params=params)
async def getTask(self, taskId: str) -> Dict[str, Any]:
params = {"include_subtasks": "true"}
return await self._request("GET", f"/task/{taskId}", params=params)
async def searchTeamTasks(self, teamId: str, *, query: str, page: int = 0) -> Dict[str, Any]:
params = {"query": query, "page": page}
return await self._request("GET", f"/team/{teamId}/task", params=params)
async def uploadTaskAttachment(self, taskId: str, fileBytes: bytes, fileName: str) -> Dict[str, Any]:
if not self.accessToken:
return {"error": "Access token is not set."}
url = f"{_CLICKUP_API_BASE}/task/{taskId}/attachment"
headers = {"Authorization": clickupAuthorizationHeader(self.accessToken)}
formData = aiohttp.FormData()
formData.add_field("attachment", fileBytes, filename=fileName, content_type="application/octet-stream")
timeout = aiohttp.ClientTimeout(total=120)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(url, headers=headers, data=formData) as resp:
text = await resp.text()
if resp.status >= 400:
return {"error": f"HTTP {resp.status}", "body": text}
return json.loads(text) if text else {}
except Exception as e:
return {"error": str(e)}
class ClickupListsAdapter(ServiceAdapter): class ClickupListsAdapter(ServiceAdapter):
"""Maps ClickUp hierarchy + list tasks to browse/download/upload/search.""" """Maps ClickUp hierarchy + list tasks to browse/download/upload/search."""
def __init__(self, access_token: str): def __init__(self, access_token: str):
self._token = access_token self._token = access_token
self._svc = ClickupApiClient(access_token) # Minimal service instance for API calls (no ServiceCenter context)
self._svc = ClickupService(context=None, get_service=lambda _: None)
self._svc.setAccessToken(access_token)
async def browse( async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
self,
path: str,
filter: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
p = _norm(path) p = _norm(path)
out: List[ExternalEntry] = [] out: List[ExternalEntry] = []
@ -285,11 +173,7 @@ class ClickupListsAdapter(ServiceAdapter):
) )
if len(tasks) < 100: if len(tasks) < 100:
break break
if limit is not None and len(out) >= int(limit):
break
page += 1 page += 1
if limit is not None:
out = out[: max(1, int(limit))]
return out return out
m = re.match(r"^/team/([^/]+)/list/([^/]+)/task/([^/]+)$", p) m = re.match(r"^/team/([^/]+)/list/([^/]+)/task/([^/]+)$", p)
@ -317,9 +201,6 @@ class ClickupListsAdapter(ServiceAdapter):
data = await self._svc.getTask(task_id) data = await self._svc.getTask(task_id)
if isinstance(data, dict) and data.get("error"): if isinstance(data, dict) and data.get("error"):
return json.dumps(data).encode("utf-8") return json.dumps(data).encode("utf-8")
returnedId = data.get("id", "") if isinstance(data, dict) else ""
if returnedId and returnedId != task_id:
logger.warning(f"ClickUp download: requested task_id={task_id} but API returned id={returnedId}")
payload = json.dumps(data, indent=2).encode("utf-8") payload = json.dumps(data, indent=2).encode("utf-8")
return DownloadResult(data=payload, fileName=f"task-{task_id}.json", mimeType="application/json") return DownloadResult(data=payload, fileName=f"task-{task_id}.json", mimeType="application/json")
@ -332,12 +213,7 @@ class ClickupListsAdapter(ServiceAdapter):
task_id = m.group(3) task_id = m.group(3)
return await self._svc.uploadTaskAttachment(task_id, data, fileName) return await self._svc.uploadTaskAttachment(task_id, data, fileName)
async def search( async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
self,
query: str,
path: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
base = _norm(path or "/") base = _norm(path or "/")
team_id: Optional[str] = None team_id: Optional[str] = None
mt = re.match(r"^/team/([^/]+)", base) mt = re.match(r"^/team/([^/]+)", base)
@ -376,11 +252,7 @@ class ClickupListsAdapter(ServiceAdapter):
) )
if len(tasks) < 25: if len(tasks) < 25:
break break
if limit is not None and len(out) >= int(limit):
break
page += 1 page += 1
if limit is not None:
out = out[: max(1, int(limit))]
return out return out

View file

@ -0,0 +1,3 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""FTP/SFTP Provider Connector stub."""

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""FTP/SFTP ProviderConnector stub. """FTP/SFTP ProviderConnector stub.
@ -21,12 +21,7 @@ class FtpFilesAdapter(ServiceAdapter):
def __init__(self, accessToken: str): def __init__(self, accessToken: str):
self._accessToken = accessToken self._accessToken = accessToken
async def browse( async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
self,
path: str,
filter: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
logger.info(f"FTP browse stub: {path}") logger.info(f"FTP browse stub: {path}")
return [] return []
@ -37,12 +32,7 @@ class FtpFilesAdapter(ServiceAdapter):
async def upload(self, path: str, data: bytes, fileName: str) -> dict: async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "FTP upload not yet implemented"} return {"error": "FTP upload not yet implemented"}
async def search( async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
self,
query: str,
path: Optional[str] = None,
limit: Optional[int] = None,
) -> List[ExternalEntry]:
return [] return []

View file

@ -0,0 +1,3 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Google Provider Connector -- 1 Connection : n Services (Drive, Gmail)."""

View file

@ -0,0 +1,265 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Google ProviderConnector -- Drive and Gmail via Google OAuth."""
import logging
from typing import Any, Dict, List, Optional
import aiohttp
from modules.connectors.connectorProviderBase import ProviderConnector, ServiceAdapter, DownloadResult
from modules.datamodels.datamodelDataSource import ExternalEntry
logger = logging.getLogger(__name__)
_DRIVE_BASE = "https://www.googleapis.com/drive/v3"
_GMAIL_BASE = "https://gmail.googleapis.com/gmail/v1"
async def _googleGet(token: str, url: str) -> Dict[str, Any]:
headers = {"Authorization": f"Bearer {token}"}
timeout = aiohttp.ClientTimeout(total=20)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as resp:
if resp.status in (200, 201):
return await resp.json()
errorText = await resp.text()
logger.warning(f"Google API {resp.status}: {errorText[:300]}")
return {"error": f"{resp.status}: {errorText[:200]}"}
except Exception as e:
return {"error": str(e)}
class DriveAdapter(ServiceAdapter):
"""Google Drive ServiceAdapter -- browse files and folders."""
def __init__(self, accessToken: str):
self._token = accessToken
async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
folderId = (path or "").strip("/") or "root"
query = f"'{folderId}' in parents and trashed=false"
fields = "files(id,name,mimeType,size,modifiedTime,parents)"
url = f"{_DRIVE_BASE}/files?q={query}&fields={fields}&pageSize=100&orderBy=folder,name"
result = await _googleGet(self._token, url)
if "error" in result:
logger.warning(f"Google Drive browse failed: {result['error']}")
return []
entries = []
for f in result.get("files", []):
isFolder = f.get("mimeType") == "application/vnd.google-apps.folder"
entries.append(ExternalEntry(
name=f.get("name", ""),
path=f"/{f.get('id', '')}",
isFolder=isFolder,
size=int(f.get("size", 0)) if f.get("size") else None,
mimeType=f.get("mimeType") if not isFolder else None,
metadata={"id": f.get("id"), "modifiedTime": f.get("modifiedTime")},
))
return entries
_EXPORT_MIME_MAP = {
"application/vnd.google-apps.document": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.google-apps.spreadsheet": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.google-apps.presentation": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.google-apps.drawing": "application/pdf",
}
async def download(self, path: str) -> bytes:
fileId = (path or "").strip("/")
if not fileId:
return b""
headers = {"Authorization": f"Bearer {self._token}"}
timeout = aiohttp.ClientTimeout(total=60)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
# Try direct download first
url = f"{_DRIVE_BASE}/files/{fileId}?alt=media"
async with session.get(url, headers=headers) as resp:
if resp.status == 200:
return await resp.read()
logger.debug(f"Google Drive direct download returned {resp.status} for {fileId}")
# If 403/404, check if it's a native Google file that needs export
metaUrl = f"{_DRIVE_BASE}/files/{fileId}?fields=mimeType,name"
async with session.get(metaUrl, headers=headers) as metaResp:
if metaResp.status != 200:
logger.warning(f"Google Drive metadata fetch failed ({metaResp.status}) for {fileId}")
return b""
meta = await metaResp.json()
fileMime = meta.get("mimeType", "")
fileName = meta.get("name", fileId)
exportMime = self._EXPORT_MIME_MAP.get(fileMime)
if not exportMime:
logger.warning(f"Google Drive: unsupported mimeType '{fileMime}' for file '{fileName}' ({fileId})")
return b""
exportUrl = f"{_DRIVE_BASE}/files/{fileId}/export?mimeType={exportMime}"
logger.info(f"Google Drive: exporting '{fileName}' as {exportMime}")
async with session.get(exportUrl, headers=headers) as exportResp:
if exportResp.status == 200:
return await exportResp.read()
logger.warning(f"Google Drive export failed ({exportResp.status}) for '{fileName}'")
except Exception as e:
logger.error(f"Google Drive download failed for {fileId}: {e}")
return b""
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "Google Drive upload not yet implemented"}
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
safeQuery = query.replace("'", "\\'")
folderId = (path or "").strip("/")
qParts = [f"name contains '{safeQuery}'", "trashed=false"]
if folderId:
qParts.append(f"'{folderId}' in parents")
qStr = " and ".join(qParts)
url = f"{_DRIVE_BASE}/files?q={qStr}&fields=files(id,name,mimeType,size)&pageSize=25"
logger.debug(f"Google Drive search: q={qStr}")
result = await _googleGet(self._token, url)
if "error" in result:
return []
return [
ExternalEntry(
name=f.get("name", ""),
path=f"/{f.get('id', '')}",
isFolder=f.get("mimeType") == "application/vnd.google-apps.folder",
size=int(f.get("size", 0)) if f.get("size") else None,
)
for f in result.get("files", [])
]
class GmailAdapter(ServiceAdapter):
"""Gmail ServiceAdapter -- browse labels and messages."""
def __init__(self, accessToken: str):
self._token = accessToken
async def browse(self, path: str, filter: Optional[str] = None) -> list:
cleanPath = (path or "").strip("/")
if not cleanPath:
url = f"{_GMAIL_BASE}/users/me/labels"
result = await _googleGet(self._token, url)
if "error" in result:
logger.warning(f"Gmail labels failed: {result['error']}")
return []
_SYSTEM_LABELS = {"INBOX", "SENT", "DRAFT", "TRASH", "SPAM", "STARRED", "IMPORTANT"}
labels = []
for lbl in result.get("labels", []):
labelId = lbl.get("id", "")
labelName = lbl.get("name", labelId)
if lbl.get("type") == "system" and labelId not in _SYSTEM_LABELS:
continue
labels.append(ExternalEntry(
name=labelName,
path=f"/{labelId}",
isFolder=True,
metadata={"id": labelId, "type": lbl.get("type", "")},
))
labels.sort(key=lambda e: (0 if e.metadata.get("type") == "system" else 1, e.name))
return labels
url = f"{_GMAIL_BASE}/users/me/messages?labelIds={cleanPath}&maxResults=25"
result = await _googleGet(self._token, url)
if "error" in result:
return []
entries = []
for msg in result.get("messages", [])[:25]:
msgId = msg.get("id", "")
detailUrl = f"{_GMAIL_BASE}/users/me/messages/{msgId}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date"
detail = await _googleGet(self._token, detailUrl)
if "error" in detail:
entries.append(ExternalEntry(name=f"Message {msgId}", path=f"/{cleanPath}/{msgId}", isFolder=False))
continue
headers = {h.get("name", ""): h.get("value", "") for h in detail.get("payload", {}).get("headers", [])}
entries.append(ExternalEntry(
name=headers.get("Subject", "(no subject)"),
path=f"/{cleanPath}/{msgId}",
isFolder=False,
metadata={
"id": msgId,
"from": headers.get("From", ""),
"date": headers.get("Date", ""),
"snippet": detail.get("snippet", ""),
},
))
return entries
async def download(self, path: str) -> DownloadResult:
"""Download a Gmail message as RFC 822 EML via format=raw."""
import base64
import re
cleanPath = (path or "").strip("/")
msgId = cleanPath.split("/")[-1] if cleanPath else ""
if not msgId:
return DownloadResult()
url = f"{_GMAIL_BASE}/users/me/messages/{msgId}?format=raw"
result = await _googleGet(self._token, url)
if "error" in result:
return DownloadResult()
rawB64 = result.get("raw", "")
if not rawB64:
return DownloadResult()
emlBytes = base64.urlsafe_b64decode(rawB64)
metaUrl = f"{_GMAIL_BASE}/users/me/messages/{msgId}?format=metadata&metadataHeaders=Subject"
meta = await _googleGet(self._token, metaUrl)
subject = msgId
if "error" not in meta:
for h in meta.get("payload", {}).get("headers", []):
if h.get("name", "").lower() == "subject":
subject = h.get("value", msgId)
break
safeName = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", subject)[:80].strip(". ") or "email"
return DownloadResult(
data=emlBytes,
fileName=f"{safeName}.eml",
mimeType="message/rfc822",
)
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "Gmail upload not applicable"}
async def search(self, query: str, path: Optional[str] = None) -> list:
url = f"{_GMAIL_BASE}/users/me/messages?q={query}&maxResults=10"
result = await _googleGet(self._token, url)
if "error" in result:
return []
return [
ExternalEntry(
name=f"Message {m.get('id', '')}",
path=f"/{m.get('id', '')}",
isFolder=False,
metadata={"id": m.get("id")},
)
for m in result.get("messages", [])
]
class GoogleConnector(ProviderConnector):
"""Google ProviderConnector -- 1 connection -> Drive + Gmail."""
_SERVICE_MAP = {
"drive": DriveAdapter,
"gmail": GmailAdapter,
}
def getAvailableServices(self) -> List[str]:
return list(self._SERVICE_MAP.keys())
def getServiceAdapter(self, service: str) -> ServiceAdapter:
adapterClass = self._SERVICE_MAP.get(service)
if not adapterClass:
raise ValueError(f"Unknown Google service: {service}. Available: {list(self._SERVICE_MAP.keys())}")
return adapterClass(self.accessToken)

View file

@ -0,0 +1,3 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Microsoft Provider Connector -- 1 Connection : n Services (SharePoint, Outlook, Teams, OneDrive)."""

View file

@ -0,0 +1,508 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Microsoft ProviderConnector -- one MSFT connection serves SharePoint, Outlook, Teams, OneDrive.
All ServiceAdapters share the same OAuth access token obtained from the
UserConnection (authority=msft).
"""
import logging
import aiohttp
import asyncio
from typing import Dict, Any, List, Optional
from modules.connectors.connectorProviderBase import ProviderConnector, ServiceAdapter, DownloadResult
from modules.datamodels.datamodelDataSource import ExternalEntry
logger = logging.getLogger(__name__)
_GRAPH_BASE = "https://graph.microsoft.com/v1.0"
class _GraphApiMixin:
"""Shared Graph API call logic for all MSFT service adapters."""
def __init__(self, accessToken: str):
self._accessToken = accessToken
async def _graphGet(self, endpoint: str) -> Dict[str, Any]:
return await _makeGraphCall(self._accessToken, endpoint, "GET")
async def _graphPost(self, endpoint: str, data: Any = None) -> Dict[str, Any]:
return await _makeGraphCall(self._accessToken, endpoint, "POST", data)
async def _graphPut(self, endpoint: str, data: bytes = None) -> Dict[str, Any]:
return await _makeGraphCall(self._accessToken, endpoint, "PUT", data)
async def _graphDelete(self, endpoint: str) -> Dict[str, Any]:
return await _makeGraphCall(self._accessToken, endpoint, "DELETE")
async def _graphDownload(self, endpoint: str) -> Optional[bytes]:
"""Download binary content from Graph API."""
headers = {"Authorization": f"Bearer {self._accessToken}"}
timeout = aiohttp.ClientTimeout(total=60)
url = f"{_GRAPH_BASE}/{endpoint.lstrip('/')}"
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as resp:
if resp.status == 200:
return await resp.read()
logger.error(f"Download failed {resp.status}: {await resp.text()}")
return None
except Exception as e:
logger.error(f"Graph download error: {e}")
return None
async def _makeGraphCall(
token: str, endpoint: str, method: str = "GET", data: Any = None
) -> Dict[str, Any]:
"""Execute a single Microsoft Graph API call."""
url = f"{_GRAPH_BASE}/{endpoint.lstrip('/')}"
contentType = "application/json"
if method == "PUT" and isinstance(data, bytes):
contentType = "application/octet-stream"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": contentType,
}
timeout = aiohttp.ClientTimeout(total=30)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
kwargs: Dict[str, Any] = {"headers": headers}
if data is not None:
kwargs["data"] = data
if method == "GET":
async with session.get(url, **kwargs) as resp:
return await _handleResponse(resp)
elif method == "POST":
async with session.post(url, **kwargs) as resp:
return await _handleResponse(resp)
elif method == "PUT":
async with session.put(url, **kwargs) as resp:
return await _handleResponse(resp)
elif method == "DELETE":
async with session.delete(url, **kwargs) as resp:
if resp.status in (200, 204):
return {}
return await _handleResponse(resp)
except asyncio.TimeoutError:
return {"error": f"Graph API timeout: {endpoint}"}
except Exception as e:
return {"error": f"Graph API error: {e}"}
return {"error": f"Unsupported method: {method}"}
async def _handleResponse(resp: aiohttp.ClientResponse) -> Dict[str, Any]:
if resp.status in (200, 201):
return await resp.json()
errorText = await resp.text()
logger.error(f"Graph API {resp.status}: {errorText}")
return {"error": f"{resp.status}: {errorText}"}
def _graphItemToExternalEntry(item: Dict[str, Any], basePath: str = "") -> ExternalEntry:
isFolder = "folder" in item
return ExternalEntry(
name=item.get("name", ""),
path=f"{basePath}/{item.get('name', '')}" if basePath else item.get("name", ""),
isFolder=isFolder,
size=item.get("size"),
mimeType=item.get("file", {}).get("mimeType") if not isFolder else None,
lastModified=None,
metadata={
"id": item.get("id"),
"webUrl": item.get("webUrl"),
"childCount": item.get("folder", {}).get("childCount") if isFolder else None,
},
)
# ---------------------------------------------------------------------------
# SharePoint Adapter
# ---------------------------------------------------------------------------
class SharepointAdapter(_GraphApiMixin, ServiceAdapter):
"""ServiceAdapter for SharePoint (files, sites) via Microsoft Graph."""
async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
"""List items in a SharePoint folder.
Path format: /sites/<SiteName>/<FolderPath>
Root "/" lists available sites via discovery.
"""
if not path or path == "/":
return await self._discoverSites()
siteId, folderPath = _parseSharepointPath(path)
if not siteId:
return await self._discoverSites()
if not folderPath or folderPath == "/":
endpoint = f"sites/{siteId}/drive/root/children"
else:
cleanPath = folderPath.lstrip("/")
endpoint = f"sites/{siteId}/drive/root:/{cleanPath}:/children"
result = await self._graphGet(endpoint)
if "error" in result:
logger.warning(f"SharePoint browse failed: {result['error']}")
return []
entries = [_graphItemToExternalEntry(item, path) for item in result.get("value", [])]
if filter:
entries = [e for e in entries if _matchFilter(e, filter)]
return entries
async def _discoverSites(self) -> List[ExternalEntry]:
"""Discover accessible SharePoint sites."""
result = await self._graphGet("sites?search=*&$top=50")
if "error" in result:
logger.warning(f"SharePoint site discovery failed: {result['error']}")
return []
return [
ExternalEntry(
name=s.get("displayName") or s.get("name", ""),
path=f"/sites/{s.get('id', '')}",
isFolder=True,
metadata={
"id": s.get("id"),
"webUrl": s.get("webUrl"),
"description": s.get("description", ""),
},
)
for s in result.get("value", [])
if s.get("displayName")
]
async def download(self, path: str) -> bytes:
siteId, filePath = _parseSharepointPath(path)
if not siteId or not filePath:
return b""
cleanPath = filePath.strip("/")
endpoint = f"sites/{siteId}/drive/root:/{cleanPath}:/content"
data = await self._graphDownload(endpoint)
return data or b""
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
siteId, folderPath = _parseSharepointPath(path)
if not siteId:
return {"error": "Invalid SharePoint path"}
cleanFolder = (folderPath or "").strip("/")
uploadPath = f"{cleanFolder}/{fileName}" if cleanFolder else fileName
endpoint = f"sites/{siteId}/drive/root:/{uploadPath}:/content"
result = await self._graphPut(endpoint, data)
return result
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
siteId, _ = _parseSharepointPath(path or "")
if not siteId:
return []
safeQuery = query.replace("'", "''")
endpoint = f"sites/{siteId}/drive/root/search(q='{safeQuery}')"
result = await self._graphGet(endpoint)
if "error" in result:
return []
return [_graphItemToExternalEntry(item) for item in result.get("value", [])]
# ---------------------------------------------------------------------------
# Outlook Adapter
# ---------------------------------------------------------------------------
class OutlookAdapter(_GraphApiMixin, ServiceAdapter):
"""ServiceAdapter for Outlook (mail, calendar) via Microsoft Graph."""
async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
"""List mail folders or messages.
path = "" or "/" list mail folders
path = "/Inbox" list messages in Inbox
"""
if not path or path == "/":
result = await self._graphGet("me/mailFolders")
if "error" in result:
return []
return [
ExternalEntry(
name=f.get("displayName", ""),
path=f"/{f.get('id', '')}",
isFolder=True,
metadata={"id": f.get("id"), "totalItemCount": f.get("totalItemCount")},
)
for f in result.get("value", [])
]
folderId = path.strip("/")
endpoint = f"me/mailFolders/{folderId}/messages?$top=25&$orderby=receivedDateTime desc"
result = await self._graphGet(endpoint)
if "error" in result:
return []
return [
ExternalEntry(
name=m.get("subject", "(no subject)"),
path=f"{path}/{m.get('id', '')}",
isFolder=False,
metadata={
"id": m.get("id"),
"from": m.get("from", {}).get("emailAddress", {}).get("address"),
"receivedDateTime": m.get("receivedDateTime"),
"hasAttachments": m.get("hasAttachments", False),
},
)
for m in result.get("value", [])
]
async def download(self, path: str) -> DownloadResult:
"""Download a mail message as RFC 822 EML via Graph API $value endpoint."""
import re
messageId = path.strip("/").split("/")[-1]
meta = await self._graphGet(f"me/messages/{messageId}?$select=subject")
subject = meta.get("subject", messageId) if "error" not in meta else messageId
safeName = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", subject)[:80].strip(". ") or "email"
emlBytes = await self._graphDownload(f"me/messages/{messageId}/$value")
if not emlBytes:
return DownloadResult()
return DownloadResult(
data=emlBytes,
fileName=f"{safeName}.eml",
mimeType="message/rfc822",
)
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
"""Not applicable for Outlook in the file sense."""
return {"error": "Upload not supported for Outlook"}
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
safeQuery = query.replace("'", "''")
endpoint = f"me/messages?$search=\"{safeQuery}\"&$top=25"
result = await self._graphGet(endpoint)
if "error" in result:
return []
return [
ExternalEntry(
name=m.get("subject", "(no subject)"),
path=f"/search/{m.get('id', '')}",
isFolder=False,
metadata={
"id": m.get("id"),
"from": m.get("from", {}).get("emailAddress", {}).get("address"),
"receivedDateTime": m.get("receivedDateTime"),
},
)
for m in result.get("value", [])
]
def _buildMessage(
self, to: List[str], subject: str, body: str,
bodyType: str = "Text",
cc: Optional[List[str]] = None,
attachments: Optional[List[Dict]] = None,
) -> Dict[str, Any]:
"""Build a Graph API message object.
attachments: list of {"name": str, "contentBytes": str (base64), "contentType": str}
"""
message: Dict[str, Any] = {
"subject": subject,
"body": {"contentType": bodyType, "content": body},
"toRecipients": [{"emailAddress": {"address": addr}} for addr in to],
}
if cc:
message["ccRecipients"] = [{"emailAddress": {"address": addr}} for addr in cc]
if attachments:
message["attachments"] = [
{
"@odata.type": "#microsoft.graph.fileAttachment",
"name": att["name"],
"contentBytes": att["contentBytes"],
"contentType": att.get("contentType", "application/octet-stream"),
}
for att in attachments
]
return message
async def sendMail(
self, to: List[str], subject: str, body: str,
bodyType: str = "Text",
cc: Optional[List[str]] = None,
attachments: Optional[List[Dict]] = None,
) -> Dict[str, Any]:
"""Send an email via Microsoft Graph. bodyType: 'Text' or 'HTML'."""
import json
message = self._buildMessage(to, subject, body, bodyType, cc, attachments)
payload = json.dumps({"message": message, "saveToSentItems": True}).encode("utf-8")
result = await self._graphPost("me/sendMail", payload)
if "error" in result:
return result
return {"success": True}
async def createDraft(
self, to: List[str], subject: str, body: str,
bodyType: str = "Text",
cc: Optional[List[str]] = None,
attachments: Optional[List[Dict]] = None,
) -> Dict[str, Any]:
"""Create a draft email in the user's Drafts folder via Microsoft Graph."""
import json
message = self._buildMessage(to, subject, body, bodyType, cc, attachments)
payload = json.dumps(message).encode("utf-8")
result = await self._graphPost("me/messages", payload)
if "error" in result:
return result
return {"success": True, "draft": True, "messageId": result.get("id", "")}
# ---------------------------------------------------------------------------
# Teams Adapter (Stub)
# ---------------------------------------------------------------------------
class TeamsAdapter(_GraphApiMixin, ServiceAdapter):
"""ServiceAdapter for Microsoft Teams -- browse joined teams and channels."""
async def browse(self, path: str, filter: Optional[str] = None) -> list:
cleanPath = (path or "").strip("/")
if not cleanPath:
result = await self._graphGet("me/joinedTeams")
if "error" in result:
logger.warning(f"Teams browse failed: {result['error']}")
return []
return [
ExternalEntry(
name=t.get("displayName", ""),
path=f"/{t.get('id', '')}",
isFolder=True,
metadata={"id": t.get("id"), "description": t.get("description", "")},
)
for t in result.get("value", [])
]
parts = cleanPath.split("/", 1)
teamId = parts[0]
if len(parts) == 1:
result = await self._graphGet(f"teams/{teamId}/channels")
if "error" in result:
return []
return [
ExternalEntry(
name=ch.get("displayName", ""),
path=f"/{teamId}/{ch.get('id', '')}",
isFolder=True,
metadata={"id": ch.get("id"), "membershipType": ch.get("membershipType", "")},
)
for ch in result.get("value", [])
]
return []
async def download(self, path: str) -> bytes:
return b""
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
return {"error": "Teams upload not implemented"}
async def search(self, query: str, path: Optional[str] = None) -> list:
return []
# ---------------------------------------------------------------------------
# OneDrive Adapter (Stub -- similar to SharePoint but personal drive)
# ---------------------------------------------------------------------------
class OneDriveAdapter(_GraphApiMixin, ServiceAdapter):
"""ServiceAdapter stub for OneDrive (personal drive)."""
async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
cleanPath = (path or "").strip("/")
if not cleanPath:
endpoint = "me/drive/root/children"
else:
endpoint = f"me/drive/root:/{cleanPath}:/children"
result = await self._graphGet(endpoint)
if "error" in result:
return []
entries = [_graphItemToExternalEntry(item, path) for item in result.get("value", [])]
if filter:
entries = [e for e in entries if _matchFilter(e, filter)]
return entries
async def download(self, path: str) -> bytes:
cleanPath = (path or "").strip("/")
if not cleanPath:
return b""
data = await self._graphDownload(f"me/drive/root:/{cleanPath}:/content")
return data or b""
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
cleanPath = (path or "").strip("/")
uploadPath = f"{cleanPath}/{fileName}" if cleanPath else fileName
endpoint = f"me/drive/root:/{uploadPath}:/content"
return await self._graphPut(endpoint, data)
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
safeQuery = query.replace("'", "''")
endpoint = f"me/drive/root/search(q='{safeQuery}')"
result = await self._graphGet(endpoint)
if "error" in result:
return []
return [_graphItemToExternalEntry(item) for item in result.get("value", [])]
# ---------------------------------------------------------------------------
# MsftConnector (1:n)
# ---------------------------------------------------------------------------
class MsftConnector(ProviderConnector):
"""Microsoft ProviderConnector -- 1 connection → n services."""
_SERVICE_MAP = {
"sharepoint": SharepointAdapter,
"outlook": OutlookAdapter,
"teams": TeamsAdapter,
"onedrive": OneDriveAdapter,
}
def getAvailableServices(self) -> List[str]:
return list(self._SERVICE_MAP.keys())
def getServiceAdapter(self, service: str) -> ServiceAdapter:
adapterClass = self._SERVICE_MAP.get(service)
if not adapterClass:
raise ValueError(f"Unknown MSFT service: {service}. Available: {list(self._SERVICE_MAP.keys())}")
return adapterClass(self.accessToken)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _parseSharepointPath(path: str) -> tuple:
"""Parse a SharePoint path into (siteId, innerPath).
Expected format: /sites/<siteId>/<innerPath>
Also accepts bare siteId if no /sites/ prefix.
"""
if not path:
return ("", "")
clean = path.strip("/")
if clean.startswith("sites/"):
parts = clean.split("/", 2)
siteId = parts[1] if len(parts) > 1 else ""
innerPath = parts[2] if len(parts) > 2 else ""
return (siteId, innerPath)
parts = clean.split("/", 1)
return (parts[0], parts[1] if len(parts) > 1 else "")
def _matchFilter(entry: ExternalEntry, pattern: str) -> bool:
"""Simple glob-like filter (supports * wildcard)."""
import fnmatch
return fnmatch.fnmatch(entry.name.lower(), pattern.lower())

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Unified modules.datamodels package. Unified modules.datamodels package.
@ -13,5 +13,4 @@ from . import datamodelSecurity as security
from . import datamodelChat as chat from . import datamodelChat as chat
from . import datamodelFiles as files from . import datamodelFiles as files
from . import datamodelVoice as voice from . import datamodelVoice as voice
from . import datamodelUtils as utils from . import datamodelUtils as utils
from . import jsonContinuation

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
from typing import Optional, List, Dict, Any, Callable, TYPE_CHECKING, Tuple from typing import Optional, List, Dict, Any, Callable, TYPE_CHECKING, Tuple
from pydantic import BaseModel, Field, ConfigDict from pydantic import BaseModel, Field, ConfigDict
@ -125,7 +125,7 @@ class AiModel(BaseModel):
# Metadata # Metadata
version: Optional[str] = Field(default=None, description="Model version") version: Optional[str] = Field(default=None, description="Model version")
lastUpdated: Optional[float] = Field(default=None, description="Last update timestamp (UTC unix)", json_schema_extra={"frontend_type": "timestamp"}) lastUpdated: Optional[str] = Field(default=None, description="Last update timestamp")
model_config = ConfigDict(arbitrary_types_allowed=True) # Allow Callable type model_config = ConfigDict(arbitrary_types_allowed=True) # Allow Callable type
@ -162,7 +162,6 @@ class AiCallOptions(BaseModel):
# Provider filtering (from UI multiselect or automation config) # Provider filtering (from UI multiselect or automation config)
allowedProviders: Optional[List[str]] = Field(default=None, description="List of allowed AI providers to use (empty = all RBAC-permitted)") allowedProviders: Optional[List[str]] = Field(default=None, description="List of allowed AI providers to use (empty = all RBAC-permitted)")
allowedModels: Optional[List[str]] = Field(default=None, description="Whitelist of allowed model names (AND-filter with allowedProviders). None/empty = all allowed.")
class AiCallRequest(BaseModel): class AiCallRequest(BaseModel):
@ -245,10 +244,11 @@ class AiCallPromptWebCrawl(BaseModel):
class AiCallPromptImage(BaseModel): class AiCallPromptImage(BaseModel):
"""Structured prompt format for image generation.""" """Structured prompt format for image generation."""
prompt: str = Field(description="Text description of the image to generate") prompt: str = Field(description="Text description of the image to generate")
size: Optional[str] = Field(default="1024x1024", description="Image size (1024x1024, 1536x1024, 1024x1536)") size: Optional[str] = Field(default="1024x1024", description="Image size (1024x1024, 1792x1024, 1024x1792)")
quality: Optional[str] = Field(default="auto", description="Image quality (auto, high, medium, low)") quality: Optional[str] = Field(default="standard", description="Image quality (standard, hd)")
style: Optional[str] = Field(default="vivid", description="Image style (vivid, natural)")
class AiProcessParameters(BaseModel): class AiProcessParameters(BaseModel):
@ -351,4 +351,4 @@ class CodeContentPromptArgs(BaseModel):
class CodeStructurePromptArgs(BaseModel): class CodeStructurePromptArgs(BaseModel):
"""Type-safe arguments for code structure prompt builder.""" """Type-safe arguments for code structure prompt builder."""
userPrompt: str userPrompt: str
contentParts: List[ContentPart] = Field(default_factory=list) contentParts: List[ContentPart] = Field(default_factory=list)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2026 PowerOn AG # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
"""AI Audit Log data model for Compliance & AI-Datenfluss tracking. """AI Audit Log data model for Compliance & AI-Datenfluss tracking.
@ -9,15 +9,14 @@ for compliance, audit, and data-protection reporting.
import uuid import uuid
from typing import Optional from typing import Optional
from pydantic import Field from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
from modules.shared.i18nRegistry import i18nModel from modules.shared.i18nRegistry import i18nModel
from modules.shared.timeUtils import getUtcTimestamp from modules.shared.timeUtils import getUtcTimestamp
@i18nModel("AI-Audit-Eintrag") @i18nModel("AI-Audit-Eintrag")
class AiAuditLogEntry(PowerOnModel): class AiAuditLogEntry(BaseModel):
id: str = Field( id: str = Field(
default_factory=lambda: str(uuid.uuid4()), default_factory=lambda: str(uuid.uuid4()),
description="Primary key", description="Primary key",
@ -35,7 +34,7 @@ class AiAuditLogEntry(PowerOnModel):
userId: str = Field( userId: str = Field(
description="ID of the user who triggered the AI call", description="ID of the user who triggered the AI call",
json_schema_extra={"label": "Benutzer-ID", "fk_target": {"db": "poweron_app", "table": "UserInDB", "labelField": "username", "softFk": True}}, json_schema_extra={"label": "Benutzer-ID", "fk_target": {"db": "poweron_app", "table": "User"}},
) )
username: Optional[str] = Field( username: Optional[str] = Field(
default=None, default=None,
@ -44,17 +43,17 @@ class AiAuditLogEntry(PowerOnModel):
) )
mandateId: str = Field( mandateId: str = Field(
description="Mandate context of the call", description="Mandate context of the call",
json_schema_extra={"label": "Mandanten-ID", "fk_target": {"db": "poweron_app", "table": "Mandate", "labelField": "label", "softFk": True}}, json_schema_extra={"label": "Mandanten-ID", "fk_target": {"db": "poweron_app", "table": "Mandate"}},
) )
featureInstanceId: Optional[str] = Field( featureInstanceId: Optional[str] = Field(
default=None, default=None,
description="Feature instance context", description="Feature instance context",
json_schema_extra={"label": "Feature-Instanz-ID", "fk_target": {"db": "poweron_app", "table": "FeatureInstance", "labelField": "label", "softFk": True}}, json_schema_extra={"label": "Feature-Instanz-ID", "fk_target": {"db": "poweron_app", "table": "FeatureInstance"}},
) )
featureCode: Optional[str] = Field( featureCode: Optional[str] = Field(
default=None, default=None,
description="Feature code (e.g. workspace, trustee)", description="Feature code (e.g. workspace, trustee)",
json_schema_extra={"label": "Feature", "fk_target": {"db": "poweron_app", "table": "Feature", "column": "code", "labelField": "code", "softFk": True}}, json_schema_extra={"label": "Feature", "fk_target": {"db": "poweron_app", "table": "Feature", "column": "code"}},
) )
instanceLabel: Optional[str] = Field( instanceLabel: Optional[str] = Field(
default=None, default=None,

Some files were not shown because too many files have changed in this diff Show more