Compare commits
242 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d842884ccf | |||
| fc6de11c37 | |||
| 12c1d768ac | |||
| 30db7a310c | |||
| dce41a01ac | |||
| 06e68c343b | |||
| ebc4b2a080 | |||
| 4a60086c80 | |||
| 26dd8f6f3f | |||
| 4f8473bd70 | |||
| c1655bdd0a | |||
| e0caad0a75 | |||
| ce612ffcfc | |||
| 9be2d8aab5 | |||
| 39aba4cca8 | |||
| 2b208ee504 | |||
| 877f859f6b | |||
| cf0233f193 | |||
| bc7c6fe27c | |||
| 10fad32049 | |||
| 10f172e950 | |||
| e006d85302 | |||
| 74f6f35ad4 | |||
| 4e33cc26dd | |||
| 76753f6037 | |||
| 9b674027a0 | |||
| f766219d4b | |||
| 5eac61f734 | |||
|
|
b1d4137935 | ||
| 2510493891 | |||
| 08fa70e4e0 | |||
| ae81d27295 | |||
| b7503e0272 | |||
| 60bb771158 | |||
| 2eb1a5589d | |||
| 7d207677fd | |||
| 67806e5323 | |||
| d61e29bcac | |||
| 24899b0cf2 | |||
| a6c89c5159 | |||
| 33dd694ba1 | |||
| a7b5192e25 | |||
| 92a4c27afe | |||
| 3b0881b0ca | |||
| 3345f65c40 | |||
| cb6b88aa3c | |||
| 51ac15e501 | |||
| bf261d6566 | |||
| 14714f3ee2 | |||
| a2c5360364 | |||
| b04211bed4 | |||
| 8c2e9d2183 | |||
| 3e2c07a776 | |||
| f24b67ed85 | |||
| 2b58f7a45d | |||
| 2d796a34ed | |||
| 6ab51cf67e | |||
| 19bc4819ee | |||
| 51b789b5aa | |||
| 513ded84d5 | |||
| da1f3f53d0 | |||
| 060ca72eb4 | |||
| 6b5e386469 | |||
| 1a1128cc8c | |||
| e2d230f2c6 | |||
| 0c7ab77728 | |||
| 1053d0c715 | |||
| ac85c8e3dc | |||
| 9719a22581 | |||
| c2443a7781 | |||
| 31955751fb | |||
| a46e12638e | |||
| afbb8177a3 | |||
| e7874d8e38 | |||
| c4a9a66c60 | |||
| 59ad6f3849 | |||
| bc6bb44d6d | |||
| 8bc1dd22f1 | |||
| c990dd0317 | |||
| 79ec552264 | |||
| 906870faa8 | |||
| a59ee53e3c | |||
| c3530fe2aa | |||
| 2cfbb41cdf | |||
| ca261c1f5f | |||
| e800bc0b71 | |||
| 94ce05c443 | |||
| bc8b0288ca | |||
| d82fc0d955 | |||
| 0c2082896c | |||
| f67dfb3245 | |||
| f1cb455ccd | |||
| 56639922e9 | |||
| 7624af5b46 | |||
| 5a99d73f93 | |||
| c01189ec68 | |||
| 149934b730 | |||
| 3725ca1a02 | |||
| b888035261 | |||
| 0ea5af4ce8 | |||
| af383fdfb5 | |||
| fe4d7afd40 | |||
| 4bd5171644 | |||
| 9d3185976b | |||
| c4883ef22e | |||
| 76cb841973 | |||
| 575e5b6fbf | |||
| f468a377e4 | |||
|
|
45091dc596 | ||
|
|
09c6d33dec | ||
|
|
a173fab15f | ||
|
|
9773c00bca | ||
|
|
1ed462ad13 | ||
|
|
4064ac0266 | ||
| 26505ba7af | |||
|
|
2bb65c2303 | ||
|
|
a31e0dadc3 | ||
|
|
f5aba4bf99 | ||
|
|
7c4c5e079a | ||
| 2f8abb5ac4 | |||
|
|
d1b13f8574 | ||
|
|
4a5028405f | ||
|
|
67ba8847dc | ||
|
|
f121c99ab7 | ||
|
|
3b0192e428 | ||
|
|
ab43b42aa9 | ||
|
|
e3284994d0 | ||
|
|
16ab816c65 | ||
|
|
3718745931 | ||
|
|
6380f14ebe | ||
|
|
c130f49cf9 | ||
|
|
48c0f900af | ||
|
|
03a6d3248b | ||
|
|
75e07743a6 | ||
|
|
7d43d032c3 | ||
|
|
57a9257047 | ||
|
|
fac191fc77 | ||
|
|
df9a43c190 | ||
|
|
92dc6172f2 | ||
|
|
1aec73f110 | ||
|
|
ca6d8b9635 | ||
|
|
f2c3090070 | ||
|
|
35693a61e3 | ||
|
|
0403a19c22 | ||
|
|
130bdfb7cc | ||
|
|
73fe11230d | ||
|
|
513d879ae8 | ||
|
|
436547d47e | ||
|
|
e9c39f8e31 | ||
|
|
cfd303792f | ||
| dac9911f8b | |||
| 5455e09367 | |||
| 9ae2ffc415 | |||
| f184da9898 | |||
| e6ca6a9d8e | |||
| f96325f804 | |||
| 60b2fcf56b | |||
|
|
e07ac24fd8 | ||
|
|
2f87fae44d | ||
|
|
64ee5200af | ||
|
|
e93ce71174 | ||
|
|
3da6e24bec | ||
|
|
d3d682fe4d | ||
|
|
7942766931 | ||
|
|
c140bd14d4 | ||
| 06d9910ecd | |||
|
|
b500bfa6c1 | ||
|
|
afd7e9d941 | ||
|
|
b12671bbb5 | ||
|
|
880fa4d787 | ||
| 72d3175f49 | |||
| ce671f61b6 | |||
| 4a840e9e6e | |||
| 93cb6939dc | |||
| 3add5c9a80 | |||
| 6a5ff1ff7c | |||
| dff3d41845 | |||
| a7f4055130 | |||
| 078b4eaaaf | |||
| 9d82d3d353 | |||
|
|
1369812cef | ||
|
|
ba21005401 | ||
|
|
052647a52b | ||
|
|
49f3660d89 | ||
|
|
9816f13ae9 | ||
|
|
b405cebdec | ||
|
|
fb3a1f0a51 | ||
|
|
15dbb431ca | ||
|
|
4d7ccb0418 | ||
|
|
d9fcea54ff | ||
|
|
e8abd553d0 | ||
|
|
30ea8bbefe | ||
|
|
96e2356ddd | ||
|
|
e499cd47d8 | ||
|
|
3507c16055 | ||
|
|
f8853d23ca | ||
|
|
d505ffd9cd | ||
|
|
60d5062204 | ||
|
|
564a1200c6 | ||
|
|
8221a0da3e | ||
|
|
24f0c3e2eb | ||
|
|
794ba36f27 | ||
|
|
b6be8f391e | ||
| 269d0fa5ff | |||
| 1f5b5b5580 | |||
| c42f0403df | |||
| e9ceac139a | |||
| dd2c771cb8 | |||
| e8adf18b0f | |||
|
|
ea566c270f | ||
|
|
af68f6a8c8 | ||
|
|
d3551f0287 | ||
|
|
4dc43b5e8f | ||
|
|
71f4265e06 | ||
|
|
908be0511b | ||
|
|
dc0346904f | ||
|
|
2f1e4a24de | ||
|
|
1a675425a6 | ||
|
|
be43876461 | ||
| dc8cddf4ed | |||
|
|
e3c74329e5 | ||
|
|
5ef311a82e | ||
|
|
67cd15d8ea | ||
|
|
7fd942a1b5 | ||
|
|
d0735ad342 | ||
|
|
cdca242f82 | ||
| 2fe50f648a | |||
| 56068a8174 | |||
| 1c298528a1 | |||
| ef10db5fee | |||
| 8f900c9b54 | |||
| 5354694a14 | |||
| adc4007546 | |||
| 43afbfcdd0 | |||
| e8d3fd72a7 | |||
| 6fd9ce736d | |||
|
|
755e76add5 | ||
|
|
507fd54388 | ||
|
|
99b9dc97b7 | ||
|
|
f29e0c9edc | ||
|
|
b84ca37d99 | ||
|
|
daf76fd166 |
909 changed files with 100317 additions and 55321 deletions
|
|
@ -29,10 +29,10 @@ ENV
|
|||
*.swo
|
||||
*~
|
||||
|
||||
# Environment files (env_gcp.env will be copied as .env by workflow)
|
||||
env_*.env
|
||||
# Environment files (env-gateway-*.env will be copied as .env by workflow)
|
||||
env-*.env
|
||||
.env.local
|
||||
# Note: .env is NOT ignored - it will be created from env_gcp.env by the workflow
|
||||
# Note: .env is NOT ignored - it will be created from env-gateway-*.env by the workflow
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
|
|
|||
58
.forgejo/workflows/int_porta-int-platform-core.yml
Normal file
58
.forgejo/workflows/int_porta-int-platform-core.yml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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
|
||||
"
|
||||
58
.forgejo/workflows/main_porta-main-platform-core.yml
Normal file
58
.forgejo/workflows/main_porta-main-platform-core.yml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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
|
||||
"
|
||||
|
|
@ -30,7 +30,7 @@ ENV
|
|||
*~
|
||||
|
||||
# Environment files (will be handled separately)
|
||||
env_*.env
|
||||
env-*.env
|
||||
.env.local
|
||||
|
||||
# Logs
|
||||
|
|
|
|||
151
.github/workflows/deploy-gcp.yml
vendored
151
.github/workflows/deploy-gcp.yml
vendored
|
|
@ -1,151 +0,0 @@
|
|||
# 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
88
.github/workflows/int_gateway-int.yml
vendored
|
|
@ -1,88 +0,0 @@
|
|||
# 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
88
.github/workflows/main_gateway-prod.yml
vendored
|
|
@ -1,88 +0,0 @@
|
|||
# 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 }}
|
||||
51
.github/workflows/update-requirements-lock.yml
vendored
51
.github/workflows/update-requirements-lock.yml
vendored
|
|
@ -1,51 +0,0 @@
|
|||
# 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
2
.gitignore
vendored
|
|
@ -131,7 +131,7 @@ env.bak/
|
|||
venv.bak/
|
||||
|
||||
# Don't ignore environment templates
|
||||
!env*.env
|
||||
!env-*.env
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ COPY requirements.lock .
|
|||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.lock
|
||||
|
||||
# Copy application code (includes .env file created by workflow from env_gcp.env)
|
||||
# Copy application code (includes .env file created by workflow from env-gateway-*.env)
|
||||
COPY . .
|
||||
|
||||
# Create directories for logs (Cloud Run uses /tmp for writable storage)
|
||||
RUN mkdir -p /tmp/logs /tmp/debug
|
||||
|
||||
# Note: .env file (created from env_gcp.env by workflow) contains encrypted secrets
|
||||
# Note: .env file (created from env-gateway-*.env by workflow) contains encrypted secrets
|
||||
# These are decrypted at runtime using the master key from Secret Manager
|
||||
# (mounted as CONFIG_KEY environment variable in Cloud Run)
|
||||
|
||||
|
|
@ -46,5 +46,4 @@ 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
|
||||
|
||||
# Run the application
|
||||
# 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
|
||||
CMD exec gunicorn app:app --bind 0.0.0.0:${PORT:-8000} --timeout 600 --worker-class uvicorn.workers.UvicornWorker --workers 1
|
||||
|
|
|
|||
343
app.py
343
app.py
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -61,6 +61,13 @@ class DailyRotatingFileHandler(RotatingFileHandler):
|
|||
return True
|
||||
return False
|
||||
|
||||
def doRollover(self):
|
||||
"""Size-based rollover that tolerates Windows file locks."""
|
||||
try:
|
||||
super().doRollover()
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit a log record, switching files if date has changed"""
|
||||
# Check if we need to switch to a new file
|
||||
|
|
@ -282,7 +289,7 @@ initLogging()
|
|||
logger = logging.getLogger(__name__)
|
||||
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
|
||||
|
||||
# Pre-warm AI connectors on process load (before lifespan). Critical for chatbot latency.
|
||||
# Pre-warm AI connectors on process load (before lifespan). Critical for AI/agent latency.
|
||||
try:
|
||||
import modules.aicore.aicoreModelRegistry # noqa: F401
|
||||
logger.info("AI connectors pre-warm (app load) triggered")
|
||||
|
|
@ -294,8 +301,41 @@ except Exception as e:
|
|||
async def lifespan(app: FastAPI):
|
||||
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.
|
||||
|
||||
# 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.)
|
||||
# This must happen before getting root interface
|
||||
from modules.security.rootAccess import getRootDbAppConnector
|
||||
|
|
@ -310,18 +350,49 @@ async def lifespan(app: FastAPI):
|
|||
# Register all feature definitions in RBAC catalog (for /api/features/ endpoint)
|
||||
try:
|
||||
from modules.security.rbacCatalog import getCatalogService
|
||||
from modules.system.registry import registerAllFeaturesInCatalog
|
||||
from modules.system.registry import registerAllFeaturesInCatalog, syncCatalogFeaturesToDb
|
||||
catalogService = getCatalogService()
|
||||
registerAllFeaturesInCatalog(catalogService)
|
||||
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:
|
||||
logger.error(f"Feature catalog registration failed: {e}")
|
||||
|
||||
# Sync gateway i18n registry to DB and load translation cache
|
||||
try:
|
||||
from modules.shared.i18nRegistry import _syncRegistryToDb, _loadCache
|
||||
await _syncRegistryToDb()
|
||||
await _loadCache()
|
||||
from modules.system.i18nBootSync import syncRegistryToDb, loadCache
|
||||
from modules.serviceCenter.registry import IMPORTABLE_SERVICES
|
||||
serviceLabels = [svc.get("label") for svc in IMPORTABLE_SERVICES.values()]
|
||||
|
||||
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")
|
||||
except Exception as e:
|
||||
logger.warning(f"i18n registry sync failed (non-critical): {e}")
|
||||
|
|
@ -353,14 +424,74 @@ async def lifespan(app: FastAPI):
|
|||
except Exception as 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 ---
|
||||
import asyncio
|
||||
try:
|
||||
main_loop = asyncio.get_running_loop()
|
||||
eventManager.set_event_loop(main_loop)
|
||||
from modules.workflows.scheduler.mainScheduler import setMainLoop as setSchedulerMainLoop
|
||||
from modules.workflowAutomation.scheduler.mainScheduler import setMainLoop as setSchedulerMainLoop, setOnRunFailedCallback
|
||||
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
|
||||
# when clients (browsers) close connections abruptly. This is a known
|
||||
# asyncio issue on Windows: https://bugs.python.org/issue39010
|
||||
|
|
@ -370,35 +501,138 @@ async def lifespan(app: FastAPI):
|
|||
return
|
||||
if isinstance(exc, ConnectionAbortedError):
|
||||
return
|
||||
if exc and "LocalProtocolError" in type(exc).__name__:
|
||||
return
|
||||
loop.default_exception_handler(ctx)
|
||||
main_loop.set_exception_handler(_suppressClientDisconnect)
|
||||
except RuntimeError:
|
||||
pass
|
||||
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
|
||||
from modules.shared.auditLogger import registerAuditLogCleanupScheduler
|
||||
from modules.dbHelpers.auditLogger import 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
|
||||
|
||||
# --- Stop Managers ---
|
||||
eventManager.stop()
|
||||
|
||||
# --- Stop Feature Containers (Plug&Play) ---
|
||||
# --- Shutdown sequence (protected against CancelledError) ---
|
||||
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}")
|
||||
|
||||
logger.info("Application has been shut down")
|
||||
# 1. Drain SSE queues and cancel agent tasks FIRST so that open
|
||||
# streaming connections break out of their queue.get() loop
|
||||
# immediately. Without this, uvicorn waits for the SSE generators
|
||||
# to finish (up to 120 s keepalive timeout) before the rest of
|
||||
# the shutdown can proceed.
|
||||
try:
|
||||
from modules.shared.eventManager import get_event_manager as _getStreamingEM
|
||||
_getStreamingEM().shutdown()
|
||||
except Exception as e:
|
||||
logger.warning(f"Streaming EventManager shutdown failed: {e}")
|
||||
|
||||
# 2. Signal DB layer to abort in-flight borrow waits immediately.
|
||||
# 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
|
||||
|
|
@ -471,8 +705,8 @@ def getAllowedOrigins():
|
|||
|
||||
|
||||
# CORS origin regex pattern for wildcard subdomain support
|
||||
# Matches all subdomains of poweron.swiss and poweron-center.net
|
||||
CORS_ORIGIN_REGEX = r"https://.*\.(poweron\.swiss|poweron-center\.net)"
|
||||
# Matches all subdomains of poweron.swiss
|
||||
CORS_ORIGIN_REGEX = r"https://.*\.poweron\.swiss"
|
||||
|
||||
|
||||
# SlowAPI rate limiter initialization
|
||||
|
|
@ -502,14 +736,18 @@ from modules.auth import (
|
|||
ProactiveTokenRefreshMiddleware,
|
||||
)
|
||||
|
||||
# i18n language detection middleware (sets per-request language from Accept-Language header)
|
||||
from modules.shared.i18nRegistry import _setLanguage, normalizePrimaryLanguageTag
|
||||
# Per-request context middleware: language (Accept-Language) + user timezone (X-User-Timezone).
|
||||
# Both are written into ContextVars and consumed by t() / resolveText() and getRequestNow()
|
||||
# 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")
|
||||
async def _i18nMiddleware(request: Request, call_next):
|
||||
async def _requestContextMiddleware(request: Request, call_next):
|
||||
acceptLang = request.headers.get("Accept-Language", "")
|
||||
lang = normalizePrimaryLanguageTag(acceptLang, "de")
|
||||
_setLanguage(lang)
|
||||
setLanguage(lang)
|
||||
setRequestTimezone(request.headers.get("X-User-Timezone", ""))
|
||||
return await call_next(request)
|
||||
|
||||
app.add_middleware(CSRFMiddleware)
|
||||
|
|
@ -555,15 +793,27 @@ app.include_router(fileRouter)
|
|||
from modules.routes.routeDataSources import router as 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
|
||||
app.include_router(promptRouter)
|
||||
|
||||
from modules.routes.routeDataConnections import router as 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
|
||||
app.include_router(localRouter)
|
||||
|
||||
from modules.routes.routeMfa import router as mfaRouter
|
||||
app.include_router(mfaRouter)
|
||||
|
||||
from modules.routes.routeSecurityMsft import router as msftRouter
|
||||
app.include_router(msftRouter)
|
||||
|
||||
|
|
@ -573,6 +823,9 @@ app.include_router(googleRouter)
|
|||
from modules.routes.routeSecurityClickup import router as 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
|
||||
app.include_router(clickupApiRouter)
|
||||
|
||||
|
|
@ -627,6 +880,9 @@ app.include_router(billingRouter)
|
|||
from modules.routes.routeSubscription import router as subscriptionRouter
|
||||
app.include_router(subscriptionRouter)
|
||||
|
||||
from modules.routes.routeJobs import router as jobsRouter
|
||||
app.include_router(jobsRouter)
|
||||
|
||||
# ============================================================================
|
||||
# SYSTEM ROUTES (Navigation, etc.)
|
||||
# ============================================================================
|
||||
|
|
@ -634,8 +890,8 @@ from modules.routes.routeSystem import router as systemRouter, navigationRouter
|
|||
app.include_router(systemRouter)
|
||||
app.include_router(navigationRouter)
|
||||
|
||||
from modules.routes.routeWorkflowDashboard import router as workflowDashboardRouter
|
||||
app.include_router(workflowDashboardRouter)
|
||||
from modules.routes.routeWorkflowAutomation import router as workflowAutomationRouter
|
||||
app.include_router(workflowAutomationRouter)
|
||||
|
||||
# ============================================================================
|
||||
# PLUG&PLAY FEATURE ROUTERS
|
||||
|
|
@ -644,4 +900,23 @@ app.include_router(workflowDashboardRouter)
|
|||
from modules.system.registry import loadFeatureRouters
|
||||
|
||||
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)
|
||||
|
|
|
|||
BIN
assets/fonts/NotoEmoji-Regular.ttf
Normal file
BIN
assets/fonts/NotoEmoji-Regular.ttf
Normal file
Binary file not shown.
|
|
@ -45,11 +45,6 @@ Connector_StacSwisstopo_MAX_RETRIES = 3
|
|||
Connector_StacSwisstopo_RETRY_DELAY = 1.0
|
||||
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_CompanyName = PowerOn AG
|
||||
Operator_Address = Birmensdorferstrasse 94, 8003 Zürich
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""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).
|
||||
|
|
|
|||
127
demoData/pwg/_generateScans.py
Normal file
127
demoData/pwg/_generateScans.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# 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()
|
||||
68
demoData/pwg/_seedTrusteeData.json
Normal file
68
demoData/pwg/_seedTrusteeData.json
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"_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"
|
||||
}
|
||||
]
|
||||
}
|
||||
80
demoData/pwg/scans/mieter01-bestaetigt.pdf
Normal file
80
demoData/pwg/scans/mieter01-bestaetigt.pdf
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
%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
|
||||
80
demoData/pwg/scans/mieter02-abweichung-betrag.pdf
Normal file
80
demoData/pwg/scans/mieter02-abweichung-betrag.pdf
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
%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
|
||||
74
demoData/pwg/scans/mieter03-keine-unterschrift.pdf
Normal file
74
demoData/pwg/scans/mieter03-keine-unterschrift.pdf
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
%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
|
||||
152
demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json
Normal file
152
demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
{
|
||||
"$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": []
|
||||
}
|
||||
BIN
docs/billing-ui-tests.xlsx
Normal file
BIN
docs/billing-ui-tests.xlsx
Normal file
Binary file not shown.
225
docs/briefing-abacus-c-level.md
Normal file
225
docs/briefing-abacus-c-level.md
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
# 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) | 4–6 Wochen |
|
||||
| 3 | Strategie-Workshop zu JV-Modell (Thesen A–D) | 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.*
|
||||
100
docs/brochure-poweron-investor-clevel.md
Normal file
100
docs/brochure-poweron-investor-clevel.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# 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).
|
||||
347
docs/case-study-power-desktop.md
Normal file
347
docs/case-study-power-desktop.md
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
# 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
|
||||
217
docs/case-study-poweron-48h-agent.md
Normal file
217
docs/case-study-poweron-48h-agent.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# 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 1–3.
|
||||
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 7’000**-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. **25–40 %** 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)
|
||||
|
||||
246
docs/concept-poweron-48h-agent-offer.md
Normal file
246
docs/concept-poweron-48h-agent-offer.md
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
# 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 9’000**, aufgeteilt in **CHF 2’000** bei Vertragsstart und **CHF 7’000** 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 9’000**, **CHF 7’000** 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. **2’000** 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 (60–90 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 2’000**-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 3–5 Arbeitstage vor Sprint):** Kickoff 60 Min., Scope-Freeze, Zugriffe, Testdaten.
|
||||
|
||||
| Zeit | Tag 1 | Tag 2 |
|
||||
| --- | --- | --- |
|
||||
| Vormittag | Phase 1–2 Abschluss: Use-Case frozen, Quellen angebunden/indexiert | Phase 4: Integration finalisieren, End-to-End-Tests |
|
||||
| Nachmittag | Phase 3–4 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 7’000
|
||||
|
||||
*Im Vertrag: **3–5 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 1–5-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 7’000** 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 9’000** (exkl. MWSt. je nach Vereinbarung) | |
|
||||
| **Anzahlung** | **CHF 2’000** | Bei Vertragsunterzeichnung / Sprint-Freigabe |
|
||||
| **Erfolgszahlung** | **CHF 7’000** | 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 (0–2) |
|
||||
| --- | --- |
|
||||
| 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
|
||||
BIN
docs/connections-ui-tests.xlsx
Normal file
BIN
docs/connections-ui-tests.xlsx
Normal file
Binary file not shown.
183
docs/feature-deck-ai-chat.md
Normal file
183
docs/feature-deck-ai-chat.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# 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.
|
||||
22
docs/feature-pitch-automation.md
Normal file
22
docs/feature-pitch-automation.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# 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.
|
||||
22
docs/feature-pitch-commcoach.md
Normal file
22
docs/feature-pitch-commcoach.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# 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.
|
||||
BIN
docs/files-ui-tests.xlsx
Normal file
BIN
docs/files-ui-tests.xlsx
Normal file
Binary file not shown.
105
docs/flyer-poweron-48h-agent.md
Normal file
105
docs/flyer-poweron-48h-agent.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# 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 1–4, 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 9’000** Fixpreis |
|
||||
| **Zu Beginn** | **CHF 2’000** |
|
||||
| **Bei Erfolg** | **CHF 7’000** (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
|
||||
138
docs/landing-billing-transparenz-poweron.md
Normal file
138
docs/landing-billing-transparenz-poweron.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# 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 (**10–500 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 (**10–500 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.*
|
||||
233
docs/launch48-deck-presentation.md
Normal file
233
docs/launch48-deck-presentation.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# 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: **3–6 Monate** statt Wochen
|
||||
|
||||
- **Vorgehen (4 Phasen, KI-gestuetzt, Human-in-the-Loop)**
|
||||
1. **Analyse & Dokumentation** (ca. 2–3 Tage): **47** TypeScript-Dateien inventarisiert und dokumentiert
|
||||
2. **Technische Spezifikation** (ca. 2–3 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. 2–3 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**.
|
||||
494
docs/launch48-offer-page.html
Normal file
494
docs/launch48-offer-page.html
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
<!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 · 48h AI Sprint · 2026</p>
|
||||
<h1>48h AI Sprint</h1>
|
||||
<p class="hero-lead">Von der Idee zum <strong>pilotfaehigen MVP</strong> in <strong>48 Stunden</strong> – gebaut mit <strong>KI-gestuetztem Engineering</strong> durch das PowerOn-Team, zum <strong>Fixpreis</strong> und mit Erfolgsanteil. Die KI ist unser Produktivitaetshebel – 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 – 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’000</strong></span>
|
||||
<span class="pill" role="listitem"><strong>CHF 7’000</strong> erfolgsgebunden</span>
|
||||
</div>
|
||||
<p class="pill-hint">CHF 7’000 entspricht 78% des Pakets – wird faellig, sobald die <strong>vorab vereinbarten Erfolgsziele</strong> im Pilot nachgewiesen sind.</p>
|
||||
<p class="payment-line"><strong>Zahlungslogik:</strong> CHF 2’000 zu Projektstart · CHF 7’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 – 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 – 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 – 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 – 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 – z. B. MVP, Integrations-Schnittstelle, Automatisierung oder Migrationsschritt – 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 – dieselbe Sprache fuer Fachbereich, IT und Einkauf.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Betrieb & Wissenstransfer</h3>
|
||||
<p>Kurze Dokumentation, Einweisung und Uebergabe, damit Ihr Team die Loesung im Paketrahmen <strong>selbststaendig weiterbetreiben oder ausbauen</strong> kann – 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 – 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> · <strong>48 Stunden</strong> Umsetzung · <strong>gemeinsam</strong> mit Ihrem Team</p>
|
||||
<p class="sub" style="margin-top:0.5rem">Ihre freigegebenen Systeme und Daten – umgesetzt durch <strong>KI-gestuetztes Engineering</strong> erfahrener Architektinnen und Architekten auf der PowerOn-Plattform, mit nachvollziehbaren Ablä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 – messbar und in 48 Stunden realistisch umsetzbar.</p>
|
||||
</article>
|
||||
<article class="step">
|
||||
<div class="step-num">Phase 2</div>
|
||||
<h3>Design & Architektur</h3>
|
||||
<p>Software-Architektur, Datenmodell und Integration auf PowerOn. <strong>Erfolgsziele</strong> werden vor dem Start schriftlich fixiert – sie steuern die Erfolgszahlung.</p>
|
||||
</article>
|
||||
<article class="step">
|
||||
<div class="step-num">Phase 3</div>
|
||||
<h3>Build & Integration</h3>
|
||||
<p>Build der Loesung, Anbindung im Vereinbarten, Tests. Ihre Fachseite prueft mit – parallel zum Build, mit Alltags-Beispielen.</p>
|
||||
</article>
|
||||
<article class="step">
|
||||
<div class="step-num">Phase 4</div>
|
||||
<h3>Deploy & Handover</h3>
|
||||
<p>Go-Live in Ihrer <strong>vereinbarten PowerOn-Umgebung</strong>, Wissenstransfer und Dokumentation fü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 – 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 – Skalierung, Rechte, Nachvollziehbarkeit inbegriffen.</p>
|
||||
</div>
|
||||
<div class="card why-card">
|
||||
<h3>Erfolg teilen</h3>
|
||||
<p><strong>CHF 7’000</strong> (78% von CHF 9’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 – 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) – 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 – strukturiert geklaert in <strong>10 Themenbereichen</strong> mit <strong>49 Detailfragen</strong> vor der Umsetzung. Ein vergleichbares Vorhaben waere klassisch oft mit <strong>3–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 & Dokumentation</strong> (ca. 2–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–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 → .NET/C#</strong>; erfahrene Architektinnen und Architekten <strong>validieren jedes Modul</strong>; Tests und Integration laufen parallel.</p>
|
||||
<p><strong>4. Testing & Uebergabe</strong> (ca. 2–3 Tage): Mock- und End-to-End-Validierung, Testdaten, automatisierte Tests; <strong>Wissenstransfer</strong> (Training on the Job, aufgezeichnete Session) – 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 – 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 – 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 – 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 – 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> – End-to-End, mit klaren Meilensteinen.</p>
|
||||
<div class="grid-3">
|
||||
<div class="card">
|
||||
<h3>Patrick Motsch</h3>
|
||||
<p><strong>CEO/CTO</strong> – 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> – Verantwortet Architektur, Qualitaet und Machbarkeit auf der PowerOn-Plattform – damit Scope, Daten und Integration im 48h-Rahmen stimmig bleiben.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Stephan Schellworth</h3>
|
||||
<p><strong>Business Integration</strong> – Verbindet Use Case, Stakeholder und Projektsteuerung – 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 – ohne Druck.</p>
|
||||
<a class="btn" href="https://www.poweron.swiss" target="_blank" rel="noopener">15-Minuten-Check – 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> · Birmensdorferstrasse 94, 8003 Zuerich · <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>
|
||||
52
docs/poweron-ki-betriebssystem-prompts.md
Normal file
52
docs/poweron-ki-betriebssystem-prompts.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# 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.
|
||||
```
|
||||
467
docs/poweron-ki-betriebssystem-slide.html
Normal file
467
docs/poweron-ki-betriebssystem-slide.html
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
<!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 & Steuerung</div>
|
||||
<div class="gov-sub">Zugriffsrechte & Rollen, Entscheidungsgrenzen, Validierung & 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 & Systeme</p>
|
||||
<ul>
|
||||
<li>➔ Chat, Spracheingabe, Oberflächen</li>
|
||||
<li>➔ API & 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 & Agenten</h3>
|
||||
<p class="sub">Entscheiden & Abläufe planen</p>
|
||||
<ul>
|
||||
<li>➔ Aufgaben delegieren</li>
|
||||
<li>➔ Skills & 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- & Tool-Layer</h3>
|
||||
<p class="sub">Ausführung konkreter Aufgaben</p>
|
||||
<ul>
|
||||
<li>➔ Prozesse, Aktionen, Integration</li>
|
||||
<li>➔ API-Aufrufe, Funktionen & 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 & 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- & Kontextschicht</h3>
|
||||
<p class="sub">Dokumente & Wissen</p>
|
||||
<ul>
|
||||
<li>➔ Vektordatenbanken</li>
|
||||
<li>➔ Retrieval & Historien</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="gov gov-right">
|
||||
<div class="gov-title">Transparenz & Kontrolle</div>
|
||||
<div class="gov-sub">Nachvollziehbarkeit, Qualitätssicherung, Kosten- und Nutzungsübersicht</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
176
docs/poweron-launch48-offer.md
Normal file
176
docs/poweron-launch48-offer.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# 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 7’000) 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 9’000** (zzgl. MWSt. falls anwendbar) | Fixpreis fuer das vereinbarte Paket |
|
||||
| **Zu Beginn** | **CHF 2’000** | Wenn Sie starten und wir die Umsetzung freigeben |
|
||||
| **Nach dem Pilot** | **CHF 7’000** | 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. 15–30 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.*
|
||||
529
docs/poweron-plattform-layer-schaubild.html
Normal file
529
docs/poweron-plattform-layer-schaubild.html
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
<!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 & Regeln</div>
|
||||
<div class="gov-sub">Wer darf was?<br>Getrennt pro Kunde / Mandant<br>Sensible Daten schützen<br>DSGVO: Auskunft & 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 & 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 & 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 & Buchhaltung, Immobilien & 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 & 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. 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 & 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 & Gesprächen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="gov gov-right">
|
||||
<div class="gov-title">Kosten & 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>
|
||||
174
docs/product-teaser-billing-poweron.md
Normal file
174
docs/product-teaser-billing-poweron.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# 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*
|
||||
245
docs/product-teaser-poweron.md
Normal file
245
docs/product-teaser-poweron.md
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# 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
|
||||

|
||||
```
|
||||
BIN
docs/prompts-ui-tests.xlsx
Normal file
BIN
docs/prompts-ui-tests.xlsx
Normal file
Binary file not shown.
168
docs/screen-recording-script-ai-chat.md
Normal file
168
docs/screen-recording-script-ai-chat.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# PORTO AI Chat — Screen-Recording-Skript (Werbeclip)
|
||||
|
||||
**Zielgruppe:** Entscheider, C-Level, Investoren
|
||||
**Ton:** sachlich, vertrauensbildend, ohne Tech-Slang
|
||||
**Gesamtlänge:** ca. 90–120 Sekunden
|
||||
**Auflösung:** mindestens 1920×1080; UI zoomed / Browser auf 125–150 % 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:00–0: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; 1–2 Sekunden Pause, dann sanft zur linken Sidebar zoomen oder leicht scrollen (optional).
|
||||
|
||||
---
|
||||
|
||||
## Szene B — Problem (0:05–0: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:20–0: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:30–0: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:45–1: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:00–1: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:15–1: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:25–1: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, 3–4 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»
|
||||
BIN
docs/settings-ui-tests.xlsx
Normal file
BIN
docs/settings-ui-tests.xlsx
Normal file
Binary file not shown.
137
docs/slide-erfolgreicher-einsatz-von-ki.md
Normal file
137
docs/slide-erfolgreicher-einsatz-von-ki.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# 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), 2–3 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 1–2 Zeilen, Belege als Bullet-Liste mit max. 3–4 Einträgen pro Folie.
|
||||
- **Quellen-Footer (optional):** klein am Folienrand: „Quelle: PowerOn Wiki – a-strategy / b-reference / e-compliance".
|
||||
266
docs/social-clip-poweron-ai-desktop.md
Normal file
266
docs/social-clip-poweron-ai-desktop.md
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
# 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. **30–60 s**, je nach Anzahl Stufen (pro Stufe typisch **3–5 s**).
|
||||
**Plattformen:** Reels, Shorts, TikTok (**9:16**), LinkedIn (**1:1** oder **4:5**).
|
||||
|
||||
---
|
||||
|
||||
## Dieses Format vs. „Problem–Loesung–Montage“
|
||||
|
||||
| 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 Hook–Pain–Solution-Clip | Ein emotionaler Bogen in 20–30 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** (1–2 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, 2–3 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:** 2–3 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 2–3 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 / Vorher–Nachher 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 (3–5 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 | 2–3 |
|
||||
| 1 Workspace | 4–5 |
|
||||
| 2 KI-Chat | 4–5 |
|
||||
| 3 Dateien | 3–4 |
|
||||
| 4 Kontextsteuerung | 3–4 |
|
||||
| 5 Quellen | 3–4 |
|
||||
| 6 Editor | 4–5 |
|
||||
| 7 Aktivitaet | 3–4 |
|
||||
| 8 CTA | 3–5 |
|
||||
|
||||
**Summe:** etwa **48–65 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 |
|
||||
| 1–7 | **Echtes Screen Recording** aus Demo-Instanz (Zoom 100–125 %, 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*
|
||||
160
docs/social-clip-poweron-treuhand.md
Normal file
160
docs/social-clip-poweron-treuhand.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# 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. 20–35 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
|
||||
97
env-dev.env
Normal file
97
env-dev.env
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# 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
|
||||
|
||||
92
env-int.env
Normal file
92
env-int.env
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# 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
|
||||
91
env-prod.env
Normal file
91
env-prod.env
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# 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
98
env_dev.env
|
|
@ -1,98 +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/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
91
env_int.env
|
|
@ -1,91 +0,0 @@
|
|||
# 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
91
env_prod.env
|
|
@ -1,91 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
import re as _re
|
||||
import re
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, Optional, AsyncGenerator, Union
|
||||
from modules.datamodels.datamodelAi import AiModel, AiModelCall, AiModelResponse
|
||||
|
||||
|
||||
_RETRY_AFTER_PATTERN = _re.compile(
|
||||
r"(?:try again in|retry after)\s+(\d+(?:\.\d+)?)\s*s", _re.IGNORECASE
|
||||
_RETRY_AFTER_PATTERN = re.compile(
|
||||
r"(?:try again in|retry after)\s+(\d+(?:\.\d+)?)\s*s", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Dynamic model registry that collects models from all AI connectors.
|
||||
|
|
@ -9,12 +9,12 @@ import logging
|
|||
import importlib
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from modules.datamodels.datamodelAi import AiModel
|
||||
from modules.datamodels.datamodelRbac import AccessRuleContext, RbacProtocol
|
||||
from .aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.security.rbacHelpers import checkResourceAccess
|
||||
from modules.security.rbac import RbacClass
|
||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -31,11 +31,37 @@ class ModelRegistry:
|
|||
self._connectors: Dict[str, BaseConnectorAi] = {}
|
||||
self._lastRefresh: Optional[float] = None
|
||||
self._refreshInterval: float = 300.0 # 5 minutes
|
||||
self._refreshLock = threading.Lock()
|
||||
self._connectorsInitialized: bool = False
|
||||
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._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):
|
||||
"""Register a connector and collect its models."""
|
||||
connectorType = connector.getConnectorType()
|
||||
|
|
@ -47,26 +73,10 @@ class ModelRegistry:
|
|||
|
||||
self._connectors[connectorType] = connector
|
||||
|
||||
# Collect models from this connector
|
||||
try:
|
||||
models = connector.getCachedModels()
|
||||
for model in models:
|
||||
# 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}")
|
||||
self._addModel(model, connectorType)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register models from {connectorType}: {e}")
|
||||
raise
|
||||
|
|
@ -116,51 +126,40 @@ class ModelRegistry:
|
|||
self._connectorsInitialized = True
|
||||
|
||||
def refreshModels(self, force: bool = False):
|
||||
"""Refresh models from all registered connectors."""
|
||||
import time
|
||||
|
||||
"""Refresh models from all registered connectors. Thread-safe via _refreshLock."""
|
||||
self.ensureConnectorsRegistered()
|
||||
|
||||
currentTime = time.time()
|
||||
|
||||
# Check if refresh is needed
|
||||
if (not force and
|
||||
self._lastRefresh is not None and
|
||||
|
||||
if (not force and
|
||||
self._lastRefresh is not None and
|
||||
currentTime - self._lastRefresh < self._refreshInterval):
|
||||
return
|
||||
|
||||
logger.info("Refreshing model registry...")
|
||||
|
||||
# Clear existing models
|
||||
self._models.clear()
|
||||
|
||||
# Re-register all connectors
|
||||
for connector in self._connectors.values():
|
||||
try:
|
||||
connector.clearCache() # Clear connector cache
|
||||
models = connector.getCachedModels()
|
||||
for model in models:
|
||||
# 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: {connector.getConnectorType()}). 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
|
||||
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")
|
||||
|
||||
if not self._refreshLock.acquire(blocking=False):
|
||||
logger.debug("refreshModels already running in another thread, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("Refreshing model registry...")
|
||||
newModels: Dict[str, AiModel] = {}
|
||||
|
||||
for connector in self._connectors.values():
|
||||
connectorType = connector.getConnectorType()
|
||||
try:
|
||||
connector.clearCache()
|
||||
models = connector.getCachedModels()
|
||||
for model in models:
|
||||
self._addModelToDict(model, connectorType, newModels)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to refresh models from {connectorType}: {e}")
|
||||
raise
|
||||
|
||||
self._models = newModels
|
||||
self._lastRefresh = time.time()
|
||||
logger.info(f"Model registry refreshed: {len(self._models)} models available")
|
||||
finally:
|
||||
self._refreshLock.release()
|
||||
|
||||
def getModel(self, displayName: str) -> Optional[AiModel]:
|
||||
"""Get a specific model by displayName (displayName must be unique)."""
|
||||
|
|
@ -186,7 +185,7 @@ class ModelRegistry:
|
|||
def getAvailableModels(
|
||||
self,
|
||||
currentUser: Optional[User] = None,
|
||||
rbacInstance: Optional[RbacClass] = None,
|
||||
rbacInstance: Optional[RbacProtocol] = None,
|
||||
mandateId: Optional[str] = None,
|
||||
featureInstanceId: Optional[str] = None
|
||||
) -> List[AiModel]:
|
||||
|
|
@ -237,7 +236,7 @@ class ModelRegistry:
|
|||
self,
|
||||
models: List[AiModel],
|
||||
currentUser: User,
|
||||
rbacInstance: RbacClass,
|
||||
rbacInstance: RbacProtocol,
|
||||
mandateId: Optional[str] = None,
|
||||
featureInstanceId: Optional[str] = None
|
||||
) -> List[AiModel]:
|
||||
|
|
@ -262,7 +261,7 @@ class ModelRegistry:
|
|||
logger.debug(f"User {currentUser.username} does not have access to model {model.displayName} (connector: {model.connectorType})")
|
||||
return filteredModels
|
||||
|
||||
def getModel(self, displayName: str, currentUser: Optional[User] = None, rbacInstance: Optional[RbacClass] = None) -> Optional[AiModel]:
|
||||
def getModel(self, displayName: str, currentUser: Optional[User] = None, rbacInstance: Optional[RbacProtocol] = None) -> Optional[AiModel]:
|
||||
"""Get a specific model by displayName, optionally checking RBAC permissions.
|
||||
|
||||
Args:
|
||||
|
|
@ -284,8 +283,15 @@ class ModelRegistry:
|
|||
connectorResourcePath = f"ai.model.{model.connectorType}"
|
||||
modelResourcePath = f"ai.model.{model.connectorType}.{model.displayName}"
|
||||
|
||||
hasConnectorAccess = checkResourceAccess(rbacInstance, currentUser, connectorResourcePath)
|
||||
hasModelAccess = checkResourceAccess(rbacInstance, currentUser, modelResourcePath)
|
||||
try:
|
||||
connPerms = rbacInstance.getUserPermissions(currentUser, AccessRuleContext.RESOURCE, connectorResourcePath)
|
||||
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):
|
||||
logger.warning(f"User {currentUser.username} does not have access to model {displayName}")
|
||||
|
|
@ -341,8 +347,8 @@ class ModelRegistry:
|
|||
modelRegistry = ModelRegistry()
|
||||
|
||||
# Eager pre-warm on first import: ensures connectors are ready in this process.
|
||||
# Critical for chatbot performance — avoids 4–8 s latency on first request.
|
||||
# Runs when this module is first imported (lifespan or first chatbot request).
|
||||
# Critical for AI/agent performance — avoids 4–8 s latency on first request.
|
||||
# Runs when this module is first imported (lifespan or first AI request).
|
||||
def _eager_prewarm() -> None:
|
||||
try:
|
||||
modelRegistry.ensureConnectorsRegistered()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Simplified model selection based on model properties and priority-based sorting.
|
||||
|
|
@ -140,11 +140,10 @@ class ModelSelector:
|
|||
promptFiltered.append(model)
|
||||
else:
|
||||
maxAllowedTokens = model.contextLength * 0.8
|
||||
# Compare prompt tokens (not bytes) with model's token limit
|
||||
if promptTokens <= maxAllowedTokens:
|
||||
if totalTokens <= maxAllowedTokens:
|
||||
promptFiltered.append(model)
|
||||
else:
|
||||
logger.debug(f"Model {model.name} filtered out: promptSize={promptTokens:.0f} tokens > maxAllowed={maxAllowedTokens:.0f} tokens (80% of {model.contextLength} tokens)")
|
||||
logger.debug(f"Model {model.name} filtered out: totalTokens={totalTokens:.0f} > maxAllowed={maxAllowedTokens:.0f} tokens (80% of {model.contextLength} tokens)")
|
||||
|
||||
logger.debug(f"After prompt size filtering: {len(promptFiltered)} models")
|
||||
|
||||
|
|
@ -272,7 +271,9 @@ class ModelSelector:
|
|||
return 1.0
|
||||
|
||||
elif requestedPriority == PriorityEnum.SPEED:
|
||||
return model.speedRating / 10.0
|
||||
# Scale to same magnitude as operation type (x1000) so speed
|
||||
# can meaningfully influence model ranking across tiers.
|
||||
return model.speedRating * 100.0
|
||||
|
||||
elif requestedPriority == PriorityEnum.QUALITY:
|
||||
return model.qualityRating / 10.0
|
||||
|
|
@ -322,4 +323,4 @@ class ModelSelector:
|
|||
|
||||
|
||||
# Global model selector instance
|
||||
modelSelector = ModelSelector()
|
||||
modelSelector = ModelSelector()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import httpx
|
||||
|
|
@ -13,6 +14,37 @@ from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingMode
|
|||
# Configure logger
|
||||
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():
|
||||
"""Load configuration data for Anthropic connector"""
|
||||
return {
|
||||
|
|
@ -49,6 +81,150 @@ class AiAnthropic(BaseConnectorAi):
|
|||
def getModels(self) -> List[AiModel]:
|
||||
# Get all available Anthropic models.
|
||||
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(
|
||||
name="claude-sonnet-4-5-20250929",
|
||||
displayName="Anthropic Claude Sonnet 4.5",
|
||||
|
|
@ -180,9 +356,12 @@ class AiAnthropic(BaseConnectorAi):
|
|||
payload: Dict[str, Any] = {
|
||||
"model": model.name,
|
||||
"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
|
||||
if maxTokens is None:
|
||||
raise ValueError("maxTokens must be provided for Anthropic API calls")
|
||||
|
|
@ -223,6 +402,7 @@ class AiAnthropic(BaseConnectorAi):
|
|||
|
||||
# Parse response
|
||||
anthropicResponse = response.json()
|
||||
stop_reason = anthropicResponse.get("stop_reason")
|
||||
|
||||
# Extract content and tool_use blocks from response
|
||||
content = ""
|
||||
|
|
@ -246,9 +426,25 @@ class AiAnthropic(BaseConnectorAi):
|
|||
|
||||
if not content and not toolCalls:
|
||||
logger.warning(f"Anthropic API returned empty content. Full response: {anthropicResponse}")
|
||||
content = "[Anthropic API returned empty response]"
|
||||
err = (
|
||||
"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", "")}
|
||||
if stop_reason:
|
||||
metadata["stop_reason"] = stop_reason
|
||||
if toolCalls:
|
||||
metadata["toolCalls"] = toolCalls
|
||||
|
||||
|
|
@ -285,10 +481,11 @@ class AiAnthropic(BaseConnectorAi):
|
|||
payload: Dict[str, Any] = {
|
||||
"model": model.name,
|
||||
"messages": converted,
|
||||
"temperature": temperature,
|
||||
"max_tokens": model.maxTokens,
|
||||
"stream": True,
|
||||
}
|
||||
if _supportsCustomTemperature(model.name):
|
||||
payload["temperature"] = temperature
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
if modelCall.tools:
|
||||
|
|
@ -363,6 +560,19 @@ class AiAnthropic(BaseConnectorAi):
|
|||
f"Anthropic stream returned empty response: model={model.name}, "
|
||||
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] = {}
|
||||
if stopReason:
|
||||
|
|
@ -445,9 +655,9 @@ class AiAnthropic(BaseConnectorAi):
|
|||
mimeType = parts[0].replace("data:", "")
|
||||
base64Data = parts[1]
|
||||
|
||||
import base64 as _b64
|
||||
_SUPPORTED = {"image/jpeg", "image/png", "image/gif", "image/webp"}
|
||||
try:
|
||||
rawHead = _b64.b64decode(base64Data[:32])
|
||||
rawHead = base64.b64decode(base64Data[:32])
|
||||
if rawHead[:3] == b"\xff\xd8\xff":
|
||||
mimeType = "image/jpeg"
|
||||
elif rawHead[:8] == b"\x89PNG\r\n\x1a\n":
|
||||
|
|
@ -458,6 +668,9 @@ class AiAnthropic(BaseConnectorAi):
|
|||
mimeType = "image/webp"
|
||||
except Exception:
|
||||
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
|
||||
anthropicMessages = [{
|
||||
|
|
@ -512,10 +725,10 @@ class AiAnthropic(BaseConnectorAi):
|
|||
|
||||
if systemPrompt:
|
||||
payload["system"] = systemPrompt
|
||||
|
||||
# Set temperature from model
|
||||
payload["temperature"] = temperature
|
||||
|
||||
|
||||
if _supportsCustomTemperature(model.name):
|
||||
payload["temperature"] = temperature
|
||||
|
||||
# Make API call with headers from httpClient (which includes anthropic-version)
|
||||
response = await self.httpClient.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
|
|
@ -649,4 +862,4 @@ def _convertToolsToAnthropicFormat(openaiTools: List[Dict[str, Any]]) -> List[Di
|
|||
"description": fn.get("description", ""),
|
||||
"input_schema": fn.get("parameters", {"type": "object", "properties": {}})
|
||||
})
|
||||
return anthropicTools
|
||||
return anthropicTools
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
import logging
|
||||
from typing import List
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
import logging
|
||||
import json as _json
|
||||
import json
|
||||
import httpx
|
||||
from typing import List, Dict, Any, AsyncGenerator, Union
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -274,7 +274,7 @@ class AiMistral(BaseConnectorAi):
|
|||
bodyStr = body.decode()
|
||||
if response.status_code == 429:
|
||||
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):
|
||||
errorMsg = f"Rate limit exceeded for {model.name}"
|
||||
raise RateLimitExceededException(f"Rate limit exceeded for {model.name}: {errorMsg}")
|
||||
|
|
@ -287,8 +287,8 @@ class AiMistral(BaseConnectorAi):
|
|||
if data.strip() == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = _json.loads(data)
|
||||
except _json.JSONDecodeError:
|
||||
chunk = json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
import logging
|
||||
import json as _json
|
||||
import json
|
||||
import httpx
|
||||
from typing import List, Dict, Any, AsyncGenerator, Union
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -11,6 +11,30 @@ from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingMode
|
|||
|
||||
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():
|
||||
"""Load configuration data for OpenAI connector"""
|
||||
return {
|
||||
|
|
@ -123,6 +147,135 @@ class AiOpenai(BaseConnectorAi):
|
|||
version="gpt-4o",
|
||||
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(
|
||||
name="text-embedding-3-small",
|
||||
displayName="OpenAI Embedding Small",
|
||||
|
|
@ -166,25 +319,24 @@ class AiOpenai(BaseConnectorAi):
|
|||
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.00013
|
||||
),
|
||||
AiModel(
|
||||
name="dall-e-3",
|
||||
displayName="OpenAI DALL-E 3",
|
||||
name="gpt-image-1",
|
||||
displayName="OpenAI GPT Image",
|
||||
connectorType="openai",
|
||||
apiUrl="https://api.openai.com/v1/images/generations",
|
||||
temperature=0.0, # Image generation doesn't use temperature
|
||||
maxTokens=0, # Image generation doesn't use tokens
|
||||
temperature=0.0,
|
||||
maxTokens=0,
|
||||
contextLength=0,
|
||||
costPer1kTokensInput=0.04,
|
||||
costPer1kTokensOutput=0.0,
|
||||
speedRating=5, # Slow for image generation
|
||||
qualityRating=9, # High quality art generation
|
||||
# capabilities removed (not used in business logic)
|
||||
speedRating=5,
|
||||
qualityRating=9,
|
||||
functionCall=self.generateImage,
|
||||
priority=PriorityEnum.QUALITY,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
operationTypes=createOperationTypeRatings(
|
||||
(OperationTypeEnum.IMAGE_GENERATE, 10)
|
||||
),
|
||||
version="dall-e-3",
|
||||
version="gpt-image-1",
|
||||
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
|
||||
)
|
||||
]
|
||||
|
|
@ -215,10 +367,18 @@ class AiOpenai(BaseConnectorAi):
|
|||
payload = {
|
||||
"model": model.name,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": maxTokens
|
||||
# Universal output-length cap. `max_tokens` is deprecated and
|
||||
# rejected outright by gpt-5.x / o-series; `max_completion_tokens`
|
||||
# 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:
|
||||
payload["tools"] = modelCall.tools
|
||||
payload["tool_choice"] = modelCall.toolChoice or "auto"
|
||||
|
|
@ -295,10 +455,15 @@ class AiOpenai(BaseConnectorAi):
|
|||
payload: Dict[str, Any] = {
|
||||
"model": model.name,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": model.maxTokens,
|
||||
# See callAiBasic for the rationale: `max_completion_tokens`
|
||||
# is the universal output-length parameter; `max_tokens` is
|
||||
# deprecated and rejected by gpt-5.x / o-series.
|
||||
"max_completion_tokens": model.maxTokens,
|
||||
"stream": True,
|
||||
}
|
||||
if _supportsCustomTemperature(model.name):
|
||||
payload["temperature"] = temperature
|
||||
|
||||
if modelCall.tools:
|
||||
payload["tools"] = modelCall.tools
|
||||
payload["tool_choice"] = modelCall.toolChoice or "auto"
|
||||
|
|
@ -312,7 +477,7 @@ class AiOpenai(BaseConnectorAi):
|
|||
bodyStr = body.decode()
|
||||
if response.status_code == 429:
|
||||
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):
|
||||
errorMsg = f"Rate limit exceeded for {model.name}"
|
||||
raise RateLimitExceededException(f"Rate limit exceeded for {model.name}: {errorMsg}")
|
||||
|
|
@ -325,8 +490,8 @@ class AiOpenai(BaseConnectorAi):
|
|||
if data.strip() == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = _json.loads(data)
|
||||
except _json.JSONDecodeError:
|
||||
chunk = json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
||||
|
|
@ -449,15 +614,15 @@ class AiOpenai(BaseConnectorAi):
|
|||
# Use the messages directly - they should already contain the image data
|
||||
# in the format: {"type": "image_url", "image_url": {"url": "data:...base64,..."}}
|
||||
|
||||
# Use parameters from model
|
||||
temperature = model.temperature
|
||||
# Don't set maxTokens - let the model use its full context length
|
||||
|
||||
|
||||
payload = {
|
||||
"model": model.name,
|
||||
"messages": messages,
|
||||
"temperature": temperature
|
||||
}
|
||||
if _supportsCustomTemperature(model.name):
|
||||
payload["temperature"] = temperature
|
||||
|
||||
response = await self.httpClient.post(
|
||||
model.apiUrl,
|
||||
|
|
@ -487,105 +652,82 @@ class AiOpenai(BaseConnectorAi):
|
|||
)
|
||||
|
||||
async def generateImage(self, modelCall: AiModelCall) -> AiModelResponse:
|
||||
"""
|
||||
Generate an image using DALL-E 3 using standardized pattern.
|
||||
|
||||
Args:
|
||||
modelCall: AiModelCall with messages and generation options
|
||||
|
||||
Returns:
|
||||
AiModelResponse with generated image data
|
||||
"""
|
||||
"""Generate an image using GPT Image model (gpt-image-1)."""
|
||||
try:
|
||||
# Extract parameters from modelCall
|
||||
messages = modelCall.messages
|
||||
model = modelCall.model
|
||||
options = modelCall.options
|
||||
|
||||
# Get prompt from messages
|
||||
promptContent = messages[0]["content"] if messages else ""
|
||||
|
||||
# Parse prompt using AiCallPromptImage model
|
||||
import json
|
||||
|
||||
|
||||
messages = modelCall.messages
|
||||
options = modelCall.options
|
||||
promptContent = messages[0]["content"] if messages else ""
|
||||
|
||||
try:
|
||||
# Try to parse as JSON
|
||||
promptData = json.loads(promptContent)
|
||||
promptModel = AiCallPromptImage(**promptData)
|
||||
except:
|
||||
# If not JSON, use plain text prompt
|
||||
except Exception:
|
||||
promptModel = AiCallPromptImage(
|
||||
prompt=promptContent,
|
||||
size=options.size if options and hasattr(options, 'size') else "1024x1024",
|
||||
quality=options.quality if options and hasattr(options, 'quality') else "standard",
|
||||
style=options.style if options and hasattr(options, 'style') else "vivid"
|
||||
size=options.size if options and hasattr(options, "size") else "1024x1024",
|
||||
quality=options.quality if options and hasattr(options, "quality") else "auto",
|
||||
)
|
||||
|
||||
# Extract parameters from Pydantic model
|
||||
|
||||
prompt = promptModel.prompt
|
||||
size = promptModel.size or "1024x1024"
|
||||
quality = promptModel.quality or "standard"
|
||||
style = promptModel.style or "vivid"
|
||||
|
||||
rawQuality = promptModel.quality or "auto"
|
||||
quality = {"standard": "auto", "hd": "high"}.get(rawQuality, rawQuality)
|
||||
|
||||
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 = {
|
||||
"model": "dall-e-3",
|
||||
"model": "gpt-image-1",
|
||||
"prompt": prompt,
|
||||
"size": size,
|
||||
"quality": quality,
|
||||
"style": style,
|
||||
"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(
|
||||
dalle_url,
|
||||
json=payload
|
||||
"https://api.openai.com/v1/images/generations",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"DALL-E API error: {response.status_code} - {response.text}")
|
||||
logger.error(f"Image generation API error: {response.status_code} - {response.text}")
|
||||
return AiModelResponse(
|
||||
content="",
|
||||
success=False,
|
||||
error=f"DALL-E API error: {response.status_code} - {response.text}"
|
||||
error=f"Image generation API error: {response.status_code} - {response.text}",
|
||||
)
|
||||
|
||||
|
||||
responseJson = response.json()
|
||||
|
||||
|
||||
if "data" in responseJson and len(responseJson["data"]) > 0:
|
||||
image_data = responseJson["data"][0]["b64_json"]
|
||||
|
||||
logger.info(f"Successfully generated image: {len(image_data)} characters")
|
||||
imageData = responseJson["data"][0].get("b64_json", "")
|
||||
if not imageData:
|
||||
imageData = responseJson["data"][0].get("url", "")
|
||||
|
||||
logger.info(f"Successfully generated image: {len(imageData)} characters")
|
||||
return AiModelResponse(
|
||||
content=image_data,
|
||||
content=imageData,
|
||||
success=True,
|
||||
modelId="dall-e-3",
|
||||
modelId="gpt-image-1",
|
||||
metadata={
|
||||
"size": size,
|
||||
"quality": quality,
|
||||
"style": style,
|
||||
"response_id": responseJson.get("id", "")
|
||||
}
|
||||
"response_id": responseJson.get("id", ""),
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.error("No image data in DALL-E response")
|
||||
logger.error("No image data in generation response")
|
||||
return AiModelResponse(
|
||||
content="",
|
||||
success=False,
|
||||
error="No image data in DALL-E response"
|
||||
error="No image data in generation response",
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during image generation: {str(e)}", exc_info=True)
|
||||
return AiModelResponse(
|
||||
content="",
|
||||
success=False,
|
||||
error=f"Error during image generation: {str(e)}"
|
||||
)
|
||||
error=f"Error during image generation: {str(e)}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
import logging
|
||||
import httpx
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
AI Connector for PowerOn Private-LLM Service.
|
||||
|
|
@ -6,14 +6,17 @@ AI Connector for PowerOn Private-LLM Service.
|
|||
Connects to the private-llm service running on-premise with Ollama backend.
|
||||
Provides OCR and Vision capabilities via local AI models.
|
||||
|
||||
Models:
|
||||
- poweron-text-general: Text (qwen2.5); NEUTRALIZATION_TEXT + data/plan ops
|
||||
- poweron-vision-general: Vision (qwen2.5vl); IMAGE_ANALYSE + NEUTRALIZATION_IMAGE
|
||||
Models (current — L4 24 GB):
|
||||
- poweron-text-general: Text (qwen2.5:7b); NEUTRALIZATION_TEXT + data/plan ops
|
||||
- poweron-vision-general: Vision (qwen2.5vl:7b); IMAGE_ANALYSE + NEUTRALIZATION_IMAGE
|
||||
- poweron-vision-deep: Vision (granite3.2); IMAGE_ANALYSE + NEUTRALIZATION_IMAGE
|
||||
|
||||
Pricing (CHF per call):
|
||||
- Text models: CHF 0.010
|
||||
- Vision models: CHF 0.100
|
||||
Models (next-gen — RTX PRO 6000 96 GB, auto-activated when pulled in Ollama):
|
||||
- poweron-text-reasoning: Reasoning (deepseek-r1:70b); complex logic, math, planning
|
||||
- poweron-vision-general: Vision (llama4:scout); multimodal, long-context documents
|
||||
- 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
|
||||
|
|
@ -36,9 +39,20 @@ from modules.datamodels.datamodelAi import (
|
|||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pricing constants (CHF)
|
||||
PRICE_TEXT_PER_CALL = 0.01 # CHF 0.010 per text model call
|
||||
PRICE_VISION_PER_CALL = 0.10 # CHF 0.100 per vision model call
|
||||
# Pricing constants (CHF per 1k tokens; billed byte-based via bytes/4 ~ 1 token)
|
||||
PRICE_INPUT_PER_1K = 0.0075
|
||||
PRICE_OUTPUT_PER_1K = 0.0375
|
||||
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)
|
||||
|
|
@ -233,8 +247,8 @@ class AiPrivateLlm(BaseConnectorAi):
|
|||
temperature=0.1,
|
||||
maxTokens=4096,
|
||||
contextLength=8192, # Reduced for RAM constraints
|
||||
costPer1kTokensInput=0.0, # Flat rate pricing
|
||||
costPer1kTokensOutput=0.0, # Flat rate pricing
|
||||
costPer1kTokensInput=PRICE_INPUT_PER_1K,
|
||||
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K,
|
||||
speedRating=8, # Fast and efficient
|
||||
qualityRating=9, # High quality text model
|
||||
functionCall=self.callAiText,
|
||||
|
|
@ -246,9 +260,11 @@ class AiPrivateLlm(BaseConnectorAi):
|
|||
(OperationTypeEnum.DATA_GENERATE, 8),
|
||||
(OperationTypeEnum.DATA_EXTRACT, 8),
|
||||
(OperationTypeEnum.NEUTRALIZATION_TEXT, 9),
|
||||
# Agent loop (workspace etc.) selects models by OperationTypeEnum.AGENT for streaming.
|
||||
(OperationTypeEnum.AGENT, 8),
|
||||
),
|
||||
version="qwen2.5:7b",
|
||||
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: PRICE_TEXT_PER_CALL
|
||||
calculatepriceCHF=_calcPrivatePriceCHF
|
||||
),
|
||||
"ollamaModel": "qwen2.5:7b"
|
||||
},
|
||||
|
|
@ -262,8 +278,8 @@ class AiPrivateLlm(BaseConnectorAi):
|
|||
temperature=0.2,
|
||||
maxTokens=2048,
|
||||
contextLength=4096, # Reduced for RAM constraints (vision needs more)
|
||||
costPer1kTokensInput=0.0, # Flat rate pricing
|
||||
costPer1kTokensOutput=0.0, # Flat rate pricing
|
||||
costPer1kTokensInput=PRICE_INPUT_PER_1K,
|
||||
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K,
|
||||
speedRating=7,
|
||||
qualityRating=9,
|
||||
functionCall=self.callAiVision,
|
||||
|
|
@ -274,7 +290,7 @@ class AiPrivateLlm(BaseConnectorAi):
|
|||
(OperationTypeEnum.NEUTRALIZATION_IMAGE, 9),
|
||||
),
|
||||
version="qwen2.5vl:7b",
|
||||
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: PRICE_VISION_PER_CALL
|
||||
calculatepriceCHF=_calcPrivatePriceCHF
|
||||
),
|
||||
"ollamaModel": "qwen2.5vl:7b"
|
||||
},
|
||||
|
|
@ -288,8 +304,8 @@ class AiPrivateLlm(BaseConnectorAi):
|
|||
temperature=0.1,
|
||||
maxTokens=2048,
|
||||
contextLength=4096, # Reduced for RAM constraints
|
||||
costPer1kTokensInput=0.0, # Flat rate pricing
|
||||
costPer1kTokensOutput=0.0, # Flat rate pricing
|
||||
costPer1kTokensInput=PRICE_INPUT_PER_1K,
|
||||
costPer1kTokensOutput=PRICE_OUTPUT_PER_1K,
|
||||
speedRating=9, # Fast due to small 2B model
|
||||
qualityRating=8, # Good for document understanding
|
||||
functionCall=self.callAiVision,
|
||||
|
|
@ -300,10 +316,92 @@ class AiPrivateLlm(BaseConnectorAi):
|
|||
(OperationTypeEnum.NEUTRALIZATION_IMAGE, 9),
|
||||
),
|
||||
version="granite3.2-vision",
|
||||
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: PRICE_VISION_PER_CALL
|
||||
calculatepriceCHF=_calcPrivatePriceCHF
|
||||
),
|
||||
"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
|
||||
|
|
@ -318,7 +416,7 @@ class AiPrivateLlm(BaseConnectorAi):
|
|||
unavailableModels.append(modelDef["model"].name)
|
||||
|
||||
if unavailableModels:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
f"Private-LLM: {len(unavailableModels)} models not available in Ollama: {', '.join(unavailableModels)}. "
|
||||
f"Install with: ollama pull <model-name>"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""Tavily web search class.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Authentication and authorization modules for routes and services.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Authentication module for backend API.
|
||||
|
|
@ -437,7 +437,7 @@ def requireSysAdmin(currentUser: User = Depends(getCurrentUser)) -> User:
|
|||
|
||||
# Audit for all SysAdmin actions
|
||||
try:
|
||||
from modules.shared.auditLogger import audit_logger
|
||||
from modules.dbHelpers.auditLogger import audit_logger
|
||||
audit_logger.logSecurityEvent(
|
||||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
|
|
@ -483,7 +483,7 @@ def requirePlatformAdmin(currentUser: User = Depends(getCurrentUser)) -> User:
|
|||
|
||||
# Audit for all Platform-Admin actions
|
||||
try:
|
||||
from modules.shared.auditLogger import audit_logger
|
||||
from modules.dbHelpers.auditLogger import audit_logger
|
||||
audit_logger.logSecurityEvent(
|
||||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
CSRF Protection Middleware for PowerOn Gateway
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
JWT Service
|
||||
Centralizes local JWT creation and cookie helpers.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Tuple
|
||||
from fastapi import Response
|
||||
from jose import jwt
|
||||
|
|
@ -19,10 +19,28 @@ ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM")
|
|||
ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY"))
|
||||
REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7"))
|
||||
|
||||
# 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
|
||||
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
|
||||
def _cookiePolicy() -> Tuple[bool, str, str]:
|
||||
"""
|
||||
Return (useSecure, samesiteStarlette, samesiteSetCookieHeader).
|
||||
|
||||
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"]:
|
||||
|
|
@ -54,13 +72,14 @@ def createRefreshToken(data: dict) -> Tuple[str, "datetime"]:
|
|||
|
||||
def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[timedelta] = None) -> None:
|
||||
"""Set access token as httpOnly cookie."""
|
||||
useSecure, samesite, _ = _cookiePolicy()
|
||||
maxAge = int(expiresDelta.total_seconds()) if expiresDelta else ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
response.set_cookie(
|
||||
key="auth_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
|
||||
samesite="strict",
|
||||
secure=useSecure,
|
||||
samesite=samesite,
|
||||
path="/",
|
||||
max_age=maxAge
|
||||
)
|
||||
|
|
@ -68,12 +87,13 @@ def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[
|
|||
|
||||
def setRefreshTokenCookie(response: Response, token: str) -> None:
|
||||
"""Set refresh token as httpOnly cookie."""
|
||||
useSecure, samesite, _ = _cookiePolicy()
|
||||
response.set_cookie(
|
||||
key="refresh_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
|
||||
samesite="strict",
|
||||
secure=useSecure,
|
||||
samesite=samesite,
|
||||
path="/",
|
||||
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
|
||||
)
|
||||
|
|
@ -84,17 +104,23 @@ def clearAccessTokenCookie(response: Response) -> None:
|
|||
Clear access token cookie by setting it to expire immediately.
|
||||
Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility.
|
||||
"""
|
||||
# Build secure flag based on environment
|
||||
secure_flag = "; Secure" if USE_SECURE_COOKIES else ""
|
||||
|
||||
useSecure, samesite, samesiteHeader = _cookiePolicy()
|
||||
secure_flag = "; Secure" if useSecure else ""
|
||||
|
||||
# Primary method: Raw Set-Cookie header for guaranteed deletion
|
||||
response.headers.append(
|
||||
"Set-Cookie",
|
||||
f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite=Strict"
|
||||
f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={samesiteHeader}"
|
||||
)
|
||||
|
||||
# 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:
|
||||
|
|
@ -102,16 +128,22 @@ def clearRefreshTokenCookie(response: Response) -> None:
|
|||
Clear refresh token cookie by setting it to expire immediately.
|
||||
Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility.
|
||||
"""
|
||||
# Build secure flag based on environment
|
||||
secure_flag = "; Secure" if USE_SECURE_COOKIES else ""
|
||||
|
||||
useSecure, samesite, samesiteHeader = _cookiePolicy()
|
||||
secure_flag = "; Secure" if useSecure else ""
|
||||
|
||||
# Primary method: Raw Set-Cookie header for guaranteed deletion
|
||||
response.headers.append(
|
||||
"Set-Cookie",
|
||||
f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite=Strict"
|
||||
f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={samesiteHeader}"
|
||||
)
|
||||
|
||||
# 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="/")
|
||||
|
||||
|
||||
|
|
|
|||
132
modules/auth/mfaService.py
Normal file
132
modules/auth/mfaService.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# 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
|
||||
101
modules/auth/oauthConnectTicket.py
Normal file
101
modules/auth/oauthConnectTicket.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# 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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""OAuth scope sets for split Auth- vs Data-apps (Google / Microsoft)."""
|
||||
|
||||
|
|
@ -9,13 +9,15 @@ googleAuthScopes = [
|
|||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
]
|
||||
|
||||
# Google — Data app (Gmail + Drive + identity for token responses)
|
||||
# Google — Data app (Gmail + Drive + Calendar + Contacts + identity for token responses)
|
||||
googleDataScopes = [
|
||||
"openid",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/gmail.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, …)
|
||||
|
|
@ -34,9 +36,18 @@ msftDataScopes = [
|
|||
"OnlineMeetings.Read",
|
||||
"Chat.ReadWrite",
|
||||
"ChatMessage.Send",
|
||||
"Calendars.Read",
|
||||
"Contacts.Read",
|
||||
]
|
||||
|
||||
|
||||
def msftDataScopesForRefresh() -> str:
|
||||
"""Space-separated scope string identical to authorization request (Token v2 refresh)."""
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Token Manager Service
|
||||
|
|
@ -29,6 +29,7 @@ class TokenManager:
|
|||
# Google Data-app OAuth
|
||||
self.google_client_id = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_ID")
|
||||
self.google_client_secret = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_SECRET")
|
||||
|
||||
|
||||
def refreshMicrosoftToken(self, refreshToken: str, userId: str, oldToken: Token) -> Optional[Token]:
|
||||
"""Refresh Microsoft OAuth token using refresh token"""
|
||||
|
|
@ -161,7 +162,7 @@ class TokenManager:
|
|||
except Exception as e:
|
||||
logger.error(f"Error refreshing Google token: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def refreshToken(self, oldToken: Token) -> Optional[Token]:
|
||||
"""Refresh an expired token using the appropriate OAuth service"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Token Refresh Middleware for PowerOn Gateway
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Token Refresh Service for PowerOn Gateway
|
||||
|
|
@ -12,7 +12,7 @@ import logging
|
|||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelUam import UserConnection, AuthAuthority
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
from modules.shared.auditLogger import audit_logger
|
||||
from modules.dbHelpers.auditLogger import audit_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ class TokenRefreshService:
|
|||
except Exception as e:
|
||||
logger.error(f"Error refreshing Microsoft token for connection {connection.id}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def refresh_expired_tokens(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Refresh expired OAuth tokens for a user
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Azure Communication Services Email Connector
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Twilio SMS Connector
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
ÖREB WFS Connector
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Preprocessor connector for executing SQL queries via HTTP API.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""Abstract base classes for the Provider-Connector architecture (1:n).
|
||||
|
||||
|
|
@ -24,8 +24,21 @@ class ServiceAdapter(ABC):
|
|||
"""Standardized operations for a single service of a provider."""
|
||||
|
||||
@abstractmethod
|
||||
async def browse(self, path: str, filter: Optional[str] = None) -> list:
|
||||
"""List items (files/folders) at the given path."""
|
||||
async def browse(
|
||||
self,
|
||||
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
|
||||
|
|
@ -39,8 +52,16 @@ class ServiceAdapter(ABC):
|
|||
...
|
||||
|
||||
@abstractmethod
|
||||
async def search(self, query: str, path: Optional[str] = None) -> list:
|
||||
"""Search for items matching the query."""
|
||||
async def search(
|
||||
self,
|
||||
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``.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""ClickUp ProviderConnector — virtual paths for teams → lists → tasks (table rows).
|
||||
|
||||
|
|
@ -13,10 +13,13 @@ Path convention (leading slash, no trailing slash except root):
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import aiohttp
|
||||
|
||||
from modules.connectors.connectorProviderBase import (
|
||||
ProviderConnector,
|
||||
|
|
@ -24,11 +27,11 @@ from modules.connectors.connectorProviderBase import (
|
|||
DownloadResult,
|
||||
)
|
||||
from modules.datamodels.datamodelDataSource import ExternalEntry
|
||||
from modules.serviceCenter.services.serviceClickup.mainServiceClickup import ClickupService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# type metadata for ExternalEntry.metadata["cuType"]
|
||||
_CLICKUP_API_BASE = "https://api.clickup.com/api/v2"
|
||||
|
||||
_CU_TEAM = "team"
|
||||
_CU_SPACE = "space"
|
||||
_CU_FOLDER = "folder"
|
||||
|
|
@ -45,16 +48,125 @@ def _norm(path: str) -> str:
|
|||
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):
|
||||
"""Maps ClickUp hierarchy + list tasks to browse/download/upload/search."""
|
||||
|
||||
def __init__(self, access_token: str):
|
||||
self._token = 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)
|
||||
self._svc = ClickupApiClient(access_token)
|
||||
|
||||
async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
|
||||
async def browse(
|
||||
self,
|
||||
path: str,
|
||||
filter: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> List[ExternalEntry]:
|
||||
p = _norm(path)
|
||||
out: List[ExternalEntry] = []
|
||||
|
||||
|
|
@ -173,7 +285,11 @@ class ClickupListsAdapter(ServiceAdapter):
|
|||
)
|
||||
if len(tasks) < 100:
|
||||
break
|
||||
if limit is not None and len(out) >= int(limit):
|
||||
break
|
||||
page += 1
|
||||
if limit is not None:
|
||||
out = out[: max(1, int(limit))]
|
||||
return out
|
||||
|
||||
m = re.match(r"^/team/([^/]+)/list/([^/]+)/task/([^/]+)$", p)
|
||||
|
|
@ -201,6 +317,9 @@ class ClickupListsAdapter(ServiceAdapter):
|
|||
data = await self._svc.getTask(task_id)
|
||||
if isinstance(data, dict) and data.get("error"):
|
||||
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")
|
||||
return DownloadResult(data=payload, fileName=f"task-{task_id}.json", mimeType="application/json")
|
||||
|
||||
|
|
@ -213,7 +332,12 @@ class ClickupListsAdapter(ServiceAdapter):
|
|||
task_id = m.group(3)
|
||||
return await self._svc.uploadTaskAttachment(task_id, data, fileName)
|
||||
|
||||
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
|
||||
async def search(
|
||||
self,
|
||||
query: str,
|
||||
path: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> List[ExternalEntry]:
|
||||
base = _norm(path or "/")
|
||||
team_id: Optional[str] = None
|
||||
mt = re.match(r"^/team/([^/]+)", base)
|
||||
|
|
@ -252,7 +376,11 @@ class ClickupListsAdapter(ServiceAdapter):
|
|||
)
|
||||
if len(tasks) < 25:
|
||||
break
|
||||
if limit is not None and len(out) >= int(limit):
|
||||
break
|
||||
page += 1
|
||||
if limit is not None:
|
||||
out = out[: max(1, int(limit))]
|
||||
return out
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""FTP/SFTP ProviderConnector stub.
|
||||
|
||||
|
|
@ -21,7 +21,12 @@ class FtpFilesAdapter(ServiceAdapter):
|
|||
def __init__(self, accessToken: str):
|
||||
self._accessToken = accessToken
|
||||
|
||||
async def browse(self, path: str, filter: Optional[str] = None) -> List[ExternalEntry]:
|
||||
async def browse(
|
||||
self,
|
||||
path: str,
|
||||
filter: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> List[ExternalEntry]:
|
||||
logger.info(f"FTP browse stub: {path}")
|
||||
return []
|
||||
|
||||
|
|
@ -32,7 +37,12 @@ class FtpFilesAdapter(ServiceAdapter):
|
|||
async def upload(self, path: str, data: bytes, fileName: str) -> dict:
|
||||
return {"error": "FTP upload not yet implemented"}
|
||||
|
||||
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
|
||||
async def search(
|
||||
self,
|
||||
query: str,
|
||||
path: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> List[ExternalEntry]:
|
||||
return []
|
||||
|
||||
|
||||
940
modules/connectors/connectorProviderGoogle.py
Normal file
940
modules/connectors/connectorProviderGoogle.py
Normal file
|
|
@ -0,0 +1,940 @@
|
|||
# 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)
|
||||
1099
modules/connectors/connectorProviderInfomaniak.py
Normal file
1099
modules/connectors/connectorProviderInfomaniak.py
Normal file
File diff suppressed because it is too large
Load diff
1572
modules/connectors/connectorProviderMsft.py
Normal file
1572
modules/connectors/connectorProviderMsft.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""ConnectorResolver -- resolves a connectionId to the correct ProviderConnector and ServiceAdapter.
|
||||
|
||||
|
|
@ -15,6 +15,15 @@ from modules.connectors.connectorProviderBase import ProviderConnector, ServiceA
|
|||
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:
|
||||
"""Resolves connectionId → ProviderConnector (with fresh token) → ServiceAdapter."""
|
||||
|
||||
|
|
@ -35,29 +44,35 @@ class ConnectorResolver:
|
|||
if ConnectorResolver._providerRegistry:
|
||||
return
|
||||
try:
|
||||
from modules.connectors.providerMsft.connectorMsft import MsftConnector
|
||||
from modules.connectors.connectorProviderMsft import MsftConnector
|
||||
ConnectorResolver._providerRegistry["msft"] = MsftConnector
|
||||
except ImportError:
|
||||
logger.warning("MsftConnector not available")
|
||||
|
||||
try:
|
||||
from modules.connectors.providerGoogle.connectorGoogle import GoogleConnector
|
||||
from modules.connectors.connectorProviderGoogle import GoogleConnector
|
||||
ConnectorResolver._providerRegistry["google"] = GoogleConnector
|
||||
except ImportError:
|
||||
logger.debug("GoogleConnector not available (stub)")
|
||||
|
||||
try:
|
||||
from modules.connectors.providerFtp.connectorFtp import FtpConnector
|
||||
from modules.connectors.connectorProviderFtp import FtpConnector
|
||||
ConnectorResolver._providerRegistry["local:ftp"] = FtpConnector
|
||||
except ImportError:
|
||||
logger.debug("FtpConnector not available (stub)")
|
||||
|
||||
try:
|
||||
from modules.connectors.providerClickup.connectorClickup import ClickupConnector
|
||||
from modules.connectors.connectorProviderClickup import ClickupConnector
|
||||
ConnectorResolver._providerRegistry["clickup"] = ClickupConnector
|
||||
except ImportError:
|
||||
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:
|
||||
"""Resolve connectionId to a ProviderConnector with a fresh access token."""
|
||||
connection = await self._loadConnection(connectionId)
|
||||
|
|
@ -73,9 +88,16 @@ class ConnectorResolver:
|
|||
if not providerClass:
|
||||
raise ValueError(f"No ProviderConnector registered for authority: {authorityStr}")
|
||||
|
||||
token = self._security.getFreshToken(connectionId)
|
||||
resolved_id = _connection_uuid(connection)
|
||||
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:
|
||||
raise ValueError(f"No valid token for connection {connectionId}")
|
||||
raise ValueError(
|
||||
f"No valid token for connection {resolved_id}"
|
||||
+ (f" (ref: {connectionId})" if connectionId != resolved_id else "")
|
||||
)
|
||||
|
||||
return providerClass(connection, token.tokenAccess)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Swiss Topo MapServer Connector (Simplified)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""ClickUp connector for CRUD operations (compatible with TicketInterface).
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ from typing import Optional
|
|||
import logging
|
||||
import aiohttp
|
||||
from modules.datamodels.datamodelTickets import TicketBase, TicketFieldAttribute
|
||||
from modules.serviceCenter.services.serviceClickup.mainServiceClickup import clickup_authorization_header
|
||||
from modules.connectors.connectorProviderClickup import clickupAuthorizationHeader
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -31,7 +31,7 @@ class ConnectorTicketClickup(TicketBase):
|
|||
|
||||
def _headers(self) -> dict:
|
||||
return {
|
||||
"Authorization": clickup_authorization_header(self.apiToken),
|
||||
"Authorization": clickupAuthorizationHeader(self.apiToken),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""Jira connector for CRUD operations (neutralized to generic ticket interface).
|
||||
|
||||
|
|
|
|||
419
modules/connectors/connectorTicketsRedmine.py
Normal file
419
modules/connectors/connectorTicketsRedmine.py
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
# 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)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Google Cloud Speech-to-Text and Translation Connector
|
||||
|
|
@ -15,10 +15,34 @@ from google.cloud import speech
|
|||
from google.cloud import translate_v2 as translate
|
||||
from google.cloud import texttospeech
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.shared.voiceCatalog import getDefaultVoice as _catalogDefaultVoice
|
||||
from modules.shared.voiceCatalog import getDefaultVoice
|
||||
|
||||
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
|
||||
# SynthesisInput.prompt + VoiceSelectionParams.model_name (google-cloud-texttospeech >= 2.24.0).
|
||||
_GEMINI_TTS_DEFAULT_MODEL = "gemini-2.5-flash-tts"
|
||||
|
|
@ -73,7 +97,10 @@ class ConnectorGoogleSpeech:
|
|||
sampleRate: int = None, channels: int = None,
|
||||
skipFallbacks: bool = False,
|
||||
phraseHints: Optional[list] = None,
|
||||
alternativeLanguages: Optional[list] = None) -> Dict:
|
||||
alternativeLanguages: Optional[list] = None,
|
||||
model: str = "latest_long",
|
||||
lightweight: bool = False,
|
||||
audioFormat: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
Convert speech to text using Google Cloud Speech-to-Text API.
|
||||
|
||||
|
|
@ -82,6 +109,9 @@ class ConnectorGoogleSpeech:
|
|||
language: Language code (e.g., 'de-DE', 'en-US')
|
||||
sample_rate: Audio sample rate (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:
|
||||
Dict containing transcribed text, confidence, and metadata
|
||||
|
|
@ -92,8 +122,24 @@ class ConnectorGoogleSpeech:
|
|||
logger.warning(f"Invalid sampleRate={sampleRate}, treating as unknown for auto-detection")
|
||||
sampleRate = None
|
||||
|
||||
# Auto-detect audio format if not provided
|
||||
if sampleRate is None or channels is None:
|
||||
explicitFormat = (audioFormat or "").strip().lower() or None
|
||||
if explicitFormat:
|
||||
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)
|
||||
if not validation["valid"]:
|
||||
return {
|
||||
|
|
@ -156,12 +202,7 @@ class ConnectorGoogleSpeech:
|
|||
"encoding": encoding,
|
||||
"audio_channel_count": channels,
|
||||
"language_code": language,
|
||||
"enable_automatic_punctuation": True,
|
||||
"model": "latest_long",
|
||||
"enable_word_time_offsets": True,
|
||||
"enable_word_confidence": True,
|
||||
"max_alternatives": 3,
|
||||
"use_enhanced": True,
|
||||
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight),
|
||||
}
|
||||
|
||||
if phraseHints:
|
||||
|
|
@ -205,8 +246,7 @@ class ConnectorGoogleSpeech:
|
|||
sample_rate_hertz=16000,
|
||||
audio_channel_count=1,
|
||||
language_code=language,
|
||||
enable_automatic_punctuation=True,
|
||||
model="latest_long"
|
||||
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight),
|
||||
)
|
||||
try:
|
||||
response = await asyncio.to_thread(
|
||||
|
|
@ -343,7 +383,7 @@ class ConnectorGoogleSpeech:
|
|||
"error": "No recognition results (silence or unclear audio)"
|
||||
}
|
||||
|
||||
models = ["latest_long", "phone_call", "latest_short"]
|
||||
models = list(dict.fromkeys([model, "latest_long", "phone_call", "latest_short"]))
|
||||
|
||||
for fallback_config in fallback_configs:
|
||||
for model in models:
|
||||
|
|
@ -419,6 +459,9 @@ class ConnectorGoogleSpeech:
|
|||
audioQueue: asyncio.Queue,
|
||||
language: str = "de-DE",
|
||||
phraseHints: Optional[list] = None,
|
||||
model: str = "latest_long",
|
||||
lightweight: bool = False,
|
||||
singleUtterance: bool = False,
|
||||
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""
|
||||
Stream audio chunks to Google Cloud Speech-to-Text Streaming API.
|
||||
|
|
@ -429,9 +472,13 @@ class ConnectorGoogleSpeech:
|
|||
Send (b"", True) to signal end of stream.
|
||||
language: Language code
|
||||
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:
|
||||
Dicts with keys: isFinal, transcript, confidence, stabilityScore, audioDurationSec
|
||||
Dicts with keys: isFinal, transcript, confidence, stabilityScore, audioDurationSec;
|
||||
optionally endOfSingleUtterance, reconnectRequired
|
||||
"""
|
||||
STREAM_LIMIT_SEC = 290
|
||||
streamStartTs = time.time()
|
||||
|
|
@ -442,9 +489,7 @@ class ConnectorGoogleSpeech:
|
|||
"sample_rate_hertz": 48000,
|
||||
"audio_channel_count": 1,
|
||||
"language_code": language,
|
||||
"enable_automatic_punctuation": True,
|
||||
"model": "latest_long",
|
||||
"use_enhanced": True,
|
||||
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight),
|
||||
}
|
||||
if phraseHints:
|
||||
configParams["speech_contexts"] = [speech.SpeechContext(phrases=phraseHints, boost=15.0)]
|
||||
|
|
@ -453,7 +498,7 @@ class ConnectorGoogleSpeech:
|
|||
streamingConfig = speech.StreamingRecognitionConfig(
|
||||
config=recognitionConfig,
|
||||
interim_results=True,
|
||||
single_utterance=False,
|
||||
single_utterance=singleUtterance,
|
||||
)
|
||||
|
||||
import queue as threadQueue
|
||||
|
|
@ -490,7 +535,22 @@ class ConnectorGoogleSpeech:
|
|||
)
|
||||
for response in responseStream:
|
||||
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 = []
|
||||
interimTexts = []
|
||||
|
|
@ -524,6 +584,13 @@ class ConnectorGoogleSpeech:
|
|||
"stabilityScore": 0.0,
|
||||
"audioDurationSec": estimatedDurationSec,
|
||||
}), 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:
|
||||
logger.info("Streaming STT approaching 5-min limit, client should reconnect")
|
||||
asyncio.run_coroutine_threadsafe(resultOutQ.put({
|
||||
|
|
@ -1030,7 +1097,7 @@ class ConnectorGoogleSpeech:
|
|||
voice exists, in which case the caller omits `name` and Google
|
||||
auto-selects based on languageCode + ssml_gender.
|
||||
"""
|
||||
return _catalogDefaultVoice(languageCode)
|
||||
return getDefaultVoice(languageCode)
|
||||
|
||||
async def getAvailableVoices(self, languageCode: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Swiss Parcel (Liegenschaften) Connector
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""ClickUp provider connector."""
|
||||
|
||||
from .connectorClickup import ClickupConnector
|
||||
|
||||
__all__ = ["ClickupConnector"]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""FTP/SFTP Provider Connector stub."""
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""Google Provider Connector -- 1 Connection : n Services (Drive, Gmail)."""
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""Microsoft Provider Connector -- 1 Connection : n Services (SharePoint, Outlook, Teams, OneDrive)."""
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
# 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())
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Unified modules.datamodels package.
|
||||
|
|
@ -13,4 +13,5 @@ from . import datamodelSecurity as security
|
|||
from . import datamodelChat as chat
|
||||
from . import datamodelFiles as files
|
||||
from . import datamodelVoice as voice
|
||||
from . import datamodelUtils as utils
|
||||
from . import datamodelUtils as utils
|
||||
from . import jsonContinuation
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
from typing import Optional, List, Dict, Any, Callable, TYPE_CHECKING, Tuple
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
|
@ -125,7 +125,7 @@ class AiModel(BaseModel):
|
|||
|
||||
# Metadata
|
||||
version: Optional[str] = Field(default=None, description="Model version")
|
||||
lastUpdated: Optional[str] = Field(default=None, description="Last update timestamp")
|
||||
lastUpdated: Optional[float] = Field(default=None, description="Last update timestamp (UTC unix)", json_schema_extra={"frontend_type": "timestamp"})
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True) # Allow Callable type
|
||||
|
||||
|
|
@ -162,6 +162,7 @@ class AiCallOptions(BaseModel):
|
|||
|
||||
# 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)")
|
||||
allowedModels: Optional[List[str]] = Field(default=None, description="Whitelist of allowed model names (AND-filter with allowedProviders). None/empty = all allowed.")
|
||||
|
||||
|
||||
class AiCallRequest(BaseModel):
|
||||
|
|
@ -244,11 +245,10 @@ class AiCallPromptWebCrawl(BaseModel):
|
|||
|
||||
class AiCallPromptImage(BaseModel):
|
||||
"""Structured prompt format for image generation."""
|
||||
|
||||
|
||||
prompt: str = Field(description="Text description of the image to generate")
|
||||
size: Optional[str] = Field(default="1024x1024", description="Image size (1024x1024, 1792x1024, 1024x1792)")
|
||||
quality: Optional[str] = Field(default="standard", description="Image quality (standard, hd)")
|
||||
style: Optional[str] = Field(default="vivid", description="Image style (vivid, natural)")
|
||||
size: Optional[str] = Field(default="1024x1024", description="Image size (1024x1024, 1536x1024, 1024x1536)")
|
||||
quality: Optional[str] = Field(default="auto", description="Image quality (auto, high, medium, low)")
|
||||
|
||||
|
||||
class AiProcessParameters(BaseModel):
|
||||
|
|
@ -351,4 +351,4 @@ class CodeContentPromptArgs(BaseModel):
|
|||
class CodeStructurePromptArgs(BaseModel):
|
||||
"""Type-safe arguments for code structure prompt builder."""
|
||||
userPrompt: str
|
||||
contentParts: List[ContentPart] = Field(default_factory=list)
|
||||
contentParts: List[ContentPart] = Field(default_factory=list)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""AI Audit Log data model for Compliance & AI-Datenfluss tracking.
|
||||
|
||||
|
|
@ -9,14 +9,15 @@ for compliance, audit, and data-protection reporting.
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import Field
|
||||
|
||||
from modules.datamodels.datamodelBase import PowerOnModel
|
||||
from modules.shared.i18nRegistry import i18nModel
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
|
||||
|
||||
@i18nModel("AI-Audit-Eintrag")
|
||||
class AiAuditLogEntry(BaseModel):
|
||||
class AiAuditLogEntry(PowerOnModel):
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Primary key",
|
||||
|
|
@ -34,7 +35,7 @@ class AiAuditLogEntry(BaseModel):
|
|||
|
||||
userId: str = Field(
|
||||
description="ID of the user who triggered the AI call",
|
||||
json_schema_extra={"label": "Benutzer-ID", "fk_target": {"db": "poweron_app", "table": "User"}},
|
||||
json_schema_extra={"label": "Benutzer-ID", "fk_target": {"db": "poweron_app", "table": "UserInDB", "labelField": "username", "softFk": True}},
|
||||
)
|
||||
username: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
@ -43,17 +44,17 @@ class AiAuditLogEntry(BaseModel):
|
|||
)
|
||||
mandateId: str = Field(
|
||||
description="Mandate context of the call",
|
||||
json_schema_extra={"label": "Mandanten-ID", "fk_target": {"db": "poweron_app", "table": "Mandate"}},
|
||||
json_schema_extra={"label": "Mandanten-ID", "fk_target": {"db": "poweron_app", "table": "Mandate", "labelField": "label", "softFk": True}},
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature instance context",
|
||||
json_schema_extra={"label": "Feature-Instanz-ID", "fk_target": {"db": "poweron_app", "table": "FeatureInstance"}},
|
||||
json_schema_extra={"label": "Feature-Instanz-ID", "fk_target": {"db": "poweron_app", "table": "FeatureInstance", "labelField": "label", "softFk": True}},
|
||||
)
|
||||
featureCode: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature code (e.g. workspace, trustee)",
|
||||
json_schema_extra={"label": "Feature", "fk_target": {"db": "poweron_app", "table": "Feature", "column": "code"}},
|
||||
json_schema_extra={"label": "Feature", "fk_target": {"db": "poweron_app", "table": "Feature", "column": "code", "labelField": "code", "softFk": True}},
|
||||
)
|
||||
instanceLabel: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue