Update hivegrid-cloud reference to latest main
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e98d38e716
commit
7b21524c86
3781 files changed
+373484
-225607
No files matched your search
@@ -14,23 +14,37 @@
|
||||
# BRAND_MAIL_LINK=#2563eb
|
||||
|
||||
# Core
|
||||
APP_NAME=ToolCTRL
|
||||
APP_NAME=HiveGrid
|
||||
# DATABASE_URL: pooled connection for application traffic. Point at PgBouncer
|
||||
# on port 6432 in environments where the pooler is deployed and append
|
||||
# `?pgbouncer=true&connection_limit=10` so Prisma disables prepared-statement
|
||||
# caching (transaction-mode pooling cannot keep them). DIRECT_URL (below) is
|
||||
# the unpooled connection used by `prisma migrate`. See docs/ops/pgbouncer.md.
|
||||
DATABASE_URL="postgres://user:pass@localhost:6432/boilerplate?pgbouncer=true&connection_limit=10"
|
||||
DATABASE_URL="postgres://user:pass@localhost:6432/hivegrid?pgbouncer=true&connection_limit=10"
|
||||
# SESSION_SECRET signs the session cookie. Min 32 bytes of entropy recommended (openssl rand -hex 32).
|
||||
SESSION_SECRET=
|
||||
# CRYPTO_SECRET is the AES key for stored workflow credentials. Use a SEPARATE secret from SESSION_SECRET.
|
||||
CRYPTO_SECRET=
|
||||
APP_BASE_DOMAIN=localhost
|
||||
WEB_APP_HOST=app.localhost
|
||||
# Local dev: include the vite port so cross-host redirects (login -> app/admin host)
|
||||
# keep :3000 — routing derives app.localhost:3000 etc. and hostMatches tolerates it.
|
||||
APP_BASE_DOMAIN=localhost:3000
|
||||
WEB_APP_HOST=app.localhost:3000
|
||||
# Public app origin used for embed snippets, mail links, and worker-to-web callbacks.
|
||||
# APP_URL=http://app.localhost:3000
|
||||
ADMIN_APP_HOST=admin.localhost
|
||||
API_HOST=api.localhost
|
||||
ADMIN_APP_HOST=admin.localhost:3000
|
||||
API_HOST=api.localhost:3000
|
||||
# MCP runs as its own public connector host, not below app/api.
|
||||
MCP_HOST=0.0.0.0
|
||||
MCP_PORT=8080
|
||||
MCP_ALLOWED_HOSTS=mcp.hivegrid.de,staging-mcp.hivegrid.de,localhost,127.0.0.1,::1
|
||||
MCP_ALLOWED_ORIGINS=https://mcp.hivegrid.de,https://staging-mcp.hivegrid.de,http://localhost,http://127.0.0.1
|
||||
# Public MCP resource origin and OAuth authorization-server origin. Set staging to
|
||||
# https://staging-mcp.hivegrid.de and https://staging-app.hivegrid.de.
|
||||
MCP_RESOURCE_URL=http://mcp.localhost:8080
|
||||
MCP_AUTHORIZATION_SERVER_URL=http://app.localhost:3000
|
||||
# Optional dedicated HMAC secret for hash-only MCP OAuth codes/tokens. When unset,
|
||||
# API_KEY_HASH_SECRET is reused so web and mcp agree on token lookup.
|
||||
MCP_OAUTH_TOKEN_HASH_SECRET=
|
||||
# APP_PROTOCOL=http
|
||||
|
||||
# Legacy API/auth tokens
|
||||
@@ -40,6 +54,8 @@ JWT_SECRET=
|
||||
# disabled. Set to `true` ONLY during the hash-metadata backfill window, then set
|
||||
# back to `false` once the backfill is verified.
|
||||
API_KEY_LEGACY_PLAINTEXT_FALLBACK=false
|
||||
# Stable HMAC secret for legacy API-key hash lookup/backfill. Keep unchanged once deployed.
|
||||
API_KEY_HASH_SECRET=
|
||||
|
||||
# Local development
|
||||
# Set to true only for disposable local databases when you want sample tenants/users.
|
||||
@@ -66,11 +82,18 @@ STORAGE_PUBLIC_BASE_URL=
|
||||
STORAGE_FORCE_PATH_STYLE=false
|
||||
|
||||
# Billing
|
||||
STRIPE_SK=
|
||||
STRIPE_WEBHOOK_SIGNING_SECRET=
|
||||
STRIPE_ACCOUNT_COUNTRY=US
|
||||
# Lago is self-hosted and used for billing plans, seats, subscriptions and invoices.
|
||||
# Do not append /api/v1; the gateway adds it.
|
||||
LAGO_API_URL=
|
||||
LAGO_API_KEY=
|
||||
LAGO_WEBHOOK_SECRET=
|
||||
LAGO_TIMEOUT_MS=10000
|
||||
# Mollie is used only as payment processor. Keep test/live keys in the secret store.
|
||||
MOLLIE_API_KEY=
|
||||
MOLLIE_PROFILE_ID=
|
||||
MOLLIE_TIMEOUT_MS=10000
|
||||
|
||||
# Worker delivery credentials for queued transactional email (toolctrl.email -> Mailjet)
|
||||
# Worker delivery credentials for queued transactional email (hivegrid.email -> Mailjet)
|
||||
MAILJET_API_KEY=
|
||||
MAILJET_SECRET_KEY=
|
||||
MAILJET_FROM_EMAIL=
|
||||
@@ -87,9 +110,19 @@ INTEGRATIONS_CONTACT_FORMSPREE=
|
||||
# Analytics (First-Party-Ingestion-Token; Google Analytics wurde EU-sterilisiert entfernt — R-P1a)
|
||||
ANALYTICS_TOKEN=
|
||||
|
||||
# mCaptcha (self-hosted PoW captcha — see infra/mcaptcha/README.md). Public bits
|
||||
# (instance URL + per-surface sitekeys) are safe to expose; the account secret is
|
||||
# server-only. Verification fails open by default (CAPTCHA_FAIL_OPEN).
|
||||
# Self-hosted captcha. mCaptcha remains the compatibility default; set
|
||||
# CAPTCHA_PROVIDER=cap to use Cap Standalone (see infra/cap/README.md).
|
||||
# Public bits (instance URL + per-surface sitekeys) are safe to expose; secrets
|
||||
# are server-only. Verification fails open by default (CAPTCHA_FAIL_OPEN).
|
||||
CAPTCHA_PROVIDER=mcaptcha
|
||||
CAP_INSTANCE_URL=
|
||||
CAP_SITE_SECRET=
|
||||
CAP_SITE_SECRET_LOGIN=
|
||||
CAP_SITE_SECRET_FORMS=
|
||||
CAP_SITE_SECRET_BOOKING=
|
||||
CAP_SITEKEY_LOGIN=
|
||||
CAP_SITEKEY_FORMS=
|
||||
CAP_SITEKEY_BOOKING=
|
||||
MCAPTCHA_INSTANCE_URL=
|
||||
MCAPTCHA_ACCOUNT_SECRET=
|
||||
MCAPTCHA_SITEKEY_LOGIN=
|
||||
@@ -197,21 +230,21 @@ MEILI_API_KEY=devkey
|
||||
# DIRECT_URL: unpooled Postgres URL (port 5432, bypasses PgBouncer). Required
|
||||
# by `prisma migrate` / `db push` so prepared statements survive. Apps still
|
||||
# use DATABASE_URL (above) for runtime queries. See docs/ops/pgbouncer.md.
|
||||
# DIRECT_URL="postgres://user:pass@localhost:5432/boilerplate"
|
||||
# DIRECT_URL="postgres://user:pass@localhost:5432/hivegrid"
|
||||
# DATABASE_READ_URL: optional Postgres read-replica used by getReadDbClient()
|
||||
# for hot-read paths (CRM/KB listings, dashboards, public sites/portals).
|
||||
# When unset or empty, getReadDbClient() falls back to DATABASE_URL so single-
|
||||
# DB dev and self-hosted setups work without any code change. Point at the
|
||||
# replica (or a PgBouncer-fronted replica) in staging/production. Replication
|
||||
# is eventually consistent — see docs/architecture/rw-split.md.
|
||||
# DATABASE_READ_URL="postgres://read_user:pass@replica.localhost:5432/boilerplate"
|
||||
# DATABASE_READ_URL="postgres://read_user:pass@replica.localhost:5432/hivegrid"
|
||||
# SERVER_URL=https://app.example.com
|
||||
# VERCEL_URL=
|
||||
|
||||
# Observability (opt-in; leave commented to keep the safe noop reporter and disabled tracing)
|
||||
# SENTRY_DSN=https://public@sentry.example/1
|
||||
# API (apps/api) and Worker (apps/worker) — independent DSNs so the two
|
||||
# processes report to separate Sentry projects (boilerplate-api / boilerplate-worker).
|
||||
# processes report to separate Sentry projects (hivegrid-api / hivegrid-worker).
|
||||
# SENTRY_DSN_API=https://public-api@sentry.example/3
|
||||
# SENTRY_DSN_WORKER=https://public-worker@sentry.example/4
|
||||
# SENTRY_ENVIRONMENT=production
|
||||
@@ -224,14 +257,14 @@ MEILI_API_KEY=devkey
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||
# Comma-separated key=value headers forwarded with every OTLP export.
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=x-sentry-auth=Sentry sentry_key=<public-key>
|
||||
# OTEL_SERVICE_NAME=boilerplate-api
|
||||
# OTEL_SERVICE_NAME=hivegrid-api
|
||||
# OTEL_TRACES_SAMPLER=parentbased_traceidratio
|
||||
# OTEL_TRACES_SAMPLER_ARG=0.1
|
||||
# OTEL_LOG_LEVEL=debug
|
||||
# Browser-side OTel (Vite-bundled). VITE_OTEL_EXPORTER_OTLP_ENDPOINT must be
|
||||
# CORS-reachable from the user agent; gateway/relay MUST be rate-limited.
|
||||
# VITE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com/v1/traces
|
||||
# VITE_OTEL_SERVICE_NAME=boilerplate-web
|
||||
# VITE_OTEL_SERVICE_NAME=hivegrid-web
|
||||
# Web (apps/web) — separate Sentry project from the API/Worker stack.
|
||||
# SENTRY_DSN_WEB=https://public-web@sentry.example/2
|
||||
# VITE_SENTRY_DSN_WEB=https://public-web@sentry.example/2
|
||||
@@ -243,14 +276,14 @@ MEILI_API_KEY=devkey
|
||||
# Required by .github/workflows/_sentry-release.yml and the @sentry/vite-plugin step.
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
# SENTRY_ORG=
|
||||
# SENTRY_PROJECT=boilerplate-web
|
||||
# SENTRY_URL=https://sentry.boiler.saas-ctrl.com
|
||||
# SENTRY_PROJECT=hivegrid-web
|
||||
# SENTRY_URL=https://sentry.saas-ctrl.com
|
||||
|
||||
# Asset CDN (opt-in; leave commented to keep origin-relative asset URLs).
|
||||
# When set at build time, Vite emits all asset URLs prefixed with this origin
|
||||
# so the CDN (Bunny.net by default — see docs/ops/asset-cdn.md) can pull from
|
||||
# the app server and serve users from the edge. Local dev ignores the var.
|
||||
# ASSET_CDN_URL=https://cdn.boiler.saas-ctrl.com
|
||||
# ASSET_CDN_URL=https://cdn.hivegrid.de
|
||||
# Bunny.net pull-zone id used by the post-deploy purge step. CI-only.
|
||||
# BUNNY_PULL_ZONE_ID=
|
||||
# Bunny.net account-level API key for cache-purge calls. CI-only.
|
||||
@@ -278,10 +311,10 @@ MEILI_API_KEY=devkey
|
||||
# Region tag surfaced in /status. Falls back to FLY_REGION when unset.
|
||||
# APP_REGION=
|
||||
|
||||
# Backup CLI (@toolctrl/cli). See docs/operations/backup.md for the full runbook.
|
||||
# Backup CLI (@hivegrid/cli). See docs/operations/backup.md for the full runbook.
|
||||
# BACKUP_S3_ENDPOINT=https://s3.example.com
|
||||
# BACKUP_S3_REGION=us-east-1
|
||||
# BACKUP_S3_BUCKET=toolctrl-backups
|
||||
# BACKUP_S3_BUCKET=hivegrid-backups
|
||||
# BACKUP_S3_ACCESS_KEY_ID=
|
||||
# BACKUP_S3_SECRET_ACCESS_KEY=
|
||||
# BACKUP_S3_SESSION_TOKEN=
|
||||
@@ -303,8 +336,8 @@ MEILI_API_KEY=devkey
|
||||
|
||||
# Embeddable form widget (Phase 05 #58 — packages/form-widget).
|
||||
# FORM_WIDGET_CDN_URL serves /embed/widget.js. Falls back to APP_URL when unset.
|
||||
# Captcha on form submissions uses the shared mCaptcha instance (MCAPTCHA_* at
|
||||
# the top of this file), opt-in per form AND gated behind the
|
||||
# Captcha on form submissions uses the shared self-hosted captcha provider
|
||||
# configured at the top of this file, opt-in per form AND gated behind the
|
||||
# `form-widget-turnstile` feature flag. See docs/features/form-widget.md.
|
||||
# FORM_WIDGET_CDN_URL=https://forms.example.com
|
||||
|
||||
@@ -313,7 +346,7 @@ MEILI_API_KEY=devkey
|
||||
# allowed) from which the public `/api/leads/capture` endpoint accepts
|
||||
# submissions. Fail-closed: empty list rejects all browser submissions.
|
||||
# MARKETING_RATE_LIMIT_PER_MIN is the per-IP submission limit (default 5).
|
||||
# Captcha is the shared mCaptcha instance (MCAPTCHA_*); flag is gated via
|
||||
# Captcha is the shared self-hosted captcha provider; flag is gated via
|
||||
# the `marketing-funnel-turnstile` feature flag.
|
||||
# See docs/features/marketing-funnel.md.
|
||||
# MARKETING_ALLOWED_ORIGINS=https://marketing.example.com,https://*.example.com
|
||||
@@ -374,10 +407,10 @@ MEILI_API_KEY=devkey
|
||||
# Admin-Panel SSO via Authentik (Phase 09 #82 — apps/web/app/modules/admin-sso)
|
||||
# ---------------------------------------------------------------------------
|
||||
# AUTHENTIK_ISSUER_URL is the OIDC issuer published by the Authentik
|
||||
# `toolctrl-admin-panel` provider (matches the `iss` claim in ID-tokens).
|
||||
# Typically `https://auth.<domain>/application/o/toolctrl-admin-panel/`.
|
||||
# `hivegrid-admin-panel` provider (matches the `iss` claim in ID-tokens).
|
||||
# Typically `https://auth.<domain>/application/o/hivegrid-admin-panel/`.
|
||||
# AUTHENTIK_ADMIN_CLIENT_ID must equal the blueprint client_id
|
||||
# (`toolctrl-admin-panel` per infra/authentik/bootstrap/blueprints/03-*).
|
||||
# (`hivegrid-admin-panel` per infra/authentik/bootstrap/blueprints/03-*).
|
||||
# AUTHENTIK_ADMIN_CLIENT_SECRET is fetched from the Authentik admin UI
|
||||
# after the blueprint applies; rotate via Authentik then redeploy.
|
||||
# AUTHENTIK_ADMIN_REDIRECT_URI is the exact callback URI registered with
|
||||
@@ -393,58 +426,14 @@ MEILI_API_KEY=devkey
|
||||
# short (1-2 bootstrap accounts max). Each fallback login emits an
|
||||
# `admin.login.fallback.success` audit event tagged severity=warn.
|
||||
# See docs/operations/admin-sso.md.
|
||||
# AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/toolctrl-admin-panel/
|
||||
# AUTHENTIK_ADMIN_CLIENT_ID=toolctrl-admin-panel
|
||||
# AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/hivegrid-admin-panel/
|
||||
# AUTHENTIK_ADMIN_CLIENT_ID=hivegrid-admin-panel
|
||||
# AUTHENTIK_ADMIN_CLIENT_SECRET=
|
||||
# AUTHENTIK_ADMIN_REDIRECT_URI=https://admin.example.com/auth/admin/callback
|
||||
# AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI=https://admin.example.com/admin/login?logged_out=1
|
||||
# AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI=https://admin.example.com/login?logged_out=1
|
||||
# ADMIN_FALLBACK_LOGIN_ENABLED=false
|
||||
# ADMIN_FALLBACK_LOGIN_EMAILS=
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Capacitor mobile + push backend (Phase 08 #65 — apps/mobile + apps/worker)
|
||||
# ---------------------------------------------------------------------------
|
||||
# FCM_SERVICE_ACCOUNT_JSON holds the Firebase Cloud Messaging service account
|
||||
# credentials used by the worker push job to dispatch Android notifications.
|
||||
# Accepted formats: (1) raw JSON, (2) base64-encoded JSON. Generate via the
|
||||
# Firebase console (Project settings → Service accounts → Generate new
|
||||
# private key). NEVER commit this — it grants full FCM send rights.
|
||||
#
|
||||
# APNS_KEY is the PEM contents of the .p8 Apple Push Notification Service
|
||||
# signing key (a standard PKCS#8 PEM block). APNS_KEY_ID is the 10-character
|
||||
# key id shown in the Apple Developer console next to the generated .p8 file.
|
||||
# APNS_TEAM_ID is the 10-character developer team id.
|
||||
# APNS_BUNDLE_ID defaults to de.toolctrl.app (matches capacitor.config.ts
|
||||
# > appId); override only when shipping a re-branded fork.
|
||||
#
|
||||
# MOBILE_LIVE_RELOAD_URL is a dev-only convenience: when set, apps/mobile
|
||||
# emits a Capacitor `server.url` in capacitor.config.ts so the native shell
|
||||
# loads the running web dev server (typically http://192.168.x.x:3000)
|
||||
# instead of the bundled webDir. Leave empty for production builds.
|
||||
# See docs/operations/capacitor.md.
|
||||
# FCM_SERVICE_ACCOUNT_JSON=
|
||||
# APNS_KEY=
|
||||
# APNS_KEY_ID=
|
||||
# APNS_TEAM_ID=
|
||||
# APNS_BUNDLE_ID=de.toolctrl.app
|
||||
# MOBILE_LIVE_RELOAD_URL=
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI-cost Phase 3 — Stripe overage billing (apps/worker cron + reconcile)
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI_OVERAGE_SUBMISSION_ENABLED is the master killswitch for the nightly
|
||||
# Stripe Billing Meter event submitter. When `false` (or `0`), AI events are still
|
||||
# marked `isOverage = true` + `markedUpCostCents` populated, but the cron
|
||||
# skips Stripe. Default: enabled when STRIPE_SK is configured. Use this to
|
||||
# pause submissions during an incident; the next enabled run backfills via
|
||||
# `stripeReportedAt IS NULL`.
|
||||
#
|
||||
# Per-plan markup multiplier + Stripe identifiers ship in
|
||||
# `packages/entitlements/src/ai-overage.ts` (`standardAIOverageDefaults`).
|
||||
# Configure live Stripe ids via /admin/settings/pricing/ai-overage (admin
|
||||
# UI writes an audit log) and/or per-tenant overrides on
|
||||
# `Tenant.aiQuotaOverrideJson` — see docs/features/ai-cost-billing.md.
|
||||
# AI_OVERAGE_SUBMISSION_ENABLED=true
|
||||
|
||||
# Phase 06: Plugin output sanitization (strict|moderate|off)
|
||||
PLUGIN_OUTPUT_SANITIZATION_STRICTNESS=moderate
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# REQUIRED CORE
|
||||
|
||||
flyctl secrets set APP_NAME=YOUR_APP_NAME \
|
||||
DATABASE_URL=postgres://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE} \
|
||||
SESSION_SECRET=<session-secret> \
|
||||
CRYPTO_SECRET=<crypto-secret> \
|
||||
API_ACCESS_TOKEN=<api-access-token> \
|
||||
JWT_SECRET=<jwt-secret> \
|
||||
APP_BASE_DOMAIN=your-domain.invalid \
|
||||
WEB_APP_HOST=app.your-domain.invalid \
|
||||
ADMIN_APP_HOST=admin.your-domain.invalid \
|
||||
API_HOST=api.your-domain.invalid \
|
||||
APP_PROTOCOL=https \
|
||||
--app YOUR_APP_NAME
|
||||
|
||||
# APP AND PROVIDER SECRETS
|
||||
|
||||
flyctl secrets set APP_NAME=YOUR_APP_NAME \
|
||||
DATABASE_URL=postgres://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE} \
|
||||
SESSION_SECRET=<session-secret> \
|
||||
CRYPTO_SECRET=<crypto-secret> \
|
||||
API_ACCESS_TOKEN=<api-access-token> \
|
||||
JWT_SECRET=<jwt-secret> \
|
||||
APP_BASE_DOMAIN=your-domain.invalid \
|
||||
WEB_APP_HOST=app.your-domain.invalid \
|
||||
ADMIN_APP_HOST=admin.your-domain.invalid \
|
||||
API_HOST=api.your-domain.invalid \
|
||||
APP_PROTOCOL=https \
|
||||
STORAGE_ENDPOINT=https://s3.your-domain.invalid \
|
||||
STORAGE_REGION=us-east-1 \
|
||||
STORAGE_BUCKET=toolctrl-exports \
|
||||
STORAGE_ACCESS_KEY_ID=<storage-access-key-id> \
|
||||
STORAGE_SECRET_ACCESS_KEY=<storage-secret-access-key> \
|
||||
STORAGE_SESSION_TOKEN= \
|
||||
STORAGE_PUBLIC_BASE_URL=https://cdn.your-domain.invalid \
|
||||
STORAGE_FORCE_PATH_STYLE=false \
|
||||
STRIPE_SK=<stripe-secret-key> \
|
||||
STRIPE_WEBHOOK_SIGNING_SECRET=<stripe-webhook-signing-secret> \
|
||||
STRIPE_ACCOUNT_COUNTRY=US \
|
||||
MAILJET_API_KEY=<mailjet-api-key> \
|
||||
MAILJET_SECRET_KEY=<mailjet-secret-key> \
|
||||
MAILJET_FROM_EMAIL=notifications@example.com \
|
||||
MAILJET_FROM_NAME=ToolCTRL \
|
||||
SUPPORT_EMAIL=support@example.com \
|
||||
COMPANY_NAME=ExampleCo \
|
||||
COMPANY_ADDRESS="Example address" \
|
||||
INTEGRATIONS_CONTACT_FORMSPREE=https://formspree.io/f/example \
|
||||
ANALYTICS_TOKEN=<analytics-token> \
|
||||
MCAPTCHA_INSTANCE_URL=<mcaptcha-instance-url> \
|
||||
MCAPTCHA_ACCOUNT_SECRET=<mcaptcha-account-secret> \
|
||||
MCAPTCHA_SITEKEY_LOGIN=<mcaptcha-sitekey-login> \
|
||||
MCAPTCHA_SITEKEY_FORMS=<mcaptcha-sitekey-forms> \
|
||||
MCAPTCHA_SITEKEY_BOOKING=<mcaptcha-sitekey-booking> \
|
||||
AUTH_MAGIC_LINK_ENABLED=false \
|
||||
AUTH_MAGIC_LINK_TOKEN_TTL_MINUTES=15 \
|
||||
NOTIFICATIONS_NOVU_API_KEY=<novu-api-key> \
|
||||
EXPORT_RETENTION_QUEUE_ENABLED=false \
|
||||
IMPORT_QUEUE_ENABLED=false \
|
||||
BULLMQ_REDIS_URL=redis://default:<redis-password>@redis.your-domain.invalid:6379/0 \
|
||||
BROWSER_RUNTIME_URL=https://browser-runtime.your-domain.invalid \
|
||||
BROWSER_RUNTIME_API_KEY=<browser-runtime-api-key> \
|
||||
BROWSER_SESSION_TIMEOUT_MS=60000 \
|
||||
IPSTACK_API_KEY=<ipstack-api-key> \
|
||||
IPAPIIS_API_KEY=<ipapiis-api-key> \
|
||||
--app YOUR_APP_NAME
|
||||
|
||||
# REDIS_URL is a fallback only when BULLMQ_REDIS_URL is absent; keep it commented if a platform injects it.
|
||||
# REDIS_URL=redis://default:<redis-password>@redis.example.com:6379/0
|
||||
|
||||
# PORTAL SECRETS
|
||||
|
||||
flyctl secrets set PORTAL_SERVER_URL=https://portal.your-domain.invalid \
|
||||
PORTAL_SERVER_FLY_TOKEN="FlyV1 <portal-token>" \
|
||||
--app YOUR_APP_NAME
|
||||
@@ -1,5 +1,5 @@
|
||||
# Core local development
|
||||
APP_NAME=ToolCTRL Local
|
||||
APP_NAME=HiveGrid Local
|
||||
DATABASE_URL="file:./dev.db?connection_limit=1&connect_timeout=30"
|
||||
SESSION_SECRET=replace-with-local-session-secret
|
||||
CRYPTO_SECRET=replace-with-local-crypto-secret
|
||||
@@ -28,8 +28,6 @@ DEMO_USER=admin@email.com:password
|
||||
# STORAGE_SESSION_TOKEN=
|
||||
# STORAGE_PUBLIC_BASE_URL=
|
||||
# STORAGE_FORCE_PATH_STYLE=false
|
||||
# STRIPE_SK=
|
||||
# STRIPE_ACCOUNT_COUNTRY=US
|
||||
# MAILJET_API_KEY=
|
||||
# MAILJET_SECRET_KEY=
|
||||
# MAILJET_FROM_EMAIL=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Required core
|
||||
APP_NAME=ToolCTRL
|
||||
APP_NAME=HiveGrid
|
||||
# DATABASE_URL points at PgBouncer (port 6432) for application traffic.
|
||||
# Append `?pgbouncer=true&connection_limit=10` so Prisma disables prepared-
|
||||
# statement caching (transaction-mode pooling cannot keep them). DIRECT_URL
|
||||
@@ -11,12 +11,19 @@ APP_BASE_DOMAIN=your-domain.invalid
|
||||
WEB_APP_HOST=app.your-domain.invalid
|
||||
ADMIN_APP_HOST=admin.your-domain.invalid
|
||||
API_HOST=api.your-domain.invalid
|
||||
MCP_HOST=0.0.0.0
|
||||
MCP_PORT=8080
|
||||
MCP_ALLOWED_HOSTS=mcp.your-domain.invalid,staging-mcp.your-domain.invalid
|
||||
MCP_ALLOWED_ORIGINS=https://mcp.your-domain.invalid,https://staging-mcp.your-domain.invalid
|
||||
MCP_RESOURCE_URL=https://mcp.your-domain.invalid
|
||||
MCP_AUTHORIZATION_SERVER_URL=https://app.your-domain.invalid
|
||||
APP_PROTOCOL=https
|
||||
|
||||
# Required for current legacy API/auth endpoints
|
||||
API_ACCESS_TOKEN=
|
||||
JWT_SECRET=
|
||||
API_KEY_LEGACY_PLAINTEXT_FALLBACK=false
|
||||
API_KEY_HASH_SECRET=
|
||||
|
||||
# Required if public analytics ingestion is enabled
|
||||
ANALYTICS_TOKEN=
|
||||
@@ -47,9 +54,6 @@ STORAGE_SECRET_ACCESS_KEY=
|
||||
STORAGE_SESSION_TOKEN=
|
||||
STORAGE_PUBLIC_BASE_URL=
|
||||
STORAGE_FORCE_PATH_STYLE=false
|
||||
STRIPE_SK=
|
||||
STRIPE_WEBHOOK_SIGNING_SECRET=
|
||||
STRIPE_ACCOUNT_COUNTRY=US
|
||||
MAILJET_API_KEY=
|
||||
MAILJET_SECRET_KEY=
|
||||
MAILJET_FROM_EMAIL=
|
||||
@@ -59,6 +63,15 @@ SUPPORT_EMAIL=
|
||||
COMPANY_NAME=
|
||||
COMPANY_ADDRESS=
|
||||
INTEGRATIONS_CONTACT_FORMSPREE=
|
||||
CAPTCHA_PROVIDER=cap
|
||||
CAP_INSTANCE_URL=https://captcha.hivegrid.de
|
||||
CAP_SITE_SECRET=
|
||||
CAP_SITE_SECRET_LOGIN=
|
||||
CAP_SITE_SECRET_FORMS=
|
||||
CAP_SITE_SECRET_BOOKING=
|
||||
CAP_SITEKEY_LOGIN=
|
||||
CAP_SITEKEY_FORMS=
|
||||
CAP_SITEKEY_BOOKING=
|
||||
MCAPTCHA_INSTANCE_URL=
|
||||
MCAPTCHA_ACCOUNT_SECRET=
|
||||
MCAPTCHA_SITEKEY_LOGIN=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Non-secret test fixture used by env-contract checks.
|
||||
# Values are deterministic placeholders only; never copy production secrets here.
|
||||
APP_NAME=ToolCTRL Test
|
||||
APP_NAME=HiveGrid Test
|
||||
DATABASE_URL="file:./test.db?connection_limit=1&connect_timeout=30"
|
||||
SESSION_SECRET=test-session-secret-placeholder
|
||||
CRYPTO_SECRET=test-crypto-secret-placeholder
|
||||
@@ -8,11 +8,18 @@ APP_BASE_DOMAIN=localhost.test
|
||||
WEB_APP_HOST=app.localhost.test
|
||||
ADMIN_APP_HOST=admin.localhost.test
|
||||
API_HOST=api.localhost.test
|
||||
MCP_HOST=127.0.0.1
|
||||
MCP_PORT=8080
|
||||
MCP_ALLOWED_HOSTS=mcp.localhost.test,localhost,127.0.0.1,::1
|
||||
MCP_ALLOWED_ORIGINS=http://mcp.localhost.test,http://localhost,http://127.0.0.1
|
||||
MCP_RESOURCE_URL=http://mcp.localhost.test:8080
|
||||
MCP_AUTHORIZATION_SERVER_URL=http://app.localhost.test
|
||||
APP_PROTOCOL=http
|
||||
|
||||
# Legacy API/auth tokens
|
||||
API_ACCESS_TOKEN=test-api-access-token-placeholder
|
||||
JWT_SECRET=test-jwt-secret-placeholder
|
||||
API_KEY_HASH_SECRET=test-api-key-hash-secret-placeholder
|
||||
|
||||
# Test-safe platform/runtime values
|
||||
NODE_ENV=test
|
||||
@@ -43,5 +50,4 @@ SUPPORT_EMAIL=support@example.com
|
||||
COMPANY_NAME=Example Test Company
|
||||
COMPANY_ADDRESS=123 Test Street
|
||||
ANALYTICS_TOKEN=test-analytics-token-placeholder
|
||||
STRIPE_ACCOUNT_COUNTRY=US
|
||||
SITE_KEY_EXAMPLE_SITE=test-site-key-placeholder
|
||||
@@ -55,26 +55,26 @@
|
||||
"@prisma/client",
|
||||
"@prisma/client/*"
|
||||
],
|
||||
"message": "Import Prisma through @toolctrl/db package boundaries instead of generated clients."
|
||||
"message": "Import Prisma through @hivegrid/db package boundaries instead of generated clients."
|
||||
},
|
||||
{
|
||||
"group": ["@toolctrl/entitlements/src/*", "packages/entitlements/src/*"],
|
||||
"message": "Import entitlements through the public @toolctrl/entitlements package boundary."
|
||||
"group": ["@hivegrid/entitlements/src/*", "packages/entitlements/src/*"],
|
||||
"message": "Import entitlements through the public @hivegrid/entitlements package boundary."
|
||||
},
|
||||
{
|
||||
"group": [
|
||||
"@toolctrl/foundation/src/*",
|
||||
"@hivegrid/foundation/src/*",
|
||||
"packages/foundation/src/*",
|
||||
"@toolctrl/view-models/src/*",
|
||||
"@hivegrid/view-models/src/*",
|
||||
"packages/view-models/src/*",
|
||||
"@toolctrl/ui-app/src/*",
|
||||
"@hivegrid/ui-app/src/*",
|
||||
"packages/ui-app/src/*",
|
||||
"@toolctrl/ui-subframe/src/*",
|
||||
"@hivegrid/ui-subframe/src/*",
|
||||
"packages/ui-subframe/src/*",
|
||||
"@toolctrl/ui-mock/src/*",
|
||||
"@hivegrid/ui-mock/src/*",
|
||||
"packages/ui-mock/src/*"
|
||||
],
|
||||
"message": "Import Phase C packages through their public @toolctrl/* package boundaries."
|
||||
"message": "Import Phase C packages through their public @hivegrid/* package boundaries."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -151,7 +151,7 @@
|
||||
"@prisma/client",
|
||||
"@prisma/client/*"
|
||||
],
|
||||
"message": "Import Prisma through package-owned generated entrypoints inside @toolctrl/db boundaries."
|
||||
"message": "Import Prisma through package-owned generated entrypoints inside @hivegrid/db boundaries."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -173,7 +173,7 @@
|
||||
"@prisma/client",
|
||||
"@prisma/client/*"
|
||||
],
|
||||
"message": "Import Prisma through @toolctrl/db package boundaries instead of generated clients."
|
||||
"message": "Import Prisma through @hivegrid/db package boundaries instead of generated clients."
|
||||
},
|
||||
{
|
||||
"group": ["react", "react/*"],
|
||||
@@ -228,8 +228,8 @@
|
||||
"apps/web/app/generated/prisma/client/*",
|
||||
"@prisma/client",
|
||||
"@prisma/client/*",
|
||||
"@toolctrl/db",
|
||||
"@toolctrl/db/*"
|
||||
"@hivegrid/db",
|
||||
"@hivegrid/db/*"
|
||||
],
|
||||
"message": "UI packages must not access DB boundaries directly."
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema-comment": "Baseline catalogue of jsx-a11y/recommended rules that are currently disabled in .eslintrc because the codebase has pre-existing violations. The plan is to drive these counts down to zero, then promote each rule back to `error` in .eslintrc and remove it from this file. See docs/contributing/wcag-audit.md for the reduction policy. Counts are produced by `scripts/verify-a11y-baseline.mjs`.",
|
||||
"generatedAt": "2026-05-17",
|
||||
"lastUpdatedAt": "2026-05-28",
|
||||
"scope": ["apps/web/app/**/*.tsx", "packages/ui/src/stories/**/*.tsx"],
|
||||
"lastUpdatedAt": "2026-07-01",
|
||||
"scope": ["apps/web/app/**/*.tsx"],
|
||||
"ruleset": "plugin:jsx-a11y/recommended",
|
||||
"totalViolations": 81,
|
||||
"totalViolations": 56,
|
||||
"rules": {
|
||||
"jsx-a11y/no-autofocus": {
|
||||
"violations": 81,
|
||||
"violations": 56,
|
||||
"severity": "P2",
|
||||
"rationale": "Autofocus remains in form/editor surfaces. Button/link wrappers now use programmatic focus instead of DOM autoFocus. Migration plan: prefer programmatic focus on open() so screen-reader users hear the modal title first."
|
||||
}
|
||||
@@ -39,11 +39,11 @@
|
||||
},
|
||||
"jsx-a11y/anchor-is-valid": {
|
||||
"promotedAt": "2026-05-25",
|
||||
"rationale": "Reached 0 violations after Storybook Popover demo menu anchors were converted to button controls. Rule moved back to `error` and the `off` override removed from .eslintrc."
|
||||
"rationale": "Reached 0 violations after Popover demo menu anchors were converted to button controls. Rule moved back to `error` and the `off` override removed from .eslintrc."
|
||||
},
|
||||
"jsx-a11y/no-noninteractive-tabindex": {
|
||||
"promotedAt": "2026-05-25",
|
||||
"rationale": "Verifier reproduced 0 violations across the tracked app and Storybook scope. Rule moved back to `error` and the stale `off` override was removed from .eslintrc."
|
||||
"rationale": "Verifier reproduced 0 violations across the tracked app and UI scope. Rule moved back to `error` and the stale `off` override was removed from .eslintrc."
|
||||
},
|
||||
"jsx-a11y/media-has-caption": {
|
||||
"promotedAt": "2026-05-28",
|
||||
|
||||
@@ -7,7 +7,7 @@ description: |
|
||||
|
||||
inputs:
|
||||
project:
|
||||
description: "Sentry project slug (boilerplate-api|boilerplate-worker|boilerplate-web)"
|
||||
description: "Sentry project slug (hivegrid-api|hivegrid-worker|hivegrid-web)"
|
||||
required: true
|
||||
environment:
|
||||
description: "Deployment environment label (production|staging|preview)"
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
project:
|
||||
description: "Sentry project slug (boilerplate-api|boilerplate-worker|boilerplate-web)"
|
||||
description: "Sentry project slug (api|worker|web) in the 'sentry' org on sentry.saas-ctrl.com"
|
||||
required: true
|
||||
type: string
|
||||
environment:
|
||||
@@ -38,7 +38,7 @@ on:
|
||||
required: true
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -48,12 +48,12 @@ jobs:
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Download source-map artifact
|
||||
if: ${{ inputs.sourcemaps-artifact != 'none' }}
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ inputs.sourcemaps-artifact }}
|
||||
path: ${{ inputs.sourcemaps-path }}
|
||||
|
||||
@@ -5,7 +5,7 @@ name: Accessibility
|
||||
#
|
||||
# Mirror of .github/workflows/a11y.yml for the Gitea runner.
|
||||
#
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/wcag-internal.md
|
||||
# Docs: docs/contributing/wcag-audit.md
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -13,18 +13,44 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
# Cancel superseded runs so rapid pushes don't pile up unclaimed on the runner
|
||||
# (mirrors ci.yml). Without this each push stacked a new a11y run on the queue.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
axe:
|
||||
name: axe-core + color-contrast
|
||||
runs-on: ubuntu-latest
|
||||
# Use the self-hosted Gitea runner label (like every ci.yml job). The previous
|
||||
# `ubuntu-latest` was a leftover from the GitHub mirror and matches no Gitea
|
||||
# runner, so this job sat pending forever (never claimed) on every PR + push.
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
# Same repo variable ci.yml exposes top-level: the internal Gitea host IP that the
|
||||
# "Resolve internal Gitea host" step writes to /etc/hosts so checkout can clone
|
||||
# git.tool-ctrl.de inside the runner container. Without it the step is a no-op.
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
steps:
|
||||
# Same as every ci.yml job: make the internal Gitea host resolvable inside the
|
||||
# runner container so actions/checkout can clone git.tool-ctrl.de. Without this the
|
||||
# checkout step fails (the job never had it -> a11y could never run on Gitea).
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ name: Bundle Budget
|
||||
|
||||
# Gitea variant of .github/workflows/bundle-budget.yml. Uses Gitea-API to
|
||||
# post / upsert a sticky PR comment with per-route gzip sizes.
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/bundle-budget.md
|
||||
# Docs: docs/architecture/bundle-budgets.md
|
||||
|
||||
on:
|
||||
@@ -26,7 +25,7 @@ on:
|
||||
- ".gitea/workflows/bundle-budget.yml"
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
jobs:
|
||||
measure:
|
||||
@@ -34,16 +33,16 @@ jobs:
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout PR head
|
||||
@@ -67,10 +66,10 @@ jobs:
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Build web (PR head)
|
||||
run: pnpm --filter @toolctrl/web build
|
||||
run: pnpm --filter @hivegrid/web build
|
||||
|
||||
- name: Verify aggregate bundle budgets
|
||||
run: pnpm run verify:bundle-size
|
||||
|
||||
@@ -9,24 +9,29 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: Quality Gates
|
||||
# The former single "verify" gate is split into two parallel jobs (verify-1 + verify-2)
|
||||
# to halve CI wallclock. The two heavy UI checks (ui-visual + hot-swap) sit in verify-1,
|
||||
# the two heavy accessibility checks (a11y-coverage + a11y smoke) in verify-2, with the
|
||||
# lighter static gates spread evenly. Every required command still runs exactly once
|
||||
# across both jobs and keeps its canonical relative order WITHIN its owning job.
|
||||
verify-1:
|
||||
name: Quality Static Verify (1/2)
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
@@ -64,6 +69,9 @@ jobs:
|
||||
- name: Verify deploy readiness
|
||||
run: pnpm run verify:deploy-readiness
|
||||
|
||||
- name: Verify Docker baseline
|
||||
run: pnpm run verify:docker-baseline
|
||||
|
||||
- name: Verify Docker non-root runtime
|
||||
run: pnpm run verify:docker-non-root
|
||||
|
||||
@@ -85,9 +93,58 @@ jobs:
|
||||
- name: Verify API inventory
|
||||
run: pnpm run verify:api-inventory
|
||||
|
||||
- name: Verify UI mock static baseline
|
||||
run: pnpm run verify:ui-visual
|
||||
|
||||
- name: Test UI mock hot-swap
|
||||
run: pnpm run test:hot-swap
|
||||
|
||||
verify-2:
|
||||
name: Quality Static Verify (2/2)
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Verify tenant isolation report
|
||||
run: pnpm run verify:tenant-isolation-report
|
||||
|
||||
- name: Verify cross-cutting model registry completeness
|
||||
run: pnpm run verify:cross-cutting-models
|
||||
|
||||
- name: Verify sidebar routes
|
||||
run: pnpm run verify:sidebar-routes
|
||||
|
||||
@@ -102,16 +159,8 @@ jobs:
|
||||
- name: Verify Phase C package boundaries
|
||||
run: pnpm run verify:phase-c-boundaries
|
||||
|
||||
- name: Verify Feature Manifest plugin removal gates
|
||||
run: |
|
||||
pnpm run verify:core-build-without-plugins-smoke
|
||||
pnpm run verify:removed-modules
|
||||
|
||||
- name: Verify UI mock static baseline
|
||||
run: pnpm run verify:ui-visual
|
||||
|
||||
- name: Test UI mock hot-swap
|
||||
run: pnpm run test:hot-swap
|
||||
- name: Verify Feature Manifest removed modules
|
||||
run: pnpm run verify:removed-modules
|
||||
|
||||
- name: Verify accessibility coverage
|
||||
run: pnpm run verify:a11y-coverage
|
||||
@@ -126,6 +175,46 @@ jobs:
|
||||
pnpm run verify:release-docs
|
||||
pnpm run verify:docs-links
|
||||
|
||||
typecheck-test:
|
||||
name: Quality Typecheck and Tests
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
@@ -138,15 +227,55 @@ jobs:
|
||||
- name: Workspace package tests
|
||||
run: pnpm run test:packages
|
||||
|
||||
- name: Mobile wrapper tests
|
||||
run: pnpm run test:mobile
|
||||
|
||||
- name: Unit tests
|
||||
run: pnpm test
|
||||
|
||||
build:
|
||||
name: Quality Build and Bundle
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Verify Feature Manifest core without plugins
|
||||
run: pnpm run verify:core-build-without-plugins-smoke
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
TOOLCTRL_BUILD_SOURCEMAPS: "1"
|
||||
HIVEGRID_BUILD_SOURCEMAPS: "1"
|
||||
run: pnpm run build:ci
|
||||
|
||||
- name: Verify bundle size
|
||||
@@ -156,7 +285,7 @@ jobs:
|
||||
run: test -n "$(find apps/web/build/client -type f -name '*.map' -print -quit)"
|
||||
|
||||
- name: Upload web Sentry sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: web-client-sourcemaps
|
||||
path: apps/web/build/client
|
||||
@@ -171,16 +300,16 @@ jobs:
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
@@ -210,29 +339,78 @@ jobs:
|
||||
run: pnpm run verify:openapi-security
|
||||
|
||||
- name: Typecheck API
|
||||
run: pnpm --filter @toolctrl/api typecheck
|
||||
run: pnpm --filter @hivegrid/api typecheck
|
||||
|
||||
- name: Test API
|
||||
run: pnpm exec vitest run apps/api/src
|
||||
|
||||
- name: Build API
|
||||
run: pnpm --filter @toolctrl/api build
|
||||
run: pnpm --filter @hivegrid/api build
|
||||
|
||||
mcp:
|
||||
name: MCP Service
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Typecheck MCP
|
||||
run: pnpm --filter @hivegrid/mcp typecheck
|
||||
|
||||
- name: Test MCP
|
||||
run: pnpm --filter @hivegrid/mcp test
|
||||
|
||||
- name: Build MCP
|
||||
run: pnpm --filter @hivegrid/mcp build
|
||||
|
||||
worker:
|
||||
name: Worker Service
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
@@ -259,19 +437,19 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Typecheck Worker
|
||||
run: pnpm --filter @toolctrl/worker typecheck
|
||||
run: pnpm --filter @hivegrid/worker typecheck
|
||||
|
||||
- name: Test Worker
|
||||
run: pnpm --filter @toolctrl/worker exec vitest run
|
||||
run: pnpm --filter @hivegrid/worker exec vitest run
|
||||
|
||||
- name: Verify workflow outbox native readiness
|
||||
run: pnpm run verify:workflow-outbox-native
|
||||
|
||||
- name: Build Worker
|
||||
run: pnpm --filter @toolctrl/worker build
|
||||
run: pnpm --filter @hivegrid/worker build
|
||||
|
||||
- name: Health Check Worker
|
||||
run: pnpm --filter @toolctrl/worker health
|
||||
run: pnpm --filter @hivegrid/worker health
|
||||
env:
|
||||
WORKER_ENABLED: "false"
|
||||
|
||||
@@ -282,12 +460,12 @@ jobs:
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
@@ -316,7 +494,30 @@ jobs:
|
||||
- name: Generate database client
|
||||
run: pnpm run db:generate
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
|
||||
- name: Setup Docker CLI
|
||||
run: |
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
if ! command -v apt-get >/dev/null 2>&1 || [ "$(id -u)" != "0" ]; then
|
||||
echo "Docker CLI missing and apt-get is not available as root"
|
||||
exit 1
|
||||
fi
|
||||
apt-get update -qq
|
||||
apt-get install -qq -y --no-install-recommends ca-certificates curl
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
. /etc/os-release
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq
|
||||
apt-get install -qq -y --no-install-recommends docker-ce-cli
|
||||
fi
|
||||
|
||||
docker --version
|
||||
|
||||
- name: Verify migrations schema parity
|
||||
run: pnpm run verify:migrations-parity
|
||||
|
||||
- name: Verify database migration status
|
||||
if: gitea.event_name != 'pull_request'
|
||||
@@ -328,22 +529,25 @@ jobs:
|
||||
COOLIFY_STAGING_SSH_KEY: ${{ secrets.COOLIFY_STAGING_SSH_KEY }}
|
||||
|
||||
- name: Test database package
|
||||
run: pnpm --filter @toolctrl/db test
|
||||
run: pnpm --filter @hivegrid/db test
|
||||
|
||||
docker-smoke:
|
||||
name: Docker Smoke
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 30
|
||||
needs: [quality, api, worker, database]
|
||||
# No needs: the Docker images are built from source inside this job (no dependency on the
|
||||
# quality/build jobs' outputs), so it runs fully in parallel instead of waiting on the
|
||||
# ~4 min quality phase -> off the critical tail. Trade-off: if quality fails, the images
|
||||
# still build (wasted compute), but the run fails overall anyway.
|
||||
steps:
|
||||
- name: Setup hosts override and Docker CLI
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1 || ! docker buildx version >/dev/null 2>&1 || ! docker compose version >/dev/null 2>&1; then
|
||||
@@ -372,99 +576,56 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Layer-cache strategy: this job runs on the self-hosted toolctrl-ci runner against the
|
||||
# mounted host Docker daemon, whose BuildKit keeps a persistent local layer cache between
|
||||
# CI runs (proven by the fact the `docker run hivegrid-*:smoke` steps below find images the
|
||||
# build loaded into the host store). The Dockerfile additionally persists the pnpm store via
|
||||
# `RUN --mount=type=cache,id=hivegrid-pnpm-store`. The three targets share base/deps/build
|
||||
# stages, so a single daemon dedupes them within a run. No explicit `--cache-to/--cache-from`
|
||||
# (type=local/registry) is configured: it would add cold first runs, import/export overhead
|
||||
# and would require `--load` for the smoke steps, for no gain over the warm host cache.
|
||||
# `type=gha` is intentionally not used here — it is GitHub-hosted-runner-only.
|
||||
- name: Build Docker smoke targets
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
docker build --target web --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-web:smoke . &
|
||||
docker build --target web --build-arg APP_REVISION=ci-smoke-revision --tag hivegrid-web:smoke . &
|
||||
web_pid=$!
|
||||
docker build --target api --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-api:smoke . &
|
||||
docker build --target api --build-arg APP_REVISION=ci-smoke-revision --tag hivegrid-api:smoke . &
|
||||
api_pid=$!
|
||||
docker build --target worker --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-worker:smoke . &
|
||||
docker build --target mcp --build-arg APP_REVISION=ci-smoke-revision --tag hivegrid-mcp:smoke . &
|
||||
mcp_pid=$!
|
||||
docker build --target worker --build-arg APP_REVISION=ci-smoke-revision --tag hivegrid-worker:smoke . &
|
||||
worker_pid=$!
|
||||
|
||||
status=0
|
||||
wait "$web_pid" || status=$?
|
||||
wait "$api_pid" || status=$?
|
||||
wait "$mcp_pid" || status=$?
|
||||
wait "$worker_pid" || status=$?
|
||||
exit "$status"
|
||||
|
||||
- name: Validate web Docker artifacts
|
||||
run: docker run --rm --entrypoint /bin/sh toolctrl-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/build/server-runtime/server.js'
|
||||
run: docker run --rm --entrypoint /bin/sh hivegrid-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/build/server-runtime/server.js'
|
||||
|
||||
- name: Validate web Docker runtime
|
||||
run: bash scripts/docker-web-runtime-smoke.sh toolctrl-web:smoke
|
||||
run: bash scripts/docker-web-runtime-smoke.sh hivegrid-web:smoke
|
||||
env:
|
||||
DOCKER_WEB_SMOKE_TRANSPORT: exec
|
||||
|
||||
- name: Validate API Docker runtime
|
||||
run: bash scripts/docker-api-runtime-smoke.sh toolctrl-api:smoke
|
||||
run: bash scripts/docker-api-runtime-smoke.sh hivegrid-api:smoke
|
||||
env:
|
||||
DOCKER_API_SMOKE_TRANSPORT: exec
|
||||
|
||||
- name: Validate MCP Docker runtime
|
||||
run: bash scripts/docker-mcp-runtime-smoke.sh hivegrid-mcp:smoke
|
||||
env:
|
||||
DOCKER_MCP_SMOKE_TRANSPORT: exec
|
||||
|
||||
- name: Validate worker Docker runtime
|
||||
run: bash scripts/docker-worker-runtime-smoke.sh toolctrl-worker:smoke
|
||||
run: bash scripts/docker-worker-runtime-smoke.sh hivegrid-worker:smoke
|
||||
|
||||
- name: Validate Docker Compose config
|
||||
run: docker compose config
|
||||
|
||||
resolve-sentry-environment:
|
||||
name: Resolve Sentry environment
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 5
|
||||
needs: docker-smoke
|
||||
outputs:
|
||||
environment: ${{ steps.resolve.outputs.environment }}
|
||||
steps:
|
||||
- name: Resolve target environment from ref
|
||||
id: resolve
|
||||
run: |
|
||||
REF="${GITEA_REF:-${{ gitea.ref }}}"
|
||||
if [ "$REF" = "refs/heads/main" ]; then
|
||||
echo "environment=production" >> "$GITHUB_OUTPUT"
|
||||
elif [ "$REF" = "refs/heads/staging" ]; then
|
||||
echo "environment=staging" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "environment=preview" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
sentry-release-api:
|
||||
name: Sentry release (api)
|
||||
needs: resolve-sentry-environment
|
||||
uses: ./.gitea/workflows/_sentry-release.yml
|
||||
with:
|
||||
project: boilerplate-api
|
||||
environment: ${{ needs.resolve-sentry-environment.outputs.environment }}
|
||||
sourcemaps-path: none
|
||||
secrets:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
|
||||
sentry-release-worker:
|
||||
name: Sentry release (worker)
|
||||
needs: resolve-sentry-environment
|
||||
uses: ./.gitea/workflows/_sentry-release.yml
|
||||
with:
|
||||
project: boilerplate-worker
|
||||
environment: ${{ needs.resolve-sentry-environment.outputs.environment }}
|
||||
sourcemaps-path: none
|
||||
secrets:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
|
||||
sentry-release-web:
|
||||
name: Sentry release (web)
|
||||
needs: resolve-sentry-environment
|
||||
uses: ./.gitea/workflows/_sentry-release.yml
|
||||
with:
|
||||
project: boilerplate-web
|
||||
environment: ${{ needs.resolve-sentry-environment.outputs.environment }}
|
||||
sourcemaps-path: apps/web/build/client
|
||||
sourcemaps-artifact: web-client-sourcemaps
|
||||
secrets:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
@@ -4,7 +4,6 @@ name: Coverage
|
||||
# and the per-path gate (scripts/verify-test-coverage.mjs) and upserts a
|
||||
# sticky PR comment via the Gitea API.
|
||||
#
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/test-coverage.md
|
||||
# Docs: docs/architecture/test-coverage.md
|
||||
|
||||
on:
|
||||
@@ -19,7 +18,7 @@ on:
|
||||
- ".gitea/workflows/coverage.yml"
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
@@ -29,12 +28,12 @@ jobs:
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout PR head
|
||||
@@ -46,8 +45,19 @@ jobs:
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Run vitest with coverage (enforces per-path gates)
|
||||
run: pnpm run verify:test-coverage
|
||||
|
||||
@@ -3,6 +3,20 @@ name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Skip the staging deploy for test-/docs-only merges (e.g. coverage waves, doc updates).
|
||||
# paths-ignore skips the run only when EVERY changed file matches a pattern; any app or
|
||||
# config source change still deploys. Manual deploys via workflow_dispatch are unaffected.
|
||||
paths-ignore:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/*.test.mjs"
|
||||
- "**/*.test.js"
|
||||
- "**/*.spec.ts"
|
||||
- "docs/**"
|
||||
- ".planning/**"
|
||||
- "**/*.md"
|
||||
- ".gitea/workflows/**"
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
@@ -18,10 +32,10 @@ on:
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
concurrency:
|
||||
group: boilerplate-deploy
|
||||
group: hivegrid-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
@@ -32,12 +46,12 @@ jobs:
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
@@ -120,12 +134,12 @@ jobs:
|
||||
|
||||
- name: Setup hosts override and Docker CLI
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
MISSING_TOOLS=""
|
||||
@@ -191,7 +205,7 @@ jobs:
|
||||
git cat-file -e "$IMAGE_TAG^{commit}"
|
||||
git merge-base --is-ancestor "$IMAGE_TAG" origin/main
|
||||
|
||||
for TARGET in web api worker; do
|
||||
for TARGET in web api worker mcp; do
|
||||
IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/$TARGET"
|
||||
SHA_DIGEST="$(docker buildx imagetools inspect "$IMAGE:$IMAGE_TAG" --format '{{json .Manifest.Digest}}')"
|
||||
STAGING_DIGEST="$(docker buildx imagetools inspect "$IMAGE:staging" --format '{{json .Manifest.Digest}}')"
|
||||
@@ -215,7 +229,7 @@ jobs:
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
BUILD_PIDS=()
|
||||
BUILD_TARGETS=()
|
||||
for TARGET in web api worker; do
|
||||
for TARGET in web api worker mcp; do
|
||||
(
|
||||
IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/$TARGET"
|
||||
CACHE_REF="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/build-cache:$TARGET"
|
||||
@@ -229,22 +243,28 @@ jobs:
|
||||
--build-arg VITE_SENTRY_ENVIRONMENT="$VITE_SENTRY_ENVIRONMENT"
|
||||
--build-arg VITE_SENTRY_RELEASE="$SHA"
|
||||
--build-arg VITE_OTEL_EXPORTER_OTLP_ENDPOINT="$VITE_OTEL_EXPORTER_OTLP_ENDPOINT"
|
||||
--build-arg VITE_OTEL_SERVICE_NAME="boilerplate-web"
|
||||
--build-arg VITE_OTEL_SERVICE_NAME="hivegrid-web"
|
||||
)
|
||||
fi
|
||||
# `ignore-error=true` on the cache export: writing the build cache
|
||||
# to the registry is an optimization, not part of producing/pushing
|
||||
# the image. The Gitea registry rate-limits (HTTP 429) build-cache
|
||||
# blob writes under bursts of back-to-back deploys (made worse by
|
||||
# web/api/worker exporting to the same cache in parallel); without
|
||||
# this flag a 429 on a cache layer fails the whole build even though
|
||||
# the image itself built and pushed fine.
|
||||
docker buildx build --push --target "$TARGET" --file Dockerfile \
|
||||
--cache-from "type=registry,ref=$CACHE_REF" \
|
||||
--cache-to "type=registry,ref=$CACHE_REF,mode=max,ignore-error=true" \
|
||||
--build-arg APP_REVISION="$SHA" \
|
||||
"${WEB_BUILD_ARGS[@]}" \
|
||||
--tag "$IMAGE:$IMAGE_TAG" .
|
||||
# The registry cache is read-only in CI. Exporting build-cache blobs
|
||||
# to Gitea is optional and has repeatedly failed staging deploys with
|
||||
# registry-side 499/context deadline errors after the image itself
|
||||
# built successfully.
|
||||
for ATTEMPT in 1 2 3; do
|
||||
if docker buildx build --push --target "$TARGET" --provenance=false --file Dockerfile \
|
||||
--cache-from "type=registry,ref=$CACHE_REF" \
|
||||
--build-arg APP_REVISION="$SHA" \
|
||||
"${WEB_BUILD_ARGS[@]}" \
|
||||
--tag "$IMAGE:$IMAGE_TAG" .; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$ATTEMPT" -lt 3 ]; then
|
||||
echo "Docker build/push failed for $TARGET on attempt $ATTEMPT; retrying"
|
||||
sleep $((ATTEMPT * 20))
|
||||
fi
|
||||
done
|
||||
echo "Docker build/push failed for $TARGET after 3 attempts"
|
||||
exit 1
|
||||
) &
|
||||
BUILD_PIDS+=("$!")
|
||||
BUILD_TARGETS+=("$TARGET")
|
||||
@@ -275,7 +295,7 @@ jobs:
|
||||
echo "Cosign signing skipped: COSIGN_PRIVATE_KEY or COSIGN_PASSWORD not configured"
|
||||
exit 0
|
||||
fi
|
||||
for TARGET in web api worker; do
|
||||
for TARGET in web api worker mcp; do
|
||||
IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/$TARGET"
|
||||
cosign sign --yes --key env://COSIGN_PRIVATE_KEY "$IMAGE:$IMAGE_TAG"
|
||||
done
|
||||
@@ -300,16 +320,19 @@ jobs:
|
||||
|
||||
node scripts/coolify-ensure-worker-groups.mjs staging
|
||||
|
||||
- name: Configure Coolify staging migration command
|
||||
# Migrations and the guarded seed run inside the web container entrypoint
|
||||
# (start.sh, gated by MIGRATE_ON_BOOT/SEED_ON_BOOT) because Coolify's
|
||||
# pre/post_deployment_command never fired for docker-image applications.
|
||||
# This step upserts the boot flags on the web resource and clears the
|
||||
# dead hook slots so they are not mistaken for an active mechanism.
|
||||
- name: Configure Coolify staging migration boot flags
|
||||
env:
|
||||
COOLIFY_STAGING_API_URL: ${{ vars.COOLIFY_STAGING_API_URL }}
|
||||
COOLIFY_STAGING_RESOURCE_UUIDS: ${{ vars.COOLIFY_STAGING_RESOURCE_UUIDS }}
|
||||
COOLIFY_STAGING_TOKEN: ${{ secrets.COOLIFY_STAGING_TOKEN }}
|
||||
MIGRATION_COMMAND: cd /myapp && pnpm exec prisma migrate deploy --config prisma.config.ts
|
||||
SEED_COMMAND: cd /myapp && SEED_DEMO_DATA=true ALLOW_STAGING_DEMO_SEED=true NODE_ENV=production node packages/db/dist/production-seed.server.js
|
||||
run: |
|
||||
if [ "$DEPLOY_ACTION" != "stage" ]; then
|
||||
echo "Coolify migration command configuration skipped for promote"
|
||||
echo "Coolify migration boot flag configuration skipped for promote"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -323,6 +346,7 @@ jobs:
|
||||
|
||||
WEB_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/web"
|
||||
API_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/api"
|
||||
MCP_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/mcp"
|
||||
WORKER_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/worker"
|
||||
APPS_JSON="$(mktemp)"
|
||||
trap 'rm -f "${APPS_JSON:-}"' EXIT
|
||||
@@ -331,6 +355,7 @@ jobs:
|
||||
"$SELECTED_COOLIFY_API_URL/applications" > "$APPS_JSON"
|
||||
WEB_RESOURCE_UUID="$(WEB_IMAGE="$WEB_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_STAGING_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const app = apps.find((candidate) => candidate.docker_registry_image_name === process.env.WEB_IMAGE && allowed.has(candidate.uuid)); process.stdout.write(app?.uuid || "");' "$APPS_JSON")"
|
||||
API_RESOURCE_UUID="$(API_IMAGE="$API_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_STAGING_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const app = apps.find((candidate) => candidate.docker_registry_image_name === process.env.API_IMAGE && allowed.has(candidate.uuid)); process.stdout.write(app?.uuid || "");' "$APPS_JSON")"
|
||||
MCP_RESOURCE_UUID="$(MCP_IMAGE="$MCP_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_STAGING_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const app = apps.find((candidate) => candidate.docker_registry_image_name === process.env.MCP_IMAGE && allowed.has(candidate.uuid)); process.stdout.write(app?.uuid || "");' "$APPS_JSON")"
|
||||
select_worker_resource() {
|
||||
WORKER_QUEUE_GROUP="$1" WORKER_IMAGE="$WORKER_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_STAGING_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const group = process.env.WORKER_QUEUE_GROUP || ""; const envPattern = new RegExp(`WORKER_QUEUE_GROUP\\s*(?:=|:)\\s*["'\''"]?${group}\\b`, "i"); const namePattern = new RegExp(`(?:^|[\\s/_-])worker[\\s_-]*${group}(?:$|[\\s:_-])`, "i"); const app = apps.find((candidate) => { const text = [candidate.name, candidate.description, candidate.docker_registry_image_name, candidate.fqdn, candidate.domain, candidate.domains, JSON.stringify(candidate.environment_variables || candidate.envs || candidate.environment || "")].filter(Boolean).join(" "); return candidate.docker_registry_image_name === process.env.WORKER_IMAGE && allowed.has(candidate.uuid) && (envPattern.test(text) || namePattern.test(text)); }); process.stdout.write(app?.uuid || "");' "$APPS_JSON"
|
||||
}
|
||||
@@ -340,19 +365,20 @@ jobs:
|
||||
WORKER_WEBHOOK_RESOURCE_UUID="$(select_worker_resource webhook)"
|
||||
WORKER_PLUGIN_RESOURCE_UUID="$(select_worker_resource plugin)"
|
||||
WORKER_RESOURCE_UUIDS="$WORKER_GENERAL_RESOURCE_UUID,$WORKER_MAIL_RESOURCE_UUID,$WORKER_IMPORT_RESOURCE_UUID,$WORKER_WEBHOOK_RESOURCE_UUID,$WORKER_PLUGIN_RESOURCE_UUID"
|
||||
if [ -z "$WEB_RESOURCE_UUID" ] || [ -z "$API_RESOURCE_UUID" ] || [ -z "$WORKER_GENERAL_RESOURCE_UUID" ] || [ -z "$WORKER_MAIL_RESOURCE_UUID" ] || [ -z "$WORKER_IMPORT_RESOURCE_UUID" ] || [ -z "$WORKER_WEBHOOK_RESOURCE_UUID" ] || [ -z "$WORKER_PLUGIN_RESOURCE_UUID" ]; then
|
||||
echo "Coolify staging web, api, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications must all exist"
|
||||
if [ -z "$WEB_RESOURCE_UUID" ] || [ -z "$API_RESOURCE_UUID" ] || [ -z "$MCP_RESOURCE_UUID" ] || [ -z "$WORKER_GENERAL_RESOURCE_UUID" ] || [ -z "$WORKER_MAIL_RESOURCE_UUID" ] || [ -z "$WORKER_IMPORT_RESOURCE_UUID" ] || [ -z "$WORKER_WEBHOOK_RESOURCE_UUID" ] || [ -z "$WORKER_PLUGIN_RESOURCE_UUID" ]; then
|
||||
echo "Coolify staging web, api, mcp, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications must all exist"
|
||||
exit 1
|
||||
fi
|
||||
for REQUIRED_RESOURCE_UUID in "$WEB_RESOURCE_UUID" "$API_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
for REQUIRED_RESOURCE_UUID in "$WEB_RESOURCE_UUID" "$API_RESOURCE_UUID" "$MCP_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
if ! printf ",%s," "$RESOLVED_COOLIFY_STAGING_RESOURCE_UUIDS" | grep -Fq ",$REQUIRED_RESOURCE_UUID,"; then
|
||||
echo "Coolify staging deploy resources must include the web, api, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications"
|
||||
echo "Coolify staging deploy resources must include the web, api, mcp, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
DEPENDENT_RESOURCE_UUIDS="$API_RESOURCE_UUID,$WORKER_RESOURCE_UUIDS"
|
||||
DEPENDENT_RESOURCE_UUIDS="$API_RESOURCE_UUID,$MCP_RESOURCE_UUID,$WORKER_RESOURCE_UUIDS"
|
||||
{
|
||||
echo "STAGING_WEB_RESOURCE_UUID=$WEB_RESOURCE_UUID"
|
||||
echo "STAGING_MCP_RESOURCE_UUID=$MCP_RESOURCE_UUID"
|
||||
echo "STAGING_WORKER_RESOURCE_UUIDS=$WORKER_RESOURCE_UUIDS"
|
||||
echo "STAGING_WORKER_GENERAL_RESOURCE_UUID=$WORKER_GENERAL_RESOURCE_UUID"
|
||||
echo "STAGING_WORKER_MAIL_RESOURCE_UUID=$WORKER_MAIL_RESOURCE_UUID"
|
||||
@@ -362,14 +388,19 @@ jobs:
|
||||
echo "STAGING_DEPENDENT_RESOURCE_UUIDS=$DEPENDENT_RESOURCE_UUIDS"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
MIGRATION_PAYLOAD="$(node -e 'process.stdout.write(JSON.stringify({ pre_deployment_command: process.env.MIGRATION_COMMAND, post_deployment_command: process.env.SEED_COMMAND, docker_registry_image_tag: process.env.IMAGE_TAG }))')"
|
||||
MIGRATION_PAYLOAD="$(node -e 'process.stdout.write(JSON.stringify({ pre_deployment_command: "", post_deployment_command: "", docker_registry_image_tag: process.env.IMAGE_TAG }))')"
|
||||
curl --fail --silent --show-error --request PATCH \
|
||||
--header "Authorization: Bearer $SELECTED_COOLIFY_TOKEN" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$MIGRATION_PAYLOAD" \
|
||||
"$SELECTED_COOLIFY_API_URL/applications/$WEB_RESOURCE_UUID" >/dev/null
|
||||
node scripts/coolify-deploy.mjs set-envs "$WEB_RESOURCE_UUID" \
|
||||
MIGRATE_ON_BOOT=true \
|
||||
SEED_ON_BOOT=true \
|
||||
SEED_DEMO_DATA=true \
|
||||
ALLOW_STAGING_DEMO_SEED=true
|
||||
IMAGE_TAG_PAYLOAD="$(node -e 'process.stdout.write(JSON.stringify({ docker_registry_image_tag: process.env.IMAGE_TAG }))')"
|
||||
for RESOURCE_UUID in "$API_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
for RESOURCE_UUID in "$API_RESOURCE_UUID" "$MCP_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
curl --fail --silent --show-error --request PATCH \
|
||||
--header "Authorization: Bearer $SELECTED_COOLIFY_TOKEN" \
|
||||
--header "Content-Type: application/json" \
|
||||
@@ -384,26 +415,11 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for TARGET in web api worker; do
|
||||
for TARGET in web api worker mcp; do
|
||||
IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/$TARGET"
|
||||
docker buildx imagetools create --tag "$IMAGE:staging" "$IMAGE:$IMAGE_TAG"
|
||||
done
|
||||
|
||||
- name: Pre-pull staging images on Coolify host
|
||||
env:
|
||||
COOLIFY_STAGING_SSH_HOST: ${{ vars.COOLIFY_STAGING_SSH_HOST }}
|
||||
COOLIFY_STAGING_SSH_USER: ${{ vars.COOLIFY_STAGING_SSH_USER }}
|
||||
COOLIFY_STAGING_SSH_KEY: ${{ secrets.COOLIFY_STAGING_SSH_KEY }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PAT: ${{ secrets.REGISTRY_PAT }}
|
||||
run: |
|
||||
if [ "$DEPLOY_ACTION" != "stage" ]; then
|
||||
echo "Staging image pre-pull skipped for promote"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
timeout 240s scripts/coolify-prepull-images.sh || echo "Staging image pre-pull skipped after timeout; Coolify will pull during deploy"
|
||||
|
||||
- name: Trigger Coolify staging deploy
|
||||
env:
|
||||
COOLIFY_STAGING_API_URL: ${{ vars.COOLIFY_STAGING_API_URL }}
|
||||
@@ -457,7 +473,7 @@ jobs:
|
||||
EXPECTED_IMAGE_ALIAS="${4:-staging}"
|
||||
EXPECTED_SERVICE="${5:-}"
|
||||
HEALTH_URL="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const raw = app.fqdn || app.domain || app.domains || ""; const value = Array.isArray(raw) ? raw[0] : String(raw).split(/[,\s]+/).find(Boolean); process.stdout.write(value ? value.replace(/\/+$/, "") : "");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api|mcp)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
if [ -z "$HEALTH_URL" ]; then
|
||||
if [ "$REQUIRE_HEALTH_URL" = "required" ] || [ "$HTTP_RESOURCE" = "true" ]; then
|
||||
echo "$LABEL is an HTTP resource but has no health URL"
|
||||
@@ -555,9 +571,6 @@ jobs:
|
||||
env:
|
||||
COOLIFY_STAGING_API_URL: ${{ vars.COOLIFY_STAGING_API_URL }}
|
||||
COOLIFY_STAGING_TOKEN: ${{ secrets.COOLIFY_STAGING_TOKEN }}
|
||||
COOLIFY_STAGING_SSH_HOST: ${{ vars.COOLIFY_STAGING_SSH_HOST }}
|
||||
COOLIFY_STAGING_SSH_USER: ${{ vars.COOLIFY_STAGING_SSH_USER }}
|
||||
COOLIFY_STAGING_SSH_KEY: ${{ secrets.COOLIFY_STAGING_SSH_KEY }}
|
||||
run: |
|
||||
if [ "$DEPLOY_ACTION" != "stage" ]; then
|
||||
echo "Staging dependent health wait skipped for promote"
|
||||
@@ -580,35 +593,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# SSH helper for worker health verification (Coolify has no exec REST API);
|
||||
# used by verify_live_revision to run the health probe via docker exec.
|
||||
if [ -z "$COOLIFY_STAGING_SSH_HOST" ] || [ -z "$COOLIFY_STAGING_SSH_KEY" ]; then
|
||||
echo "Coolify staging SSH configuration is incomplete (worker health verification requires SSH + docker exec)"
|
||||
exit 1
|
||||
fi
|
||||
REMOTE_SSH_KEY_FILE="$(mktemp)"
|
||||
REMOTE_SSH_KNOWN_HOSTS="$(mktemp)"
|
||||
trap 'rm -f "$REMOTE_SSH_KEY_FILE" "$REMOTE_SSH_KNOWN_HOSTS"' EXIT
|
||||
# Materialise the key via an intermediate var so the secret is written to
|
||||
# a file (never stdout/logs); avoids leaking it in CI output.
|
||||
REMOTE_SSH_KEY_DATA="$COOLIFY_STAGING_SSH_KEY"
|
||||
printf '%s\n' "$REMOTE_SSH_KEY_DATA" > "$REMOTE_SSH_KEY_FILE"
|
||||
unset REMOTE_SSH_KEY_DATA
|
||||
chmod 600 "$REMOTE_SSH_KEY_FILE"
|
||||
REMOTE_SSH_USER="${COOLIFY_STAGING_SSH_USER:-root}"
|
||||
REMOTE_SSH_HOST="$COOLIFY_STAGING_SSH_HOST"
|
||||
if command -v ssh-keyscan >/dev/null 2>&1; then
|
||||
ssh-keyscan -H "$REMOTE_SSH_HOST" > "$REMOTE_SSH_KNOWN_HOSTS" 2>/dev/null || true
|
||||
fi
|
||||
if [ -s "$REMOTE_SSH_KNOWN_HOSTS" ]; then
|
||||
REMOTE_SSH_HOSTKEY_OPT="-o StrictHostKeyChecking=yes"
|
||||
else
|
||||
REMOTE_SSH_HOSTKEY_OPT="-o StrictHostKeyChecking=accept-new"
|
||||
fi
|
||||
remote_ssh() {
|
||||
ssh -i "$REMOTE_SSH_KEY_FILE" -o BatchMode=yes -o ConnectTimeout=10 -o UserKnownHostsFile="$REMOTE_SSH_KNOWN_HOSTS" $REMOTE_SSH_HOSTKEY_OPT "${REMOTE_SSH_USER}@${REMOTE_SSH_HOST}" "$@"
|
||||
}
|
||||
|
||||
DEPLOYMENT_INDEX=0
|
||||
DEPLOYMENT_WAIT_PIDS=""
|
||||
for DEPLOYMENT_UUID in $(printf "%s" "$STAGING_DEPENDENT_DEPLOYMENT_UUIDS" | tr ',' ' '); do
|
||||
@@ -636,7 +620,7 @@ jobs:
|
||||
RESOURCE_KIND="${6:-auto}"
|
||||
EXPECTED_WORKER_GROUP="${7:-}"
|
||||
HEALTH_URL="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const raw = app.fqdn || app.domain || app.domains || ""; const value = Array.isArray(raw) ? raw[0] : String(raw).split(/[,\s]+/).find(Boolean); process.stdout.write(value ? value.replace(/\/+$/, "") : "");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api|mcp)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
WORKER_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])worker(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
if [ "$RESOURCE_KIND" = "worker" ] || [ "$WORKER_RESOURCE" = "true" ]; then
|
||||
CONFIGURED_IMAGE_TAG="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(app.docker_registry_image_tag || app.image_tag || app.tag || "");' "$APP_JSON")"
|
||||
@@ -644,74 +628,7 @@ jobs:
|
||||
echo "$LABEL worker image tag does not match expected alias"
|
||||
exit 1
|
||||
fi
|
||||
# Coolify 4.x exposes no command-execution REST API, so verify worker
|
||||
# health by running the one-shot health probe inside the live container
|
||||
# over SSH (docker exec) on the Coolify host. Fail-closed: any SSH /
|
||||
# container-resolution / exec failure blocks the deploy.
|
||||
WORKER_CONTAINER="$(remote_ssh "docker ps --filter name=$RESOURCE_UUID --filter status=running --format '{{.Names}}' | head -n1" 2>/dev/null | tr -d '\r')"
|
||||
if [ -z "$WORKER_CONTAINER" ]; then
|
||||
echo "$LABEL worker container not found on Coolify host"
|
||||
exit 1
|
||||
fi
|
||||
# health.js exits non-zero when the worker reports a non-ready
|
||||
# status (e.g. "misconfigured"), yet it still prints the diagnostic
|
||||
# JSON (status + errors) to stdout. Capture stdout regardless of the
|
||||
# exit code (|| true) so the real reason surfaces in the checks below
|
||||
# instead of a generic "command unavailable". Empty output means the
|
||||
# docker exec itself could not run (container gone, SSH failure, ...).
|
||||
WORKER_HEALTH_OUTPUT="$(remote_ssh "docker exec $WORKER_CONTAINER node apps/worker/dist/health.js" 2>/dev/null | tr -d '\r' || true)"
|
||||
if [ -z "$WORKER_HEALTH_OUTPUT" ]; then
|
||||
echo "$LABEL worker docker exec health command unavailable (no output from health probe)"
|
||||
exit 1
|
||||
fi
|
||||
WORKER_HEALTH_JSON="$(mktemp)"
|
||||
printf "%s" "$WORKER_HEALTH_OUTPUT" > "$WORKER_HEALTH_JSON"
|
||||
if ! WORKER_REVISION="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.revision || "");' "$WORKER_HEALTH_JSON" 2>/dev/null)"; then
|
||||
rm -f "$WORKER_HEALTH_JSON"
|
||||
echo "$LABEL worker execute output is not JSON"
|
||||
exit 1
|
||||
fi
|
||||
WORKER_STATUS="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.status || "");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_ENABLED="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.workerEnabled === true ? "true" : "false");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_GROUP="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.workerGroup || "");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_QUEUES="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(Array.isArray(payload.queues) ? payload.queues.join(",") : "");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_HEALTH_ERRORS="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(Array.isArray(payload.errors) ? payload.errors.join("; ") : "");' "$WORKER_HEALTH_JSON")"
|
||||
rm -f "$WORKER_HEALTH_JSON"
|
||||
if [ "$WORKER_REVISION" != "$IMAGE_TAG" ]; then
|
||||
echo "$LABEL worker health revision does not match image tag yet"
|
||||
return 1
|
||||
fi
|
||||
if [ "$WORKER_ENABLED" != "true" ]; then
|
||||
echo "$LABEL worker health reports workerEnabled=false"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$WORKER_STATUS" != "ready" ]; then
|
||||
echo "$LABEL worker health status is not ready: '$WORKER_STATUS'${WORKER_HEALTH_ERRORS:+ (errors: $WORKER_HEALTH_ERRORS)}"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$EXPECTED_WORKER_GROUP" ] && [ "$WORKER_GROUP" != "$EXPECTED_WORKER_GROUP" ]; then
|
||||
echo "$LABEL worker group does not match expected queue group"
|
||||
exit 1
|
||||
fi
|
||||
EXPECTED_WORKER_QUEUES=""
|
||||
case "$EXPECTED_WORKER_GROUP" in
|
||||
general) EXPECTED_WORKER_QUEUES="toolctrl.notifications toolctrl.maintenance toolctrl.file-scan" ;;
|
||||
mail) EXPECTED_WORKER_QUEUES="toolctrl.email" ;;
|
||||
import)
|
||||
if [ -n "$WORKER_QUEUES" ]; then
|
||||
EXPECTED_WORKER_QUEUES="toolctrl.imports toolctrl.exports"
|
||||
fi
|
||||
;;
|
||||
webhook) EXPECTED_WORKER_QUEUES="toolctrl.ai-chatbot toolctrl.browser-automation" ;;
|
||||
plugin) EXPECTED_WORKER_QUEUES="toolctrl.plugin-invoke" ;;
|
||||
esac
|
||||
for EXPECTED_WORKER_QUEUE in $EXPECTED_WORKER_QUEUES; do
|
||||
if ! printf ",%s," "$WORKER_QUEUES" | grep -Fq ",$EXPECTED_WORKER_QUEUE,"; then
|
||||
echo "$LABEL worker queues do not match expected queue group"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "$LABEL worker live revision matches image tag"
|
||||
echo "$LABEL worker Coolify status is healthy and image tag matches expected alias"
|
||||
return
|
||||
fi
|
||||
if [ -z "$HEALTH_URL" ]; then
|
||||
@@ -761,6 +678,10 @@ jobs:
|
||||
RESOURCE_KIND="api"
|
||||
EXPECTED_SERVICE="api"
|
||||
EXPECTED_WORKER_GROUP=""
|
||||
if [ "$RESOURCE_UUID" = "$STAGING_MCP_RESOURCE_UUID" ]; then
|
||||
RESOURCE_KIND="mcp"
|
||||
EXPECTED_SERVICE="mcp"
|
||||
fi
|
||||
if printf ",%s," "$STAGING_WORKER_RESOURCE_UUIDS" | grep -Fq ",$RESOURCE_UUID,"; then
|
||||
RESOURCE_KIND="worker"
|
||||
EXPECTED_SERVICE=""
|
||||
@@ -829,16 +750,17 @@ jobs:
|
||||
|
||||
node scripts/coolify-ensure-worker-groups.mjs production
|
||||
|
||||
- name: Configure Coolify production migration command
|
||||
# Same entrypoint-based migration mechanism as staging: boot flags on the
|
||||
# web resource, dead Coolify hook slots cleared. Production runs the
|
||||
# ungated seed (no demo flags) inside the new container after migrate.
|
||||
- name: Configure Coolify production migration boot flags
|
||||
env:
|
||||
COOLIFY_API_URL: ${{ vars.COOLIFY_API_URL }}
|
||||
COOLIFY_RESOURCE_UUIDS: ${{ vars.COOLIFY_RESOURCE_UUIDS }}
|
||||
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
|
||||
MIGRATION_COMMAND: cd /myapp && pnpm exec prisma migrate deploy --config prisma.config.ts
|
||||
SEED_COMMAND: cd /myapp && NODE_ENV=production node packages/db/dist/production-seed.server.js
|
||||
run: |
|
||||
if [ "$DEPLOY_ACTION" != "promote" ]; then
|
||||
echo "Coolify production migration command configuration skipped for staging"
|
||||
echo "Coolify production migration boot flag configuration skipped for staging"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -850,6 +772,7 @@ jobs:
|
||||
|
||||
WEB_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/web"
|
||||
API_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/api"
|
||||
MCP_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/mcp"
|
||||
WORKER_IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/worker"
|
||||
APPS_JSON="$(mktemp)"
|
||||
trap 'rm -f "${APPS_JSON:-}"' EXIT
|
||||
@@ -858,6 +781,7 @@ jobs:
|
||||
"$COOLIFY_API_URL/applications" > "$APPS_JSON"
|
||||
WEB_RESOURCE_UUID="$(WEB_IMAGE="$WEB_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const app = apps.find((candidate) => candidate.docker_registry_image_name === process.env.WEB_IMAGE && allowed.has(candidate.uuid)); process.stdout.write(app?.uuid || "");' "$APPS_JSON")"
|
||||
API_RESOURCE_UUID="$(API_IMAGE="$API_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const app = apps.find((candidate) => candidate.docker_registry_image_name === process.env.API_IMAGE && allowed.has(candidate.uuid)); process.stdout.write(app?.uuid || "");' "$APPS_JSON")"
|
||||
MCP_RESOURCE_UUID="$(MCP_IMAGE="$MCP_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const app = apps.find((candidate) => candidate.docker_registry_image_name === process.env.MCP_IMAGE && allowed.has(candidate.uuid)); process.stdout.write(app?.uuid || "");' "$APPS_JSON")"
|
||||
select_worker_resource() {
|
||||
WORKER_QUEUE_GROUP="$1" WORKER_IMAGE="$WORKER_IMAGE" ALLOWED_RESOURCE_UUIDS="$RESOLVED_COOLIFY_RESOURCE_UUIDS" node -e 'const fs = require("node:fs"); const apps = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const allowed = new Set((process.env.ALLOWED_RESOURCE_UUIDS || "").split(",").filter(Boolean)); const group = process.env.WORKER_QUEUE_GROUP || ""; const envPattern = new RegExp(`WORKER_QUEUE_GROUP\\s*(?:=|:)\\s*["'\''"]?${group}\\b`, "i"); const namePattern = new RegExp(`(?:^|[\\s/_-])worker[\\s_-]*${group}(?:$|[\\s:_-])`, "i"); const app = apps.find((candidate) => { const text = [candidate.name, candidate.description, candidate.docker_registry_image_name, candidate.fqdn, candidate.domain, candidate.domains, JSON.stringify(candidate.environment_variables || candidate.envs || candidate.environment || "")].filter(Boolean).join(" "); return candidate.docker_registry_image_name === process.env.WORKER_IMAGE && allowed.has(candidate.uuid) && (envPattern.test(text) || namePattern.test(text)); }); process.stdout.write(app?.uuid || "");' "$APPS_JSON"
|
||||
}
|
||||
@@ -867,19 +791,20 @@ jobs:
|
||||
WORKER_WEBHOOK_RESOURCE_UUID="$(select_worker_resource webhook)"
|
||||
WORKER_PLUGIN_RESOURCE_UUID="$(select_worker_resource plugin)"
|
||||
WORKER_RESOURCE_UUIDS="$WORKER_GENERAL_RESOURCE_UUID,$WORKER_MAIL_RESOURCE_UUID,$WORKER_IMPORT_RESOURCE_UUID,$WORKER_WEBHOOK_RESOURCE_UUID,$WORKER_PLUGIN_RESOURCE_UUID"
|
||||
if [ -z "$WEB_RESOURCE_UUID" ] || [ -z "$API_RESOURCE_UUID" ] || [ -z "$WORKER_GENERAL_RESOURCE_UUID" ] || [ -z "$WORKER_MAIL_RESOURCE_UUID" ] || [ -z "$WORKER_IMPORT_RESOURCE_UUID" ] || [ -z "$WORKER_WEBHOOK_RESOURCE_UUID" ] || [ -z "$WORKER_PLUGIN_RESOURCE_UUID" ]; then
|
||||
echo "Coolify production web, api, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications must all exist"
|
||||
if [ -z "$WEB_RESOURCE_UUID" ] || [ -z "$API_RESOURCE_UUID" ] || [ -z "$MCP_RESOURCE_UUID" ] || [ -z "$WORKER_GENERAL_RESOURCE_UUID" ] || [ -z "$WORKER_MAIL_RESOURCE_UUID" ] || [ -z "$WORKER_IMPORT_RESOURCE_UUID" ] || [ -z "$WORKER_WEBHOOK_RESOURCE_UUID" ] || [ -z "$WORKER_PLUGIN_RESOURCE_UUID" ]; then
|
||||
echo "Coolify production web, api, mcp, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications must all exist"
|
||||
exit 1
|
||||
fi
|
||||
for REQUIRED_RESOURCE_UUID in "$WEB_RESOURCE_UUID" "$API_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
for REQUIRED_RESOURCE_UUID in "$WEB_RESOURCE_UUID" "$API_RESOURCE_UUID" "$MCP_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
if ! printf ",%s," "$RESOLVED_COOLIFY_RESOURCE_UUIDS" | grep -Fq ",$REQUIRED_RESOURCE_UUID,"; then
|
||||
echo "Coolify production deploy resources must include the web, api, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications"
|
||||
echo "Coolify production deploy resources must include the web, api, mcp, worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin applications"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
DEPENDENT_RESOURCE_UUIDS="$API_RESOURCE_UUID,$WORKER_RESOURCE_UUIDS"
|
||||
DEPENDENT_RESOURCE_UUIDS="$API_RESOURCE_UUID,$MCP_RESOURCE_UUID,$WORKER_RESOURCE_UUIDS"
|
||||
{
|
||||
echo "PRODUCTION_WEB_RESOURCE_UUID=$WEB_RESOURCE_UUID"
|
||||
echo "PRODUCTION_MCP_RESOURCE_UUID=$MCP_RESOURCE_UUID"
|
||||
echo "PRODUCTION_WORKER_RESOURCE_UUIDS=$WORKER_RESOURCE_UUIDS"
|
||||
echo "PRODUCTION_WORKER_GENERAL_RESOURCE_UUID=$WORKER_GENERAL_RESOURCE_UUID"
|
||||
echo "PRODUCTION_WORKER_MAIL_RESOURCE_UUID=$WORKER_MAIL_RESOURCE_UUID"
|
||||
@@ -889,14 +814,17 @@ jobs:
|
||||
echo "PRODUCTION_DEPENDENT_RESOURCE_UUIDS=$DEPENDENT_RESOURCE_UUIDS"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
MIGRATION_PAYLOAD="$(node -e 'process.stdout.write(JSON.stringify({ pre_deployment_command: process.env.MIGRATION_COMMAND, post_deployment_command: process.env.SEED_COMMAND, docker_registry_image_tag: process.env.IMAGE_TAG }))')"
|
||||
MIGRATION_PAYLOAD="$(node -e 'process.stdout.write(JSON.stringify({ pre_deployment_command: "", post_deployment_command: "", docker_registry_image_tag: process.env.IMAGE_TAG }))')"
|
||||
curl --fail --silent --show-error --request PATCH \
|
||||
--header "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$MIGRATION_PAYLOAD" \
|
||||
"$COOLIFY_API_URL/applications/$WEB_RESOURCE_UUID" >/dev/null
|
||||
node scripts/coolify-deploy.mjs set-envs "$WEB_RESOURCE_UUID" \
|
||||
MIGRATE_ON_BOOT=true \
|
||||
SEED_ON_BOOT=true
|
||||
IMAGE_TAG_PAYLOAD="$(node -e 'process.stdout.write(JSON.stringify({ docker_registry_image_tag: process.env.IMAGE_TAG }))')"
|
||||
for RESOURCE_UUID in "$API_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
for RESOURCE_UUID in "$API_RESOURCE_UUID" "$MCP_RESOURCE_UUID" "$WORKER_GENERAL_RESOURCE_UUID" "$WORKER_MAIL_RESOURCE_UUID" "$WORKER_IMPORT_RESOURCE_UUID" "$WORKER_WEBHOOK_RESOURCE_UUID" "$WORKER_PLUGIN_RESOURCE_UUID"; do
|
||||
curl --fail --silent --show-error --request PATCH \
|
||||
--header "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
--header "Content-Type: application/json" \
|
||||
@@ -911,7 +839,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for TARGET in web api worker; do
|
||||
for TARGET in web api worker mcp; do
|
||||
IMAGE="$REGISTRY/$IMAGE_NAMESPACE/$IMAGE_PREFIX/$TARGET"
|
||||
docker buildx imagetools create --tag "$IMAGE:main" "$IMAGE:$IMAGE_TAG"
|
||||
done
|
||||
@@ -968,7 +896,7 @@ jobs:
|
||||
EXPECTED_IMAGE_ALIAS="${4:-main}"
|
||||
EXPECTED_SERVICE="${5:-}"
|
||||
HEALTH_URL="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const raw = app.fqdn || app.domain || app.domains || ""; const value = Array.isArray(raw) ? raw[0] : String(raw).split(/[,\s]+/).find(Boolean); process.stdout.write(value ? value.replace(/\/+$/, "") : "");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api|mcp)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
if [ -z "$HEALTH_URL" ]; then
|
||||
if [ "$REQUIRE_HEALTH_URL" = "required" ] || [ "$HTTP_RESOURCE" = "true" ]; then
|
||||
echo "$LABEL is an HTTP resource but has no health URL"
|
||||
@@ -1118,12 +1046,6 @@ jobs:
|
||||
env:
|
||||
COOLIFY_API_URL: ${{ vars.COOLIFY_API_URL }}
|
||||
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
|
||||
# AD-24: Prod ist Multi-Server. Worker laufen auf der worker-VM (privates Netz), nur ueber
|
||||
# die control-Bastion erreichbar -> Worker-Health-Verifikation (docker exec) per SSH-ProxyJump.
|
||||
COOLIFY_PROD_SSH_KEY: ${{ secrets.COOLIFY_PROD_SSH_KEY }}
|
||||
COOLIFY_PROD_SSH_KNOWN_HOSTS: ${{ secrets.COOLIFY_PROD_SSH_KNOWN_HOSTS }}
|
||||
COOLIFY_PROD_SSH_HOST: ${{ vars.COOLIFY_PROD_SSH_HOST }}
|
||||
COOLIFY_PROD_WORKER_PRIVATE_IP: ${{ vars.COOLIFY_PROD_WORKER_PRIVATE_IP }}
|
||||
run: |
|
||||
if [ "$DEPLOY_ACTION" != "promote" ]; then
|
||||
echo "Production dependent health wait skipped for staging"
|
||||
@@ -1164,32 +1086,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# SSH helper for worker health verification (Coolify has no exec REST API). AD-24: Prod ist
|
||||
# Multi-Server -> Worker laufen auf der worker-VM (privates Netz), erreichbar nur ueber die
|
||||
# control-Bastion. ProxyJump + gepinnte Host-Keys (kein StrictHostKeyChecking=no). Secrets
|
||||
# werden via Zwischen-Variable in Dateien geschrieben (nie stdout/Logs).
|
||||
if [ -z "$COOLIFY_PROD_SSH_HOST" ] || [ -z "$COOLIFY_PROD_SSH_KEY" ] || [ -z "$COOLIFY_PROD_SSH_KNOWN_HOSTS" ] || [ -z "$COOLIFY_PROD_WORKER_PRIVATE_IP" ]; then
|
||||
echo "Coolify production SSH configuration is incomplete (worker health verification requires SSH + docker exec)"
|
||||
exit 1
|
||||
fi
|
||||
REMOTE_SSH_KEY_FILE="$(mktemp)"
|
||||
REMOTE_SSH_KNOWN_HOSTS="$(mktemp)"
|
||||
trap 'rm -f "$REMOTE_SSH_KEY_FILE" "$REMOTE_SSH_KNOWN_HOSTS"' EXIT
|
||||
REMOTE_SSH_KEY_DATA="$COOLIFY_PROD_SSH_KEY"
|
||||
printf '%s\n' "$REMOTE_SSH_KEY_DATA" > "$REMOTE_SSH_KEY_FILE"
|
||||
unset REMOTE_SSH_KEY_DATA
|
||||
chmod 600 "$REMOTE_SSH_KEY_FILE"
|
||||
REMOTE_SSH_KNOWN_HOSTS_DATA="$COOLIFY_PROD_SSH_KNOWN_HOSTS"
|
||||
printf '%s\n' "$REMOTE_SSH_KNOWN_HOSTS_DATA" > "$REMOTE_SSH_KNOWN_HOSTS"
|
||||
unset REMOTE_SSH_KNOWN_HOSTS_DATA
|
||||
# ProxyCommand (nicht ProxyJump): ProxyJump reicht den -i-Key nicht zuverlaessig an den
|
||||
# Jump-Hop weiter (MaxAuthTries -> Permission denied). ProxyCommand gibt der Bastion den Key
|
||||
# explizit (IdentitiesOnly), Host-Keys fuer beide Hops gepinnt.
|
||||
REMOTE_SSH_PROXY="ssh -i $REMOTE_SSH_KEY_FILE -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=20 -o UserKnownHostsFile=$REMOTE_SSH_KNOWN_HOSTS -o StrictHostKeyChecking=yes -W %h:%p root@${COOLIFY_PROD_SSH_HOST}"
|
||||
remote_ssh() {
|
||||
ssh -i "$REMOTE_SSH_KEY_FILE" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=20 -o UserKnownHostsFile="$REMOTE_SSH_KNOWN_HOSTS" -o StrictHostKeyChecking=yes -o ProxyCommand="$REMOTE_SSH_PROXY" "root@${COOLIFY_PROD_WORKER_PRIVATE_IP}" "$@"
|
||||
}
|
||||
|
||||
verify_live_revision() {
|
||||
APP_JSON="$1"
|
||||
LABEL="$2"
|
||||
@@ -1199,7 +1095,7 @@ jobs:
|
||||
RESOURCE_KIND="${6:-auto}"
|
||||
EXPECTED_WORKER_GROUP="${7:-}"
|
||||
HEALTH_URL="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const raw = app.fqdn || app.domain || app.domains || ""; const value = Array.isArray(raw) ? raw[0] : String(raw).split(/[,\s]+/).find(Boolean); process.stdout.write(value ? value.replace(/\/+$/, "") : "");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
HTTP_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])(?:web|api|mcp)(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
WORKER_RESOURCE="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = [app.docker_registry_image_name, app.name, app.description].filter(Boolean).join(" "); process.stdout.write(/(?:^|[\\/\\s_-])worker(?:$|[\\s:_-])/i.test(value) ? "true" : "false");' "$APP_JSON")"
|
||||
if [ "$RESOURCE_KIND" = "worker" ] || [ "$WORKER_RESOURCE" = "true" ]; then
|
||||
CONFIGURED_IMAGE_TAG="$(node -e 'const fs = require("node:fs"); const app = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(app.docker_registry_image_tag || app.image_tag || app.tag || "");' "$APP_JSON")"
|
||||
@@ -1207,74 +1103,7 @@ jobs:
|
||||
echo "$LABEL worker image tag does not match expected alias"
|
||||
exit 1
|
||||
fi
|
||||
# Coolify 4.x exposes no command-execution REST API, so verify worker
|
||||
# health by running the one-shot health probe inside the live container
|
||||
# over SSH (docker exec) on the Coolify host. Fail-closed: any SSH /
|
||||
# container-resolution / exec failure blocks the deploy.
|
||||
WORKER_CONTAINER="$(remote_ssh "docker ps --filter name=$RESOURCE_UUID --filter status=running --format '{{.Names}}' | head -n1" 2>/dev/null | tr -d '\r')"
|
||||
if [ -z "$WORKER_CONTAINER" ]; then
|
||||
echo "$LABEL worker container not found on Coolify host"
|
||||
exit 1
|
||||
fi
|
||||
# health.js exits non-zero when the worker reports a non-ready
|
||||
# status (e.g. "misconfigured"), yet it still prints the diagnostic
|
||||
# JSON (status + errors) to stdout. Capture stdout regardless of the
|
||||
# exit code (|| true) so the real reason surfaces in the checks below
|
||||
# instead of a generic "command unavailable". Empty output means the
|
||||
# docker exec itself could not run (container gone, SSH failure, ...).
|
||||
WORKER_HEALTH_OUTPUT="$(remote_ssh "docker exec $WORKER_CONTAINER node apps/worker/dist/health.js" 2>/dev/null | tr -d '\r' || true)"
|
||||
if [ -z "$WORKER_HEALTH_OUTPUT" ]; then
|
||||
echo "$LABEL worker docker exec health command unavailable (no output from health probe)"
|
||||
exit 1
|
||||
fi
|
||||
WORKER_HEALTH_JSON="$(mktemp)"
|
||||
printf "%s" "$WORKER_HEALTH_OUTPUT" > "$WORKER_HEALTH_JSON"
|
||||
if ! WORKER_REVISION="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.revision || "");' "$WORKER_HEALTH_JSON" 2>/dev/null)"; then
|
||||
rm -f "$WORKER_HEALTH_JSON"
|
||||
echo "$LABEL worker execute output is not JSON"
|
||||
exit 1
|
||||
fi
|
||||
WORKER_STATUS="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.status || "");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_ENABLED="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.workerEnabled === true ? "true" : "false");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_GROUP="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(payload.workerGroup || "");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_QUEUES="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(Array.isArray(payload.queues) ? payload.queues.join(",") : "");' "$WORKER_HEALTH_JSON")"
|
||||
WORKER_HEALTH_ERRORS="$(node -e 'const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(Array.isArray(payload.errors) ? payload.errors.join("; ") : "");' "$WORKER_HEALTH_JSON")"
|
||||
rm -f "$WORKER_HEALTH_JSON"
|
||||
if [ "$WORKER_REVISION" != "$IMAGE_TAG" ]; then
|
||||
echo "$LABEL worker health revision does not match image tag yet"
|
||||
return 1
|
||||
fi
|
||||
if [ "$WORKER_ENABLED" != "true" ]; then
|
||||
echo "$LABEL worker health reports workerEnabled=false"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$WORKER_STATUS" != "ready" ]; then
|
||||
echo "$LABEL worker health status is not ready: '$WORKER_STATUS'${WORKER_HEALTH_ERRORS:+ (errors: $WORKER_HEALTH_ERRORS)}"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$EXPECTED_WORKER_GROUP" ] && [ "$WORKER_GROUP" != "$EXPECTED_WORKER_GROUP" ]; then
|
||||
echo "$LABEL worker group does not match expected queue group"
|
||||
exit 1
|
||||
fi
|
||||
EXPECTED_WORKER_QUEUES=""
|
||||
case "$EXPECTED_WORKER_GROUP" in
|
||||
general) EXPECTED_WORKER_QUEUES="toolctrl.notifications toolctrl.maintenance toolctrl.file-scan" ;;
|
||||
mail) EXPECTED_WORKER_QUEUES="toolctrl.email" ;;
|
||||
import)
|
||||
if [ -n "$WORKER_QUEUES" ]; then
|
||||
EXPECTED_WORKER_QUEUES="toolctrl.imports toolctrl.exports"
|
||||
fi
|
||||
;;
|
||||
webhook) EXPECTED_WORKER_QUEUES="toolctrl.ai-chatbot toolctrl.browser-automation" ;;
|
||||
plugin) EXPECTED_WORKER_QUEUES="toolctrl.plugin-invoke" ;;
|
||||
esac
|
||||
for EXPECTED_WORKER_QUEUE in $EXPECTED_WORKER_QUEUES; do
|
||||
if ! printf ",%s," "$WORKER_QUEUES" | grep -Fq ",$EXPECTED_WORKER_QUEUE,"; then
|
||||
echo "$LABEL worker queues do not match expected queue group"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "$LABEL worker live revision matches image tag"
|
||||
echo "$LABEL worker Coolify status is healthy and image tag matches expected alias"
|
||||
return
|
||||
fi
|
||||
if [ -z "$HEALTH_URL" ]; then
|
||||
@@ -1324,6 +1153,10 @@ jobs:
|
||||
RESOURCE_KIND="api"
|
||||
EXPECTED_SERVICE="api"
|
||||
EXPECTED_WORKER_GROUP=""
|
||||
if [ "$RESOURCE_UUID" = "$PRODUCTION_MCP_RESOURCE_UUID" ]; then
|
||||
RESOURCE_KIND="mcp"
|
||||
EXPECTED_SERVICE="mcp"
|
||||
fi
|
||||
if printf ",%s," "$PRODUCTION_WORKER_RESOURCE_UUIDS" | grep -Fq ",$RESOURCE_UUID,"; then
|
||||
RESOURCE_KIND="worker"
|
||||
EXPECTED_SERVICE=""
|
||||
@@ -1390,7 +1223,7 @@ jobs:
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
RELEASE_SHA="${SHA}"
|
||||
fi
|
||||
for PROJECT in boilerplate-api boilerplate-worker boilerplate-web; do
|
||||
for PROJECT in api worker web; do
|
||||
bash scripts/ci/sentry-release.sh \
|
||||
--project "$PROJECT" \
|
||||
--environment "$SENTRY_ENVIRONMENT" \
|
||||
|
||||
@@ -52,8 +52,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
E2E_BASE_URL: https://boiler.saas-ctrl.com
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
E2E_BASE_URL: https://app.hivegrid.de
|
||||
E2E_A11Y_STRICT: "1"
|
||||
E2E_BROWSER_MATRIX: "1"
|
||||
E2E_ALLOW_WORKFLOW_RUN: "1"
|
||||
@@ -113,12 +113,12 @@ jobs:
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
|
||||
- name: Upload Playwright traces
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: e2e-readiness-results
|
||||
path: test-results/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Lighthouse CI
|
||||
|
||||
# Audits the deployed staging site (https://boiler.saas-ctrl.com). A
|
||||
# Audits the deployed staging site (https://app.hivegrid.de). A
|
||||
# meaningful Lighthouse audit needs a live server, a database and seeded
|
||||
# content; reproducing that whole stack inside a CI job is brittle, so the
|
||||
# audit targets the deployed environment instead.
|
||||
@@ -9,7 +9,6 @@ name: Lighthouse CI
|
||||
# gate — staging always lags the commit under review. It runs nightly and on
|
||||
# manual dispatch.
|
||||
#
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/lighthouse.md
|
||||
# Docs: docs/architecture/lighthouse.md
|
||||
|
||||
on:
|
||||
@@ -19,7 +18,7 @@ on:
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
jobs:
|
||||
lighthouse:
|
||||
@@ -29,12 +28,12 @@ jobs:
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
@@ -43,12 +42,12 @@ jobs:
|
||||
- name: Mint Lighthouse session cookie
|
||||
run: node scripts/rotate-lighthouse-session-cookie.mjs
|
||||
env:
|
||||
LIGHTHOUSE_LOGIN_URL: https://boiler.saas-ctrl.com/login
|
||||
LIGHTHOUSE_LOGIN_URL: https://app.hivegrid.de/login
|
||||
LIGHTHOUSE_LOGIN_EMAIL: ${{ secrets.LIGHTHOUSE_LOGIN_EMAIL }}
|
||||
LIGHTHOUSE_LOGIN_PASSWORD: ${{ secrets.LIGHTHOUSE_LOGIN_PASSWORD }}
|
||||
LIGHTHOUSE_SESSION_COOKIE: ${{ secrets.LIGHTHOUSE_SESSION_COOKIE }}
|
||||
LIGHTHOUSE_SESSION_COOKIE_NAME: RJ_session
|
||||
LIGHTHOUSE_COOKIE_DOMAIN: boiler.saas-ctrl.com
|
||||
LIGHTHOUSE_SESSION_COOKIE_NAME: hivegrid_session
|
||||
LIGHTHOUSE_COOKIE_DOMAIN: app.hivegrid.de
|
||||
|
||||
- name: Install Chromium
|
||||
# The Gitea runner image (node:20-bookworm) ships no browser; LHCI's
|
||||
@@ -66,7 +65,7 @@ jobs:
|
||||
|
||||
- name: Upload Lighthouse artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lighthouse-ci-results
|
||||
path: .lighthouseci/
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
name: SDK Codegen Drift
|
||||
|
||||
# Gitea variant of .github/workflows/sdk-codegen.yml. Re-runs the
|
||||
# @toolctrl/api-client-ts generation pipeline on PRs that touch the
|
||||
# @hivegrid/api-client-ts generation pipeline on PRs that touch the
|
||||
# upstream OpenAPI surface or the SDK package, then verifies the
|
||||
# committed src/generated/ tree is in sync with openapi.json.
|
||||
#
|
||||
# Spec: docs/improvement-plan/08-mobile-cli-storybook-sdk/ts-sdk.md
|
||||
# Docs: docs/sdks/typescript.md
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -17,7 +17,7 @@ on:
|
||||
- ".gitea/workflows/sdk-codegen.yml"
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
HIVEGRID_GITEA_HOST_IP: ${{ vars.HIVEGRID_GITEA_HOST_IP }}
|
||||
|
||||
jobs:
|
||||
drift-check:
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
if [ -n "${HIVEGRID_GITEA_HOST_IP:-}" ]; then
|
||||
if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "HIVEGRID_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
printf '%s git.tool-ctrl.de\n' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout PR head
|
||||
@@ -44,14 +44,25 @@ jobs:
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Regenerate OpenAPI capture
|
||||
run: pnpm --filter @toolctrl/api-client-ts generate-spec
|
||||
run: pnpm --filter @hivegrid/api-client-ts generate-spec
|
||||
|
||||
- name: Regenerate typed client (Hey-API)
|
||||
run: pnpm --filter @toolctrl/api-client-ts generate
|
||||
run: pnpm --filter @hivegrid/api-client-ts generate
|
||||
|
||||
- name: Fail if generated code drifts from committed state
|
||||
run: |
|
||||
@@ -61,7 +72,7 @@ jobs:
|
||||
echo ""
|
||||
echo "SDK codegen drift detected. Run:"
|
||||
echo ""
|
||||
echo " pnpm --filter @toolctrl/api-client-ts generate:all"
|
||||
echo " pnpm --filter @hivegrid/api-client-ts generate:all"
|
||||
echo ""
|
||||
echo "then commit packages/api-client-ts/openapi.json and packages/api-client-ts/src/generated/."
|
||||
exit 1
|
||||
@@ -70,10 +81,10 @@ jobs:
|
||||
|
||||
- name: Typecheck SDK packages
|
||||
run: |
|
||||
pnpm --filter @toolctrl/api-client-ts typecheck
|
||||
pnpm --filter @toolctrl/api-types typecheck
|
||||
pnpm --filter @hivegrid/api-client-ts typecheck
|
||||
pnpm --filter @hivegrid/api-types typecheck
|
||||
|
||||
- name: Run SDK tests
|
||||
run: |
|
||||
pnpm --filter @toolctrl/api-client-ts test
|
||||
pnpm --filter @toolctrl/api-types test
|
||||
pnpm --filter @hivegrid/api-client-ts test
|
||||
pnpm --filter @hivegrid/api-types test
|
||||
@@ -1,217 +0,0 @@
|
||||
name: Storybook
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/storybook/**"
|
||||
- "packages/ui/src/stories/**"
|
||||
- "packages/ui-app/src/stories/**"
|
||||
- "packages/ui-subframe/src/stories/**"
|
||||
- "packages/ui-mock/src/stories/**"
|
||||
- ".gitea/workflows/storybook.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TOOLCTRL_GITEA_HOST_IP: ${{ vars.TOOLCTRL_GITEA_HOST_IP }}
|
||||
PLAYWRIGHT_BROWSERS_PATH: .cache/ms-playwright
|
||||
|
||||
jobs:
|
||||
build-storybook:
|
||||
name: Build static Storybook
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Storybook static site
|
||||
run: pnpm --filter @toolctrl/storybook build-storybook
|
||||
|
||||
- name: Upload static Storybook artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storybook-static
|
||||
path: apps/storybook/storybook-static
|
||||
retention-days: 14
|
||||
|
||||
test-storybook:
|
||||
name: Storybook test-runner (a11y gate)
|
||||
needs: build-storybook
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Download built Storybook
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: storybook-static
|
||||
path: apps/storybook/storybook-static
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm --filter @toolctrl/storybook exec playwright install --with-deps chromium
|
||||
|
||||
- name: Serve static Storybook and run test-runner with a11y gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm --filter @toolctrl/storybook exec http-server storybook-static --port 6006 --silent &
|
||||
SERVER_PID=$!
|
||||
trap "kill $SERVER_PID 2>/dev/null || true" EXIT
|
||||
pnpm --filter @toolctrl/storybook exec wait-on http://127.0.0.1:6006 --timeout 60000
|
||||
TARGET_URL=http://127.0.0.1:6006 pnpm --filter @toolctrl/storybook test:storybook
|
||||
|
||||
visual-regression:
|
||||
name: Storybook visual-regression (Lost-Pixel)
|
||||
needs: build-storybook
|
||||
runs-on: toolctrl-ci
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Resolve internal Gitea host
|
||||
run: |
|
||||
if [ -n "${TOOLCTRL_GITEA_HOST_IP:-}" ]; then
|
||||
if ! TOOLCTRL_GITEA_HOST_IP="$TOOLCTRL_GITEA_HOST_IP" node -e 'process.exit(require("node:net").isIP(process.env.TOOLCTRL_GITEA_HOST_IP || "") ? 0 : 1)'; then
|
||||
echo "TOOLCTRL_GITEA_HOST_IP must be a single IPv4 or IPv6 token"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s git.tool-ctrl.de\n' "$TOOLCTRL_GITEA_HOST_IP" >> /etc/hosts
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for committed Lost-Pixel baseline
|
||||
id: baseline
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ls apps/storybook/.lostpixel/baseline/*.png >/dev/null 2>&1; then
|
||||
echo "present=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "present=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No Lost-Pixel baseline committed yet — skipping visual-regression. Run 'pnpm --filter @toolctrl/storybook visual-regression:update' and commit apps/storybook/.lostpixel/baseline/*.png to enable this gate."
|
||||
fi
|
||||
|
||||
- name: Enable Corepack
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Download built Storybook
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: storybook-static
|
||||
path: apps/storybook/storybook-static
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm --filter @toolctrl/storybook exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run Lost-Pixel visual-regression diff
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm --filter @toolctrl/storybook visual-regression
|
||||
|
||||
- name: Upload Lost-Pixel diff images
|
||||
if: steps.baseline.outputs.present == 'true' && failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lost-pixel-diff
|
||||
path: apps/storybook/.lostpixel/difference
|
||||
retention-days: 14
|
||||
+1
-1
@@ -8,7 +8,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
project:
|
||||
description: "Sentry project slug (boilerplate-api|boilerplate-worker|boilerplate-web)"
|
||||
description: "Sentry project slug (hivegrid-api|hivegrid-worker|hivegrid-web)"
|
||||
required: true
|
||||
type: string
|
||||
environment:
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Accessibility
|
||||
# Runs the axe-core vitest suites (apps/web ui components, SkipToContent)
|
||||
# and the WCAG-2.2-AA color-contrast verifier on every PR + push to main.
|
||||
#
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/wcag-internal.md
|
||||
# Docs: docs/contributing/wcag-audit.md
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ name: Bundle Budget Baseline
|
||||
# Captures the main-branch bundle report as an artifact so the
|
||||
# bundle-budget PR workflow can render Δ-from-main columns. Runs on
|
||||
# push to main; downstream PR workflow downloads `bundle-baseline-main`.
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/bundle-budget.md
|
||||
# Docs: docs/architecture/bundle-budgets.md
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build web
|
||||
run: pnpm --filter @toolctrl/web build
|
||||
run: pnpm --filter @hivegrid/web build
|
||||
|
||||
- name: Verify aggregate bundle budgets
|
||||
run: pnpm run verify:bundle-size
|
||||
|
||||
+2
-3
@@ -2,7 +2,6 @@ name: Bundle Budget
|
||||
|
||||
# Posts a sticky PR comment with per-route gzip sizes vs scripts/bundle-budgets.json.
|
||||
# Non-blocking: exceeds-limit routes show a warning row but never fail the PR check.
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/bundle-budget.md
|
||||
# Docs: docs/architecture/bundle-budgets.md
|
||||
|
||||
on:
|
||||
@@ -35,7 +34,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Checkout PR head
|
||||
uses: actions/checkout@v4
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build web (PR head)
|
||||
run: pnpm --filter @toolctrl/web build
|
||||
run: pnpm --filter @hivegrid/web build
|
||||
|
||||
- name: Verify aggregate bundle budgets
|
||||
run: pnpm run verify:bundle-size
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -122,15 +122,12 @@ jobs:
|
||||
- name: Workspace package tests
|
||||
run: pnpm run test:packages
|
||||
|
||||
- name: Mobile wrapper tests
|
||||
run: pnpm run test:mobile
|
||||
|
||||
- name: Unit tests
|
||||
run: pnpm test
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
TOOLCTRL_BUILD_SOURCEMAPS: "1"
|
||||
HIVEGRID_BUILD_SOURCEMAPS: "1"
|
||||
run: pnpm run build:ci
|
||||
|
||||
- name: Verify bundle size
|
||||
@@ -155,7 +152,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -184,20 +181,20 @@ jobs:
|
||||
run: pnpm run verify:openapi-security
|
||||
|
||||
- name: Typecheck API
|
||||
run: pnpm --filter @toolctrl/api typecheck
|
||||
run: pnpm --filter @hivegrid/api typecheck
|
||||
|
||||
- name: Test API
|
||||
run: pnpm exec vitest run apps/api/src
|
||||
|
||||
- name: Build API
|
||||
run: pnpm --filter @toolctrl/api build
|
||||
run: pnpm --filter @hivegrid/api build
|
||||
|
||||
worker:
|
||||
name: Worker Service
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -223,16 +220,16 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Typecheck Worker
|
||||
run: pnpm --filter @toolctrl/worker typecheck
|
||||
run: pnpm --filter @hivegrid/worker typecheck
|
||||
|
||||
- name: Test Worker
|
||||
run: pnpm --filter @toolctrl/worker exec vitest run
|
||||
run: pnpm --filter @hivegrid/worker exec vitest run
|
||||
|
||||
- name: Build Worker
|
||||
run: pnpm --filter @toolctrl/worker build
|
||||
run: pnpm --filter @hivegrid/worker build
|
||||
|
||||
- name: Health Check Worker
|
||||
run: pnpm --filter @toolctrl/worker health
|
||||
run: pnpm --filter @hivegrid/worker health
|
||||
env:
|
||||
WORKER_ENABLED: "false"
|
||||
|
||||
@@ -267,7 +264,10 @@ jobs:
|
||||
- name: Generate database client
|
||||
run: pnpm run db:generate
|
||||
env:
|
||||
DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public
|
||||
DATABASE_URL: postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public
|
||||
|
||||
- name: Verify migrations schema parity
|
||||
run: pnpm run verify:migrations-parity
|
||||
|
||||
- name: Verify database migration status
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
|
||||
- name: Test database package
|
||||
run: pnpm --filter @toolctrl/db test
|
||||
run: pnpm --filter @hivegrid/db test
|
||||
|
||||
docker-smoke:
|
||||
name: Docker Smoke
|
||||
@@ -296,17 +296,17 @@ jobs:
|
||||
context: .
|
||||
target: web
|
||||
build-args: APP_REVISION=ci-smoke-revision
|
||||
tags: toolctrl-web:smoke
|
||||
tags: hivegrid-web:smoke
|
||||
load: true
|
||||
push: false
|
||||
cache-from: type=gha,scope=toolctrl-web-smoke
|
||||
cache-to: type=gha,mode=max,scope=toolctrl-web-smoke
|
||||
cache-from: type=gha,scope=hivegrid-web-smoke
|
||||
cache-to: type=gha,mode=max,scope=hivegrid-web-smoke
|
||||
|
||||
- name: Validate web Docker artifacts
|
||||
run: docker run --rm --entrypoint /bin/sh toolctrl-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/build/server-runtime/server.js'
|
||||
run: docker run --rm --entrypoint /bin/sh hivegrid-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/build/server-runtime/server.js'
|
||||
|
||||
- name: Validate web Docker runtime
|
||||
run: bash scripts/docker-web-runtime-smoke.sh toolctrl-web:smoke
|
||||
run: bash scripts/docker-web-runtime-smoke.sh hivegrid-web:smoke
|
||||
|
||||
- name: Build API Docker target
|
||||
uses: docker/build-push-action@v7
|
||||
@@ -314,14 +314,14 @@ jobs:
|
||||
context: .
|
||||
target: api
|
||||
build-args: APP_REVISION=ci-smoke-revision
|
||||
tags: toolctrl-api:smoke
|
||||
tags: hivegrid-api:smoke
|
||||
load: true
|
||||
push: false
|
||||
cache-from: type=gha,scope=toolctrl-api-smoke
|
||||
cache-to: type=gha,mode=max,scope=toolctrl-api-smoke
|
||||
cache-from: type=gha,scope=hivegrid-api-smoke
|
||||
cache-to: type=gha,mode=max,scope=hivegrid-api-smoke
|
||||
|
||||
- name: Validate API Docker runtime
|
||||
run: bash scripts/docker-api-runtime-smoke.sh toolctrl-api:smoke
|
||||
run: bash scripts/docker-api-runtime-smoke.sh hivegrid-api:smoke
|
||||
|
||||
- name: Build worker Docker target
|
||||
uses: docker/build-push-action@v7
|
||||
@@ -329,14 +329,14 @@ jobs:
|
||||
context: .
|
||||
target: worker
|
||||
build-args: APP_REVISION=ci-smoke-revision
|
||||
tags: toolctrl-worker:smoke
|
||||
tags: hivegrid-worker:smoke
|
||||
load: true
|
||||
push: false
|
||||
cache-from: type=gha,scope=toolctrl-worker-smoke
|
||||
cache-to: type=gha,mode=max,scope=toolctrl-worker-smoke
|
||||
cache-from: type=gha,scope=hivegrid-worker-smoke
|
||||
cache-to: type=gha,mode=max,scope=hivegrid-worker-smoke
|
||||
|
||||
- name: Validate worker Docker runtime
|
||||
run: bash scripts/docker-worker-runtime-smoke.sh toolctrl-worker:smoke
|
||||
run: bash scripts/docker-worker-runtime-smoke.sh hivegrid-worker:smoke
|
||||
|
||||
- name: Validate Docker Compose config
|
||||
run: docker compose config
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
needs: resolve-sentry-environment
|
||||
uses: ./.github/workflows/_sentry-release.yml
|
||||
with:
|
||||
project: boilerplate-api
|
||||
project: hivegrid-api
|
||||
environment: ${{ needs.resolve-sentry-environment.outputs.environment }}
|
||||
sourcemaps-path: none
|
||||
secrets:
|
||||
@@ -379,7 +379,7 @@ jobs:
|
||||
needs: resolve-sentry-environment
|
||||
uses: ./.github/workflows/_sentry-release.yml
|
||||
with:
|
||||
project: boilerplate-worker
|
||||
project: hivegrid-worker
|
||||
environment: ${{ needs.resolve-sentry-environment.outputs.environment }}
|
||||
sourcemaps-path: none
|
||||
secrets:
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
needs: resolve-sentry-environment
|
||||
uses: ./.github/workflows/_sentry-release.yml
|
||||
with:
|
||||
project: boilerplate-web
|
||||
project: hivegrid-web
|
||||
environment: ${{ needs.resolve-sentry-environment.outputs.environment }}
|
||||
sourcemaps-path: apps/web/build/client
|
||||
sourcemaps-artifact: web-client-sourcemaps
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I acknowledge that I have read the SaasRock CLA Document and I hereby sign the CLA. I acknowledge that I understand this code is not open source and for commercial purposes. I acknowledge that I am waiving all Intellectual Property Rights; including, but not limited to copyright.') || github.event_name == 'pull_request_target'
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I acknowledge that I have read the LegacySourceProduct CLA Document and I hereby sign the CLA. I acknowledge that I understand this code is not open source and for commercial purposes. I acknowledge that I am waiving all Intellectual Property Rights; including, but not limited to copyright.') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.2.0
|
||||
env:
|
||||
|
||||
@@ -5,7 +5,6 @@ name: Coverage
|
||||
# Uploads the lightweight gate summary as an artifact and posts a sticky PR
|
||||
# comment with the coverage-summary.json totals.
|
||||
#
|
||||
# Spec: docs/improvement-plan/10-compliance-polish/test-coverage.md
|
||||
# Docs: docs/architecture/test-coverage.md
|
||||
|
||||
on:
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Run vitest with coverage (enforces per-path gates)
|
||||
env:
|
||||
TOOLCTRL_COVERAGE_GATE: "1"
|
||||
HIVEGRID_COVERAGE_GATE: "1"
|
||||
run: pnpm run verify:test-coverage
|
||||
|
||||
- name: Upload coverage gate summary
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
E2E_BASE_URL: https://boiler.saas-ctrl.com
|
||||
E2E_BASE_URL: https://app.hivegrid.de
|
||||
E2E_A11Y_STRICT: "1"
|
||||
E2E_BROWSER_MATRIX: "1"
|
||||
E2E_ALLOW_WORKFLOW_RUN: "1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Lighthouse CI
|
||||
|
||||
# GitHub mirror of .gitea/workflows/lighthouse.yml. Audits the deployed
|
||||
# staging site (https://boiler.saas-ctrl.com) nightly + on manual dispatch.
|
||||
# staging site (https://app.hivegrid.de) nightly + on manual dispatch.
|
||||
# See the Gitea variant and docs/architecture/lighthouse.md for the rationale
|
||||
# behind auditing a deployed environment rather than a CI-local server.
|
||||
|
||||
@@ -23,12 +23,12 @@ jobs:
|
||||
- name: Mint Lighthouse session cookie
|
||||
run: node scripts/rotate-lighthouse-session-cookie.mjs
|
||||
env:
|
||||
LIGHTHOUSE_LOGIN_URL: https://boiler.saas-ctrl.com/login
|
||||
LIGHTHOUSE_LOGIN_URL: https://app.hivegrid.de/login
|
||||
LIGHTHOUSE_LOGIN_EMAIL: ${{ secrets.LIGHTHOUSE_LOGIN_EMAIL }}
|
||||
LIGHTHOUSE_LOGIN_PASSWORD: ${{ secrets.LIGHTHOUSE_LOGIN_PASSWORD }}
|
||||
LIGHTHOUSE_SESSION_COOKIE: ${{ secrets.LIGHTHOUSE_SESSION_COOKIE }}
|
||||
LIGHTHOUSE_SESSION_COOKIE_NAME: RJ_session
|
||||
LIGHTHOUSE_COOKIE_DOMAIN: boiler.saas-ctrl.com
|
||||
LIGHTHOUSE_SESSION_COOKIE_NAME: hivegrid_session
|
||||
LIGHTHOUSE_COOKIE_DOMAIN: app.hivegrid.de
|
||||
|
||||
- name: Run Lighthouse-CI against staging
|
||||
# ubuntu-latest ships Chrome pre-installed; no browser setup needed.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: SDK Codegen Drift
|
||||
|
||||
# Re-runs the @toolctrl/api-client-ts generation pipeline on PRs that touch
|
||||
# Re-runs the @hivegrid/api-client-ts generation pipeline on PRs that touch
|
||||
# the upstream OpenAPI surface (apps/api/src/openapi.ts) or the SDK package
|
||||
# itself, then verifies the committed src/generated/ tree is in sync with
|
||||
# the captured openapi.json.
|
||||
#
|
||||
# Drift is a hard failure: regenerating + committing is part of the PR.
|
||||
#
|
||||
# Spec: docs/improvement-plan/08-mobile-cli-storybook-sdk/ts-sdk.md
|
||||
# Docs: docs/sdks/typescript.md
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -52,10 +52,10 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Regenerate OpenAPI capture
|
||||
run: pnpm --filter @toolctrl/api-client-ts generate-spec
|
||||
run: pnpm --filter @hivegrid/api-client-ts generate-spec
|
||||
|
||||
- name: Regenerate typed client (Hey-API)
|
||||
run: pnpm --filter @toolctrl/api-client-ts generate
|
||||
run: pnpm --filter @hivegrid/api-client-ts generate
|
||||
|
||||
- name: Fail if generated code drifts from committed state
|
||||
run: |
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
echo ""
|
||||
echo "SDK codegen drift detected. Run:"
|
||||
echo ""
|
||||
echo " pnpm --filter @toolctrl/api-client-ts generate:all"
|
||||
echo " pnpm --filter @hivegrid/api-client-ts generate:all"
|
||||
echo ""
|
||||
echo "then commit packages/api-client-ts/openapi.json and packages/api-client-ts/src/generated/."
|
||||
exit 1
|
||||
@@ -74,10 +74,10 @@ jobs:
|
||||
|
||||
- name: Typecheck SDK packages
|
||||
run: |
|
||||
pnpm --filter @toolctrl/api-client-ts typecheck
|
||||
pnpm --filter @toolctrl/api-types typecheck
|
||||
pnpm --filter @hivegrid/api-client-ts typecheck
|
||||
pnpm --filter @hivegrid/api-types typecheck
|
||||
|
||||
- name: Run SDK tests
|
||||
run: |
|
||||
pnpm --filter @toolctrl/api-client-ts test
|
||||
pnpm --filter @toolctrl/api-types test
|
||||
pnpm --filter @hivegrid/api-client-ts test
|
||||
pnpm --filter @hivegrid/api-types test
|
||||
@@ -1,197 +0,0 @@
|
||||
name: Storybook
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/storybook/**"
|
||||
- "packages/ui/src/stories/**"
|
||||
- "packages/ui-app/src/stories/**"
|
||||
- "packages/ui-subframe/src/stories/**"
|
||||
- "packages/ui-mock/src/stories/**"
|
||||
- ".github/workflows/storybook.yml"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/storybook/**"
|
||||
- "packages/ui/src/stories/**"
|
||||
- "packages/ui-app/src/stories/**"
|
||||
- "packages/ui-subframe/src/stories/**"
|
||||
- "packages/ui-mock/src/stories/**"
|
||||
- ".github/workflows/storybook.yml"
|
||||
|
||||
env:
|
||||
PLAYWRIGHT_BROWSERS_PATH: .cache/ms-playwright
|
||||
|
||||
jobs:
|
||||
build-storybook:
|
||||
name: Build static Storybook
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Storybook static site
|
||||
run: pnpm --filter @toolctrl/storybook build-storybook
|
||||
|
||||
- name: Upload static Storybook artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: apps/storybook/storybook-static
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
test-storybook:
|
||||
name: Storybook test-runner (a11y gate)
|
||||
needs: build-storybook
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Download built Storybook
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: apps/storybook/storybook-static
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm --filter @toolctrl/storybook exec playwright install --with-deps chromium
|
||||
|
||||
- name: Serve static Storybook and run test-runner with a11y gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm --filter @toolctrl/storybook exec http-server storybook-static --port 6006 --silent &
|
||||
SERVER_PID=$!
|
||||
trap "kill $SERVER_PID 2>/dev/null || true" EXIT
|
||||
pnpm --filter @toolctrl/storybook exec wait-on http://127.0.0.1:6006 --timeout 60000
|
||||
TARGET_URL=http://127.0.0.1:6006 pnpm --filter @toolctrl/storybook test:storybook
|
||||
|
||||
visual-regression:
|
||||
name: Storybook visual-regression (Lost-Pixel)
|
||||
needs: build-storybook
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for committed Lost-Pixel baseline
|
||||
id: baseline
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ls apps/storybook/.lostpixel/baseline/*.png >/dev/null 2>&1; then
|
||||
echo "present=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "present=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::No Lost-Pixel baseline committed yet — skipping visual-regression. Run 'pnpm --filter @toolctrl/storybook visual-regression:update' and commit apps/storybook/.lostpixel/baseline/*.png to enable this gate."
|
||||
fi
|
||||
|
||||
- name: Enable Corepack
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: corepack enable
|
||||
|
||||
- name: Prepare pnpm 10.33.0
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
- name: Configure pnpm store
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm config set store-dir .pnpm-store
|
||||
|
||||
- name: Cache pnpm store
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Download built Storybook
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: apps/storybook/storybook-static
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm --filter @toolctrl/storybook exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run Lost-Pixel visual-regression diff
|
||||
if: steps.baseline.outputs.present == 'true'
|
||||
run: pnpm --filter @toolctrl/storybook visual-regression
|
||||
|
||||
- name: Upload Lost-Pixel diff images
|
||||
if: steps.baseline.outputs.present == 'true' && failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lost-pixel-diff
|
||||
path: apps/storybook/.lostpixel/difference
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
@@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
.cache
|
||||
.env
|
||||
@@ -7,7 +8,6 @@ node_modules
|
||||
!.env.example
|
||||
!.env.local.example
|
||||
!.env.production.example
|
||||
!.env.fly.example
|
||||
!.env.test
|
||||
.vercel
|
||||
.output
|
||||
|
||||
@@ -274,17 +274,17 @@ Diese werden von vielen Modulen geschrieben/referenziert. Sie sind der Grund, wa
|
||||
**Struktur (keine Seat-Typen mehr - Basis-Preis pro Nutzer + Feature-Module als Positionen):**
|
||||
|
||||
```
|
||||
SubscriptionProduct ─1:N─▶ SubscriptionPrice (stripe_price_id, interval month|year|one_time)
|
||||
SubscriptionProduct ─1:N─▶ SubscriptionPrice (legacyProvider_price_id, interval month|year|one_time)
|
||||
│ (Plan: Core/Swarm/Colony)
|
||||
├─M:N─▶ SubscriptionFeature (via ProductFeature, +quota_value JSON) ◀── Konnektor-Gate!
|
||||
└─1:N─▶ SubscriptionUsageBasedPrice ─1:N─▶ UsageBasedTier (Staffel-Overage)
|
||||
│
|
||||
TenantSubscription (1:1 Tenant) ─N:1─▶ Product │
|
||||
├─1:N─▶ TenantSubscriptionProductPrice (quantity=Seats, is_metered)
|
||||
├─1:N─▶ TenantSubscriptionUsageRecord (metric, usage_amount, reported_to_stripe)
|
||||
└─ status (trial/active/past_due/cancelled/frozen) + Stripe-IDs + Dunning-Felder
|
||||
├─1:N─▶ TenantSubscriptionUsageRecord (metric, usage_amount, reported_to_legacyProvider)
|
||||
└─ status (trial/active/past_due/cancelled/frozen) + Legacy-Provider-IDs + Dunning-Felder
|
||||
Credit (ai_token/bonus, balance_after, valid_until) ─N:1─▶ Tenant
|
||||
CheckoutSessionStatus (stripe_checkout_session, intent) ─N:1─▶ Tenant
|
||||
CheckoutSessionStatus (legacyProvider_checkout_session, intent) ─N:1─▶ Tenant
|
||||
```
|
||||
|
||||
`SubscriptionFeature.code` (z. B. `kwp_integration`) ist die Brücke zur Konnektor-Pflicht: `tenantHasModule()` prüft, ob das Feature im Abo des Tenants enthalten ist. Quotas (max_users, ai_tokens) hängen an `SubscriptionProductFeature.quota_value`.
|
||||
|
||||
@@ -29,7 +29,7 @@ Spalte „Wer" im Team-Meeting ausfüllen. **Kategorie A heute anstoßen — das
|
||||
| A2 | **Google Cloud:** OAuth-App + **nur Calendar-API**-Scope-Verifizierung (Gmail-Scopes entfallen — Postfach-Sync → v2) | P16 Kalender (nur Google; Outlook → v2) | 🟠 Review Tage–Wochen; Testmodus mit Workspace-Konten als Brücke | |
|
||||
| ~~A3~~ | ~~Apple Developer~~ | **v3** (kein App-Store in v1/v2 — PWA-only, AD-17) | Konto vorhanden, ruht — keine Aktion | — |
|
||||
| ~~A4~~ | ~~Google Play Console~~ | **v3** (kein App-Store in v1/v2 — PWA-only) | ruht bis native App (v3) | — |
|
||||
| A5 | **Stripe-Konto + Live-Verifizierung** (Geschäftsdaten, Bankkonto, ggf. Webseite-Anforderungen) | P20 | 🟠 Tage; Test-Mode reicht für Entwicklung, Live für Go-Live | |
|
||||
| A5 | **Mollie-Konto + Live-Verifizierung** (Geschäftsdaten, Bankkonto, Webseite-Anforderungen) + Lago-Production-Planung | P20 / BILL-08 | 🟠 Tage; Test-Mode reicht für Entwicklung, Live für Go-Live | |
|
||||
| A6 | **Sende-Domain einrichten:** SPF/DKIM/DMARC für Mailjet + **Domain-Warm-up** beginnen | P1 (notify), P2 (Einladungen), P14 (Marketing!) | 🟠 DNS sofort; Reputation/Warm-up braucht Laufzeit — je früher, desto besser für Zustellbarkeit | |
|
||||
| ~~A7~~ | ~~Microsoft Entra (Outlook/Graph)~~ | v2 (Outlook-Kalender + Postfach-Sync → v2) | → v2-Vorbereitung | — |
|
||||
|
||||
@@ -54,7 +54,7 @@ Spalte „Wer" im Team-Meeting ausfüllen. **Kategorie A heute anstoßen — das
|
||||
|---|---|---|---|---|
|
||||
| C1 | **Meilisearch: läuft bereits zentral auf `node1`** (Infra-ADR-008) — nur **scoped Key minten** (`indexes: ["hivegrid-*"]`, Muster: Boilerplate-Key vom 06.06.; `MEILI_API_KEY`, nicht Master-Key!) | P11 (AD-5) | 🟢 5 Minuten statt Setup | |
|
||||
| C2 | **mCaptcha: läuft bereits zentral** auf `captcha.saas-ctrl.com` (Infra-ADR-013) — nur ENV anbinden + Sitekey anlegen (AD-7 ✓) | P2 Login, P16 Public-Forms/Booking | 🟢 anbinden statt aufsetzen | |
|
||||
| C3 | **Hosting/Deploy (Infra-ADR-009/010, ersetzt Fly/Scaleway — `fly.toml` entfernt, `deployment-region-provider-pinning.md` stale!):** Staging auf **`staging-1`** via zentrale **Coolify**-Control-Plane (Muster: Boilerplate läuft dort mit 8 Apps) · Prod = Hetzner-Projekt `HiveGrid` (`hivegrid-db`/`-app`/`-worker`/`-lb`) · Registry = Gitea, build-once/promote (Infra-ADR-005). **Staging (User-Entscheidung 12.06., revidiert): ÖFFENTLICH** auf `staging-app.hivegrid.de` + `staging-api.hivegrid.de` (beide unter dem `*.hivegrid.de`-Wildcard-Zertifikat, kein VPN). **Pflicht-Härtung dafür:** (a) **NUR synthetische Daten** — niemals echte Kunden-/Geerkens-Daten auf Staging (DSGVO; Demo-Seeds reichen), (b) eigene Secrets/Keys je Umgebung (Stripe Test-Mode, eigene OAuth-Apps, eigene Webhook-Secrets), (c) Webhooks auf staging-api mit HMAC-Signaturpflicht + Rate-Limit, (d) `X-Robots-Tag: noindex` + robots.txt deny, (e) Login wie Prod (TOTP), optional IP-Allowlist nachrüstbar. OAuth-Callbacks: Staging-Redirect-URIs (`staging-app.…`) bei KWP/Google mitregistrieren | ab W1 (CI/Gates brauchen Staging!) | Mitte W1 | |
|
||||
| C3 | **Hosting/Deploy (Infra-ADR-009/010, ersetzt Fly/Scaleway — `fly.toml` entfernt, `deployment-region-provider-pinning.md` stale!):** Staging auf **`staging-1`** via zentrale **Coolify**-Control-Plane (Muster: Boilerplate läuft dort mit 8 Apps) · Prod = Hetzner-Projekt `HiveGrid` (`hivegrid-db`/`-app`/`-worker`/`-lb`) · Registry = Gitea, build-once/promote (Infra-ADR-005). **Staging (User-Entscheidung 12.06., revidiert): ÖFFENTLICH** auf `staging-app.hivegrid.de` + `staging-api.hivegrid.de` (beide unter dem `*.hivegrid.de`-Wildcard-Zertifikat, kein VPN). **Pflicht-Härtung dafür:** (a) **NUR synthetische Daten** — niemals echte Kunden-/Geerkens-Daten auf Staging (DSGVO; Demo-Seeds reichen), (b) eigene Secrets/Keys je Umgebung (Mollie Test-Mode, Lago Staging, eigene OAuth-Apps, eigene Webhook-Secrets), (c) Webhooks auf staging-api mit HMAC-Signaturpflicht + Rate-Limit, (d) `X-Robots-Tag: noindex` + robots.txt deny, (e) Login wie Prod (TOTP), optional IP-Allowlist nachrüstbar. OAuth-Callbacks: Staging-Redirect-URIs (`staging-app.…`) bei KWP/Google mitregistrieren | ab W1 (CI/Gates brauchen Staging!) | Mitte W1 | |
|
||||
| C4 | **Postgres + Redis: Coolify-managed** (Staging auf `staging-1`, Prod auf `hivegrid-db` — Muster: mITapp/Boilerplate; PgBouncer auf staging-1 vorhanden, siehe `docs/ops/pgbouncer.md`) | alles | Mitte W1 | |
|
||||
| C5 | **Domains/DNS + TLS (Cloudflare — `hivegrid.de`-Zone existiert bereits!):** `app.hivegrid.de` + `admin.hivegrid.de` (Host-Routing + Cookie-Domain-Trennung!) + `staging-app.hivegrid.de` + `staging-api.hivegrid.de` (öffentlich, siehe C3) — ein `*.hivegrid.de`-Wildcard deckt alle. Portal/Forms/Booking/Help/API sind **Pfade** unter app.* gemäß `INTEGRATION-CONTRACTS.md` §6 (korrigiert 12.06.) | P1 URL-Schemata, P19 Admin, C3 Staging | W1 | |
|
||||
| C6 | (Optional) **Authentik** für Admin-SSO (AD-3, optional) | P19 | kann Fast-Follow sein | |
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# Interim-UI Design-Standards — „David-nah bauen"
|
||||
|
||||
**Zweck:** Damit eine Interim-UI (AD-28, `.planning/INTERIM-UI-PLAYBOOK.md`) möglichst nah an Davids
|
||||
kommenden finalen Subframe-Screens landet, baut der Dev nach **denselben** Layout-/Komponenten-/State-
|
||||
Standards wie David. Dann ist Davids späterer Nachzug fast nur noch Feinschliff.
|
||||
|
||||
**Quelle & Autorität:** Diese Werte sind ein **In-Repo-Spiegel** von Davids Wireframe-Vault
|
||||
`HiveGrid-Wireframes/` (Schwester-Ordner: `../HiveGrid-Wireframes/`, Gitea `git.tool-ctrl.de/david/Hivegrid-Wireframes`).
|
||||
Autoritativ ist **immer der Vault**:
|
||||
- `../HiveGrid-Wireframes/04 Standards/Subframe UI Standards/Layout/README.md` (Layout-Bibel, Entscheidungen D01–D0x)
|
||||
- `../HiveGrid-Wireframes/Tickets/Fertige Tickets/D-D0b Layout Standards.md` (Handoff der Layout-Standards)
|
||||
- `../HiveGrid-Wireframes/01 Regeln/Wireframing Playbook.md` + `Screen Agent Workflow.md` (Bau-/State-Regeln)
|
||||
- `../HiveGrid-Wireframes/02 Wireframes/Screen Paket Tracker.md` (Screen-/Flow-Liste, Subframe-Links)
|
||||
|
||||
Der Vault liegt nicht im Cloud-Repo (CI/andere Maschinen haben ihn evtl. nicht) — deshalb dieser Spiegel.
|
||||
**Bei Zweifel/Drift den Vault lesen und diese Datei nachziehen.** Davids Werte gewinnen.
|
||||
|
||||
---
|
||||
|
||||
## 0. Grundprinzip — Component-Defaults statt manueller Tailwind-Overrides
|
||||
|
||||
Davids härteste Bau-Regel (Layout-README, „Nutzung bei Subframe-MCP-Arbeit"):
|
||||
|
||||
> Komponenten-Defaults und Varianten verwenden, wenn sie den Standard bereits korrekt abbilden. Keine
|
||||
> manuellen Tailwind-Overrides für Werte, die bereits über Auto Layout, Komponenten oder Varianten
|
||||
> abgedeckt sind.
|
||||
|
||||
Für uns: **`~/ui`-Component-Defaults sind führend.** Wenn `Button`, `TextInput`, `Table`, `Dialog`, `Card`
|
||||
das Padding/Gap/die Höhe schon intern richtig haben → Component + passende Variante nutzen, **nicht**
|
||||
Höhen/Paddings mit Tailwind nachbauen. Manuelle Werte nur für feste Größen, Max-Breiten, Scroll/Overflow
|
||||
oder responsive Constraints. Die Zahlen unten sind zum Prüfen/Setzen dort, wo eine Komponente sie nicht
|
||||
selbst mitbringt — nicht zum Überschreiben funktionierender Defaults.
|
||||
|
||||
---
|
||||
|
||||
## 1. Spacing-Skala (D01) — 8px-Rhythmus
|
||||
|
||||
Erlaubte Layout-Werte (px): **4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96**.
|
||||
- `8` = Basislogik für normale Gaps/Paddings. `4/12/20` = kompakte Zwischenwerte (Field-Anatomy, kleine
|
||||
Icon-/Text-Abstände). `2` nur für Border-/Focus-/Icon-Feintuning.
|
||||
- **Nicht neu einführen:** 6, 10, 14, 28, 36, 44 — nur wenn eine bestehende Komponente/ein Token sie schon mitbringt.
|
||||
|
||||
## 2. Page / Card / Grid-Padding (D02)
|
||||
|
||||
| Rolle | Wert |
|
||||
|---|---|
|
||||
| Page Padding (Standard & Minimum) | **24px** (nie darunter) |
|
||||
| Card / Surface Padding | **16px** |
|
||||
| Detail-/Editor-Surface Padding | **16px** |
|
||||
| Grid / Card Gap | **16px** (kompakter Sonderfall 12px) |
|
||||
| Section Gap (größere Bereiche) | **24px** |
|
||||
|
||||
Kompakte Momente reduzieren **nur lokale Gaps**, nie das Page Padding. `32px` ist **kein** freier Standard
|
||||
für Detail-/Editorflächen — Ausnahme: die `Dialog`-Komponente darf eine 32px-Variante haben. Mobile-Padding = Backlog.
|
||||
|
||||
## 3. Text-Rhythmus (D03) — nur mit 150%-Text-Tokens für beschreibende Texte
|
||||
|
||||
| Zwischenraum | Gap |
|
||||
|---|---|
|
||||
| Page Title → Page Description | **0px** |
|
||||
| Page Header → erster Content | **24px** |
|
||||
| Section Title → Section Description | **0px** |
|
||||
| Section Description → Section Content | **12px** |
|
||||
|
||||
`0px` ist ein bewusster Gap (kein Auto-Layout-Abstand). Beschreibungen nutzen **150%-Line-height-Tokens**
|
||||
(auch bei einer Zeile). `0`-Line-height-Token nur für garantierte Einzeiler (Button-Label, Badge, Tab,
|
||||
kompakte Truncation-Zelle), **nie** für umbrechbaren Text.
|
||||
|
||||
## 4. Formulare & Field-Anatomy
|
||||
|
||||
- **Field-Gap** zwischen kompletten Feld-Komponenten: **16px** (kompakt 12px; bewusst getrennte Sections 24px).
|
||||
- **Field-Anatomy** (Reihenfolge `label → helpText → Control → message`):
|
||||
`label → helpText` **8px** · `helpText → Control` **6px** · `Control → message` **4px** · `message` = 12px, Medium.
|
||||
- Beschreibungen/Hilfetexte über die Component-Props/Slots füllen, **nicht** als freie Textblöcke daneben bauen.
|
||||
- **Feldnamen (Vokabular):** unterer Field-Text heißt immer **`message`** (nicht `label`/`helper`/`helpText`).
|
||||
- Form-Input horizontales Innenpadding: **12px** (ohne Icon / Rand↔Icon), Icon↔Text **8px** (Textarea ausgenommen).
|
||||
- Form-Input-Radius: **`Radius Small` = 4px**.
|
||||
|
||||
## 5. Control-Höhen & Radius
|
||||
|
||||
- **Control-Höhen (exakt):** Small **32px** · Medium **36px** · Large **48px**. **Medium ist Default**
|
||||
(heißt aber `Medium`, nicht `Default`). Gilt für Button, IconButton, TextInput, NumberInput, Select,
|
||||
Multiselect(+Tags), Combobox, SearchInput, DatePicker (Single), ToggleGroup, ButtonGroup.
|
||||
- **Radius-Tokens:** Small **4px** · Medium **8px** · Large **12px** · Round **9999px**.
|
||||
- **Button:** Default-Variante = Medium = 36px hoch. Button-Höhe/-Padding **nicht** im Screen nachbauen.
|
||||
- **Table:** Row-Height/Cell-Padding/Header-Dichte über die Table-Komponente (Carbon-Referenz: Rows
|
||||
xs 24 / sm 32 / md 40 / lg 48 / xl 64, Column-Padding 16). Dichte wählen statt eigene Tabellenwerte setzen.
|
||||
|
||||
## 6. Dialoge / Modale (D07) — echte Dialog-Struktur
|
||||
|
||||
**Modal = in `~/ui` immer die echte `Dialog`-Komponente** (`DialogLayout → Dialog → Dialog.Content`).
|
||||
**Verboten:** Modal aus `Card`, freiem `div`, absolutem Overlay, Blur-Hintergrund oder Fullscreen-Wrapper
|
||||
nur visuell nachbauen. Vor Handoff prüfen, dass `Dialog` importiert ist und `Dialog.Content` im JSX steht.
|
||||
|
||||
**Ort-Begriffe:** `Page` = eigener App-Ort (Settings etc.) · `Dialog` = temporäres zentriertes Overlay ·
|
||||
`Fullscreen Dialog` = viewport-füllendes Overlay (nicht für dauerhafte Bereiche) · `Drawer/Side Panel` =
|
||||
seitliches Overlay (eigene Breitenregel).
|
||||
|
||||
| Dialog-Größe | Breite | wofür |
|
||||
|---|---|---|
|
||||
| XS | **420px** | kurze Bestätigung / knapper Hinweis |
|
||||
| S | **560px** | kleines Formular, wenige Felder |
|
||||
| M | **720px** | normales Formular / kurze Section-Struktur |
|
||||
| L | **1200px max** | großer Editor mit Navigation/Suche |
|
||||
|
||||
- **Dialog-Padding:** **16px** Default, **32px** ruhigere Variante — über die Dialog-Variante, nicht per Override.
|
||||
- **Dialog-Interne Gaps:** Title→Description **8px** · Field/Form-Row **16px** · Section-Gap **24px** ·
|
||||
Footer-Button-Gap **8px**. **Footer-Actions rechts** ausgerichtet.
|
||||
- Breitenklasse bleibt fix (wächst nicht auf 4K mit), nur der Overlay-Rand wächst; auf kleinen Viewports
|
||||
Max-Width relativ zum Viewport. Fullscreen: Rand 24px/Seite, `width: calc(100vw - 48px)`, `height: calc(100vh - 48px)`.
|
||||
|
||||
## 7. Zustände (D09) — inline State-Komponenten, keine eigenen Screens
|
||||
|
||||
**Empty / Loading / Permission-Denied sind inline State-Komponenten, keine Modale und keine eigenen Screens**,
|
||||
solange sie dem Standard entsprechen. In `~/ui`: `EmptyState`, `ErrorState`, `SkeletonText`/`Loader`, plus
|
||||
Permission-Denied. (Erinnerung DoD/Bauprinzip: Permission-Denied zeigt **404, nicht 403**.)
|
||||
|
||||
**Nur** blockierende / sicherheits- / copy-kritische Zustände bekommen eigene Screens. Ein **blockierender
|
||||
Error-State** braucht: Titel/Kernaussage · Ursache in Nutzersprache · Primäraktion · Sekundäraktion (zurück/Support).
|
||||
|
||||
## 8. Banner / Toast / Modal / Inline / Screen — Entscheidungs-Hierarchie
|
||||
|
||||
| UI-Form | Wann | `~/ui` | eigener Screen? |
|
||||
|---|---|---|---|
|
||||
| **Inline** | betrifft genau ein Feld, dort korrigierbar (Pflichtfeld, ungültige Mail, 2FA-Code falsch) | Field-`message` | nein |
|
||||
| **Screen-Banner** | betrifft aktuellen Screen/Formular (Login falsch, Speichern fehlgeschlagen, Integration getrennt) | `BannerAlert`/`Alert` | nur wenn optisch/fachlich wichtig |
|
||||
| **Globaler Banner** | App/Shell/Tenant (Zahlung offen, Tenant gesperrt, Wartung) | App-Shell/D0 | D0 |
|
||||
| **Toast** | kurze, nicht-blockierende Rückmeldung (gespeichert, kopiert, Export gestartet) | `Toast` | nein |
|
||||
| **Modal/Dialog** | Nutzer bestätigt/entscheidet kurz, bleibt im Kontext (löschen, Rolle wechseln) | echter `Dialog` | falls eigener Entscheidmoment |
|
||||
| **Eigener Screen** | Nutzer blockiert / neuer Flow-Status (Link abgelaufen, Magic-Link gesendet, Account gesperrt) | eigene Route | ja |
|
||||
|
||||
Faustregel: direkt im Feld korrigierbar → Inline; bleibt im Screen → Banner; kurze Rückmeldung → Toast;
|
||||
muss bestätigen → Modal; blockiert → eigener Screen. Sicherheit/Zugang/Vertrauen/Recht → **eher Banner/Screen, nie Toast**.
|
||||
|
||||
## 9. Copy ist eine Design-Entscheidung
|
||||
|
||||
Bei **copy-kritischen** Zuständen (Auth, Sicherheit, Zugang, Erfolg — z.B. „Magic-Link gesendet",
|
||||
„E-Mail oder Passwort ist nicht korrekt" (neutral, keine Enumeration), „Account gesperrt") den Text
|
||||
**nicht selbst erfinden**. Entweder die im Screen-Paket dokumentierte Copy übernehmen oder Platzhalter-Copy
|
||||
**klar als interim markieren** (i18n-Key mit `TODO`/interim-Hinweis), damit Davids Copy leicht einsetzbar ist.
|
||||
Field-Validierung (Pflichtfeld, Passwort zu schwach) bleibt beim Form-Kit, kein eigener Zustand — außer fachlich/rechtlich wichtig.
|
||||
|
||||
## 10. Weitere Konventionen
|
||||
|
||||
- **Default-Ansicht = Admin**, außer der Flow braucht sichtbar eine andere Rolle (dann Variante).
|
||||
- **Vokabular:** `Medium` (nicht `Default`) · `message` (nicht `label`) · `error` ≠ `destructive` (getrennt halten) ·
|
||||
States `default/hover/active/focus/disabled/loading/error/selected` (`focus`≠`selected`).
|
||||
- **Prop-Reihenfolge** (falls du Component-APIs baust): Zustand → Erscheinung → Größe → Layout-Sonderfälle →
|
||||
Sichtbarkeit → Inhalt/Slots (Details in `07_Global-Rules.md` des Vaults).
|
||||
|
||||
---
|
||||
|
||||
## 11. Verzahnung mit Davids Screen-Format (gleiche Sprache sprechen)
|
||||
|
||||
Damit der spätere Swap trivial wird, benennt und strukturiert die Interim-UI wie Davids Vault:
|
||||
|
||||
- **Screen-IDs übernehmen:** `S` = eigener Screen · `V` = Variante · `M` = Modal · `D` = dokumentierter
|
||||
Zustand (kein Frame). Namen 1:1 wie im **Screen Paket Tracker** (z.B. „S01 Login", „S04 2FA Code eingeben",
|
||||
„M02 Rolle bearbeiten", „V01 Login falsche Daten"). Interim-`<XPage>`-Dateien/Komponenten nach diesen IDs benennen.
|
||||
- **Join-Schlüssel `ticket: D-*`:** Davids Pakete/Screens tragen `ticket: D-P2` (usw.) im Frontmatter — das
|
||||
ist die Brücke zwischen unserem Cloud-Dev-Ticket und seinen Screens. Im Interim-UI-Vermerk der Ticket-Doc
|
||||
das zugehörige `D-*` nennen (steht ohnehin im „blocked-by"/Screens-Block).
|
||||
- **Subframe-Links:** Sobald Davids Paket auf `In Subframe`/`Review`/`Fertig` steht, hat **jeder Screen einen
|
||||
`design/`-Deep-Link** (Team `f023064a5ca0`) im Tracker + Screen-Frontmatter, das Paket einen `flow/`-Link.
|
||||
Diese sind die Vorlage für die Interim-UI (annähern) und später die Quelle des finalen Nachzugs.
|
||||
- **Zustände-Tabelle** je Screen (`Zustand | Beschreibung | Umsetzung` mit Kanon Leer/Laden/Fehler/Keine
|
||||
Berechtigung, Umsetzung-Enum Bauen/Wiederverwenden/Nur dokumentieren/Später/Raus) übernehmen, damit klar
|
||||
ist, was Interim baut vs. dokumentiert.
|
||||
|
||||
**Nachzug-Trigger:** Wenn Davids Handoff kommt (neue/aktualisierte Datei in
|
||||
`../HiveGrid-Wireframes/Tickets/Fertige Tickets/D-*.md` + Paket-Status im Tracker auf `Fertig`), zieht der
|
||||
Dev die `design/`-Screens nach (siehe Refactor-Ablauf im Playbook), setzt den `🎨 design-nachzug`-Tag auf
|
||||
erledigt und zieht die Visual-Baseline neu.
|
||||
|
||||
---
|
||||
|
||||
## 12. Checkliste „David-nah" (pro Interim-Screen)
|
||||
|
||||
- [ ] Aus `~/ui`-Components + Layout gebaut, VM als Prop, kein handgeschriebenes Tailwind für Spacing/Höhen.
|
||||
- [ ] Page 24 · Card/Surface 16 · Grid-Gap 16 · Section-Gap 24 · Field-Gap 16 · Text-Rhythmus (0/24/0/12).
|
||||
- [ ] Controls: Medium(36) Default, Radius small(4) an Inputs; Button/Table-Dichte aus der Komponente.
|
||||
- [ ] Modale als echter `Dialog` (`DialogLayout → Dialog → Dialog.Content`), Größe XS420/S560/M720/L1200, Padding 16/32, Footer rechts.
|
||||
- [ ] Empty/Loading/Permission als inline State-Komponenten (Permission-Denied = 404), keine Extra-Screens.
|
||||
- [ ] Banner/Toast/Modal/Inline/Screen nach Hierarchie (§8) gewählt.
|
||||
- [ ] Copy-kritische Texte nicht erfunden — dokumentierte Copy oder interim-markierter Platzhalter.
|
||||
- [ ] Screen-IDs/Namen an Davids Tracker angelehnt; `D-*`-Bezug im Interim-UI-Vermerk notiert.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Interim-UI-Playbook — Vorschau-Screens aus `~/ui` vor Davids Delta
|
||||
|
||||
**Stand:** 2026-07-05 · **Verbindliche Grundlage:** `docs/ARCHITECTURE-DECISIONS.md` **AD-28**
|
||||
**Verwandt:** `.planning/INTERIM-UI-DESIGN-STANDARDS.md` (Davids Layout-/Komponenten-/State-Werte) ·
|
||||
`DESIGNER-FIRST-PLAYBOOK.md` · `docs/INTEGRATION-CONTRACTS.md` · `ticket-docs/README.md`
|
||||
|
||||
## Warum dieses Playbook
|
||||
|
||||
Viele Dev-Tickets (R-P8/9/11/12, R-P2, AK-P3/4/14/16/17/19/20/21, v2: VR-P22b/VR-MCP) sind
|
||||
serverseitig fertig, bleiben aber **On Hold**, weil die finale Optik an „Davids Delta" im
|
||||
jeweiligen `D-*`-Screen-Ticket hängt — und diese Screens dauern noch. Die Component-Basis liegt
|
||||
aber **schon im Repo**: `apps/web/app/ui/` (Alias `~/ui/*`) enthält 79 gesyncte Subframe-Components
|
||||
+ die Layouts `DefaultPageLayout`/`DrawerLayout`/`DialogLayout` (gepflegt über `.subframe/sync.json`).
|
||||
|
||||
Statt auf David zu warten, baut der Dev **jetzt** aus denselben `~/ui`-Components eine **Interim-UI**
|
||||
(Vorschau-Screen), stellt das Ticket damit fertig, testet es und schiebt es auf **Done**. Der spätere
|
||||
Tausch gegen Davids finalen Subframe-Screen ist ein **getrackter Nachzug** hinter der stabilen
|
||||
VM/Loader-Grenze — ein Component-Swap, kein Rewrite (Hot-Swap-Gedanke, M015/M017).
|
||||
|
||||
> **Begriff:** „Interim-UI" / „Interim-Screen". Nicht verwechseln mit `packages/ui-mock` — das ist
|
||||
> der M017-Hot-Swap-Drop-in (bewusst hässliches Alt-Markup zum Beweis der Austauschbarkeit), etwas
|
||||
> völlig anderes.
|
||||
|
||||
## Die goldene Regel (macht „später wenig Arbeit" real)
|
||||
|
||||
Ein Interim-Screen **muss**:
|
||||
1. aus `~/ui`-Components + einem `~/ui/layouts/*` komponiert sein — **nie** handgeschriebenes
|
||||
Tailwind-Markup (Negativ-Beispiel: `apps/web/app/routes/app.$tenant/tasks._index.tsx`, das eine
|
||||
handgeschriebene `<table>` rendert und „Subframe polish follows David's D-P4 delta" trägt);
|
||||
2. seine Daten aus dem bestehenden **View-Model** (`packages/view-models/**`) beziehen, das als Prop
|
||||
hereingereicht wird;
|
||||
3. alle statischen Texte über i18n-Keys (`t()`) auflösen.
|
||||
|
||||
Nur so ist Davids Delta hinterher hauptsächlich Anordnung/Feinschliff, und der Swap bleibt billig.
|
||||
Wird Interim-UI als handgeschriebenes Tailwind gebaut, ist der Nachzug ein Neubau — das verfehlt den
|
||||
ganzen Zweck und ist ein PR-Block wie ein Anti-Listen-Verstoß.
|
||||
|
||||
**Nach Davids Standards bauen (`~/ui`-Defaults + Design-Werte):** Damit die Interim-UI wirklich nah an
|
||||
Davids kommende Pages kommt, folgt der Screen denselben Layout-/Komponenten-/State-Standards, nach denen
|
||||
David selbst baut — festgehalten (mit exakten Werten: Page-Padding 24, Card 16, Dialog-Größen, Control-Höhen,
|
||||
State-Regeln, Screen-Naming) in **`.planning/INTERIM-UI-DESIGN-STANDARDS.md`** (In-Repo-Spiegel von Davids
|
||||
Vault `../HiveGrid-Wireframes/`). Kernpunkt: **`~/ui`-Component-Defaults sind führend** — Höhen/Paddings/
|
||||
Dialog-Struktur nicht mit Tailwind nachbauen, wenn die Komponente/Variante sie schon abbildet. Modale immer
|
||||
als echter `Dialog` (`DialogLayout → Dialog → Dialog.Content`), nie als Card-Fake. Diese Doc **vor dem
|
||||
Screen-Bau lesen**.
|
||||
|
||||
## Wo der Interim-Screen lebt
|
||||
|
||||
```
|
||||
apps/web/app/components/screens/<domain>/<Screen>Page.tsx
|
||||
└─ export function <Screen>Page({ vm, actions }: { vm: <Screen>VM; actions: <Screen>Actions }) { … }
|
||||
komponiert ~/ui-Components + DefaultPageLayout, rendert vm.data, ruft actions.onX
|
||||
```
|
||||
|
||||
Die Route bleibt beim etablierten Muster und rendert nur:
|
||||
|
||||
```tsx
|
||||
// apps/web/app/routes/app.$tenant/<modul>._index.tsx
|
||||
export default function XRoute() {
|
||||
const { vm } = useLoaderData<LoaderData>();
|
||||
return <XPage vm={vm} actions={{ onEdit, onArchive }} />;
|
||||
}
|
||||
```
|
||||
|
||||
Das ist genau das AGENTS.md-Zielmuster `<XPage vm={} actions={} />` — die Interim-UI füllt die heute
|
||||
fehlende „mittlere Komposition" zwischen VM und `~/ui`-Primitives.
|
||||
|
||||
### Layer-Regeln bleiben unangetastet
|
||||
Die drei Anti-Patterns (AGENTS.md) gelten weiter:
|
||||
- kein `@prisma/client`/`~/db` in `packages/ui-app/**`;
|
||||
- kein `packages/ui-subframe`-Direktimport in `app/routes/**`;
|
||||
- kein `useLoaderData` in `packages/ui-app/**` — VM wird als Prop injiziert.
|
||||
|
||||
`~/ui`-Direktimport in `apps/web/app/components/**` bzw. `app/routes/**` ist **erlaubt** — er verletzt
|
||||
kein Anti-Pattern (die verbieten nur `packages/ui-subframe`-Direktimporte). Wer den formalen
|
||||
ui-app/ui-subframe-Adapterpfad nutzen will, kann das; für Interim-Screens ist die direkte
|
||||
`~/ui`-Komposition der pragmatische, schnellere Weg.
|
||||
|
||||
## Done-Semantik (AD-28)
|
||||
|
||||
Ein UI-lastiges Ticket ist **Done-fähig**, sobald:
|
||||
- **Dev-DoD 100 %** (alle nicht-Optik-Punkte grün) **und**
|
||||
- **Interim-Screen** aus `~/ui` steht (goldene Regel erfüllt) **und**
|
||||
- **Tests grün** (siehe DoD-Mapping unten).
|
||||
|
||||
Die **finale Subframe-Optik** ist dann **kein Done-Blocker** mehr, sondern ein **getrackter Nachzug**.
|
||||
`/planning` bleibt die **Single Source of Truth** — der Nachzug wird zuerst in der Ticket-Doc markiert,
|
||||
Plane spiegelt nur:
|
||||
- **`.planning/ticket-docs/<ID>.md` (führend):** der standardisierte **`🎨 design-nachzug`-Tag** direkt
|
||||
unter der Header-Tabelle (Format unten) **plus** der ausführliche Interim-UI-Vermerk in der Sektion
|
||||
**„Dev-Nachzug (nach Davids `D-*`-Delta)"** (Muster unten),
|
||||
- **Plane-Label `design-nachzug` (spiegelt die Doc):** nur für Board-Queries; wird aus dem Doc-Tag
|
||||
abgeleitet, nie umgekehrt,
|
||||
- die Interim-Visual-Baseline ist als „interim" markiert und wird beim Nachzug neu gezogen.
|
||||
|
||||
### Header-Tag in der Ticket-Doc (Single Source of Truth)
|
||||
Direkt unter die Header-Tabelle der Ticket-Doc, damit `/planning` per `grep` autoritativ auswertbar ist
|
||||
(`grep -rl "design-nachzug: offen" .planning/ticket-docs/`):
|
||||
|
||||
```md
|
||||
**🎨 design-nachzug: offen** (seit YYYY-MM-DD) — Interim-UI aus `~/ui` steht; finale Subframe-Optik aus `D-<XXX>` ausstehend.
|
||||
```
|
||||
|
||||
Beim David-Nachzug wird der Tag auf erledigt gesetzt (Historie bleibt in der Doc):
|
||||
|
||||
```md
|
||||
**🎨 design-nachzug: erledigt** (YYYY-MM-DD) — finale `D-<XXX>`-Optik integriert, Visual-Baseline neu gezogen.
|
||||
```
|
||||
|
||||
Done setzen weiterhin nur **mit expliziter Freigabe** und nach dem Abschluss-Audit (`plan-ticket` §8).
|
||||
|
||||
## DoD-Mapping — was Interim-UI erfüllt vs. was Nachzug bleibt
|
||||
|
||||
| DoD | Punkt | Mit Interim-UI |
|
||||
|---|---|---|
|
||||
| B 6–10 | Sidebar · Permission-Gate · Route · Breadcrumbs · i18n | **jetzt erfüllbar** |
|
||||
| B 11/12 | Mobile-Tab-Bar · Customer-Portal | nur „falls relevant"; bei PWA-only (AD-17) i. d. R. Nachzug/deferred |
|
||||
| D 18 | VM-Unit-Test | **jetzt erfüllbar** (reine Funktion) |
|
||||
| D 19 | Smoke-Test `e2e/smoke/<modul>.spec.ts` | **jetzt erfüllbar** gegen Interim-Screen |
|
||||
| D 20 | Hot-Swap-Drill (`packages/ui-mock`) | **jetzt erfüllbar** |
|
||||
| D 21 | Visual-Regression (M016) | gegen **Interim-Baseline** erfüllbar; **Re-Baseline-Pflicht** beim David-Nachzug |
|
||||
| — | finale Subframe-Optik | **Nachzug** (Label `design-nachzug`) |
|
||||
|
||||
## Refactor-Ablauf, wenn Davids Delta landet
|
||||
|
||||
**Davids Delta erscheint als:** neue/aktualisierte Datei in `../HiveGrid-Wireframes/Tickets/Fertige Tickets/D-*.md`
|
||||
+ Paket-Status im `Screen Paket Tracker` auf `Fertig` + `design/`-Subframe-Links je Screen (Team `f023064a5ca0`).
|
||||
|
||||
1. VM/Loader/Route **unverändert** lassen.
|
||||
2. Internals von `<XPage>` gegen Davids Subframe-Screen tauschen (gleiche `~/ui`-Components → meist
|
||||
Umstellen/Neuanordnen, kein neuer Datenpfad).
|
||||
3. Visual-Baseline neu ziehen (Interim-Snapshot ersetzen), Smoke/Hot-Swap erneut grün.
|
||||
4. Plane-Label `design-nachzug` entfernen; Umsetzungs-Notiz + „Dev-Nachzug"-Sektion der Ticket-Doc
|
||||
mit Datum + Proof ergänzen.
|
||||
|
||||
## Pro-Ticket-Checkliste (Interim-UI)
|
||||
|
||||
- [ ] Interim-`<XPage>` in `apps/web/app/components/screens/<domain>/` aus `~/ui` + Layout, VM als Prop.
|
||||
- [ ] Route rendert `<XPage vm={} actions={} />`; kein handgeschriebenes Tailwind.
|
||||
- [ ] **Davids Design-Standards befolgt** (`.planning/INTERIM-UI-DESIGN-STANDARDS.md`): Spacing/Padding/
|
||||
Dialog-Struktur/Control-Höhen/States aus `~/ui`-Defaults; Screen-IDs/Naming an Davids Tracker angelehnt.
|
||||
- [ ] Reachability B 6–10 verdrahtet (Sidebar/Permission/Route/Breadcrumb/i18n).
|
||||
- [ ] Tests: VM-Unit (18) · Smoke (19) · Hot-Swap (20) · Visual-Regression gegen Interim-Baseline (21).
|
||||
- [ ] Ticket-Doc (führend): `🎨 design-nachzug: offen`-Tag unter der Header-Tabelle **und** Interim-UI-Vermerk in der „Dev-Nachzug"-Sektion (Muster unten).
|
||||
- [ ] Plane-Label `design-nachzug` gesetzt (spiegelt den Doc-Tag).
|
||||
- [ ] Done nur bei Dev-DoD 100 % + Interim-UI + Tests grün + Freigabe.
|
||||
|
||||
### Muster: Interim-UI-Vermerk in „Dev-Nachzug (nach Davids `D-*`-Delta)"
|
||||
|
||||
```md
|
||||
**Interim-UI (Vorschau, Stand YYYY-MM-DD):** Screen aus `~/ui` gebaut —
|
||||
`apps/web/app/components/screens/<domain>/<Screen>Page.tsx`. Proof: Smoke `e2e/smoke/<modul>.spec.ts`,
|
||||
Hot-Swap-Drill grün, Interim-Visual-Baseline `<pfad>` (als interim markiert).
|
||||
Refactor-Nachzug offen: finale Subframe-Optik aus `D-<XXX>` (Plane-Label `design-nachzug`).
|
||||
```
|
||||
|
||||
## Nachzug-Tracking
|
||||
|
||||
- **`/planning` ist führend:** Der `🎨 design-nachzug`-Tag in `.planning/ticket-docs/<ID>.md` ist die
|
||||
autoritative Quelle. Offene Nachzüge findet man per `grep -rl "design-nachzug: offen" .planning/ticket-docs/` —
|
||||
unabhängig von Plane und ohne API-Zugang.
|
||||
- **Plane spiegelt:** Das Plane-Label **`design-nachzug`** (anlegbar über `.planning/plane-sync/scrub_plane.py`
|
||||
oder manuell via `POST /labels/`) wird aus dem Doc-Tag abgeleitet und dient nur der Board-Sicht — nie
|
||||
umgekehrt. Divergiert Plane vom Doc, gewinnt das Doc.
|
||||
- Es gibt **kein** Skript, das Plane-Kommentare postet. Kommentar/Status weiter manuell via API
|
||||
(`POST /issues/{iid}/comments/`, Identität vorab `GET users/me`); Done nur mit Freigabe.
|
||||
@@ -32,7 +32,7 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
|
||||
## Phase 4: Files & Tasks
|
||||
**Specs:** `_bmad-output/specs/M23-files.md`, `M26-tasks.md`
|
||||
**Milestone-PLANs (7):** M052-datei-upload, M053-speicher-backend-scaleway, M054-datei-permissions-sharing-versionierung, M055-datei-suche-tagging-archivierung, M071-aufgaben-crud-zuweisung, M072-aufgaben-listen-pro-modul, M073-aufgaben-erinnerungen-push-email
|
||||
**Milestone-PLANs (7):** M052-datei-upload, M053-speicher-backend-hetzner, M054-datei-permissions-sharing-versionierung, M055-datei-suche-tagging-archivierung, M071-aufgaben-crud-zuweisung, M072-aufgaben-listen-pro-modul, M073-aufgaben-erinnerungen-push-email
|
||||
> M074-aufgaben-sync-erp → Phase 10 verschoben (2026-06-10): braucht Apps-Engine (P5) UND KWP-Sync-Foundation (P10) — ist inhaltlich eine KWP-Integration. Task-Entity-Verknüpfung zu CRM-Entitäten (M071 ↔ M042): Task-Kern hier, Entity-Links werden in Phase 6 nachverdrahtet.
|
||||
> Scope-Schnitt 2026-06-12 (Boilerplate-Recheck): **M054 gedünnt** — Versionierung, Office-/Video-Previews, Resume-Upload → v2; v1 = Ersetzen + Bild-/PDF-Preview + **HEIC→JPG-Konvertierung (Pflicht — iPhone-Fotos der Monteure!)**; Permissions/Sharing bleiben. **M055 gedünnt** — OCR/Inhalts-Volltext → v2; Dateiname+Tags via Meilisearch + Archiv-Flag bleiben. Boilerplate liefert S3-Adapter/Signed-URLs/ClamAV (`packages/storage/`; ClamAV läuft zentral, Infra-ADR-014).
|
||||
> ⚠ **Infra-Wechsel 12.06. (M053):** Speicher-Backend ist **Hetzner S3-kompatibles Object Storage** (per-Product-Bucket im Hetzner-Projekt `HiveGrid`, Infra-ADR-009) — NICHT mehr Scaleway (M053-PLAN-Name ist historisch). S3-API identisch, nur Endpoint/Credentials.
|
||||
@@ -48,13 +48,13 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
## Phase 7: Lead-Inbox
|
||||
**Specs:** `_bmad-output/specs/M02-lead-inbox.md`
|
||||
**Milestone-PLANs (4):** M047-lead-inbox-quellen-adapter, M048-lead-routing-pro-quelle, M050-spam-duplikat-erkennung, M051-lead-status-pipeline-verlust-gruende
|
||||
> M049-auto-verkaufsfall-anlage → Phase 8 verschoben (2026-06-10, Dependency-Inversion): braucht die Deal-Entity (M060) — Deal-Verhalten gehört in die Deal-Phase. P7 liefert den Extension-Point (Lead-Status `qualified` + Event), P8 verdrahtet die Auto-Anlage.
|
||||
> M049-auto-projekt-anlage → Phase 8 verschoben (2026-06-10, Dependency-Inversion): braucht den `Projekt`-Kern (M060). P7 liefert den Extension-Point (Lead-Status `qualified` + Event), P8 verdrahtet die automatische Projekt-Anlage in der Sales-Pipeline.
|
||||
|
||||
## Phase 8: Projekt & Pipelines (Kern)
|
||||
**Specs:** `_bmad-output/specs/M03-sales-pipeline.md` *(Bezeichner historisch; Inhalt projekt-zentrisch re-gemappt — Single Source: `.planning/ENTITY-MODEL.md`, AD-20)*
|
||||
**Milestone-PLANs (7):** M049-auto-verkaufsfall-anlage, M059-sales-pipeline-konfiguration, M060-verkaufsfall-crud, M061-sales-kanban-ui, M062-verlust-gewinn-gruende, M063-auto-auftragsprojekt-setter-closer, M064-pipeline-reporting-kwp-sync
|
||||
> 🔴 **Pivot-Re-Map (AD-20, R-REFIT 16.06.):** Die Milestone-NAMEN bleiben als Datei-Referenz, ihr **Inhalt ist projekt-zentrisch re-gemappt** — „Verkaufsfall"/„Deal" = ein `Projekt`-Kern in der Sales-Pipeline-Sicht (kein eigenes Deal-Objekt), `M060-verkaufsfall-crud` = Projekt-CRUD; Pipeline-Engine baut zusätzlich PipelineAccess/-FieldConfig/-FieldVisibility (Field-Level-Rechte v1). Maßgeblich ist ENTITY-MODEL.md, nicht der historische Bezeichner.
|
||||
> M049 (aus Phase 7, 2026-06-10): Auto-Verkaufsfall-Anlage bei Lead-Qualifizierung — hier, weil der `Projekt`-Kern (M060) hier entsteht.
|
||||
**Milestone-PLANs (7):** M049-auto-projekt-anlage, M059-sales-pipeline-konfiguration, M060-projekt-crud, M061-sales-kanban-ui, M062-verlust-gewinn-gruende, M063-auto-auftragsprojekt-setter-closer, M064-pipeline-reporting-kwp-sync
|
||||
> 🔴 **Pivot-Re-Map (AD-20, R-REFIT 16.06.):** Die Milestone-Inhalte sind projekt-zentrisch re-gemappt — „Verkaufsfall"/„Deal" = ein `Projekt`-Kern in der Sales-Pipeline-Sicht (kein eigenes Deal-Objekt), `M060-projekt-crud` = Projekt-CRUD; Pipeline-Engine baut zusätzlich PipelineAccess/-FieldConfig/-FieldVisibility (Field-Level-Rechte v1). Maßgeblich ist ENTITY-MODEL.md, nicht der historische Bezeichner in älteren Specs.
|
||||
> M049 (aus Phase 7, 2026-06-10): Auto-Projekt-Anlage bei Lead-Qualifizierung — hier, weil der `Projekt`-Kern (M060) hier entsteht.
|
||||
|
||||
## Phase 9: Projekt & Pipelines (Auftrag)
|
||||
**Specs:** `_bmad-output/specs/M04-auftragspipeline.md` *(Bezeichner historisch; Inhalt projekt-zentrisch re-gemappt — Single Source: `.planning/ENTITY-MODEL.md`, AD-20)*
|
||||
@@ -71,7 +71,7 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
## Phase 11: Globale Suche & Dashboard
|
||||
**Specs:** `_bmad-output/specs/M24-globale-suche.md`, `M20-dashboard.md`
|
||||
**Milestone-PLANs (5):** M056-such-index, M057-permission-aware-search, M058-such-ui-saved-searches, M147-dashboard-layout-widget-engine, M148-dashboard-standard-widgets
|
||||
> Scope-Schnitt 2026-06-11: M149-custom-widgets + M150-export-report → v2. Suche v1 = **Keyword-only** (Meilisearch); pgvector/Hybrid-RRF → v2 mit HiveMind (AD-5 sequenziert).
|
||||
> Scope-Schnitt 2026-06-11/AD-26: M149-custom-widgets + M150-export-report → v2. Suche = **Keyword-only** (Meilisearch); pgvector/Hybrid-RRF/HiveMind entfällt bzw. ruht als v3-Kandidat.
|
||||
> Scope-Schnitt 2026-06-12 (Boilerplate-Recheck): **M147 gedünnt** — 3 feste Rollen-Layouts (GF/Büro/Monteur) statt Drag&Drop-Grid (react-grid-layout wäre Neueinkauf); Layout-Persistenz pro User → v2. Widget-Registry bleibt voll (Inversion `M064→M147` registriert Sales-Widgets darüber).
|
||||
|
||||
## Phase 12: Automation-Engine
|
||||
@@ -96,7 +96,7 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
|
||||
## Phase 16: Kalender, Booking, Forms & Signatur
|
||||
**Specs:** `_bmad-output/specs/M25-calendar-sync.md`, `M06-booking.md`, `M05-form-builder.md`, `M07-angebot-signatur.md`
|
||||
**Milestone-PLANs (15):** M118-calendar-google-adapter, M119-calendar-termin-sync, M120-calendar-display-modul-verknuepfung, M121-form-editor, M122-form-veroeffentlichung, M123-form-submission-eingang, M124-form-conditional-display, M125-form-rate-limit-captcha-dsgvo, M126-booking-time-slot, M127-booking-verfuegbarkeit, M128-booking-bestaetigung, M129-booking-stornierung, M130-angebot-erstellung, M131-angebot-pandadoc-docusign, M132-angebot-status-tracking
|
||||
**Milestone-PLANs (15):** M118-calendar-google-adapter, M119-calendar-termin-sync, M120-calendar-display-modul-verknuepfung, M121-form-editor, M122-form-veroeffentlichung, M123-form-submission-eingang, M124-form-conditional-display, M125-form-rate-limit-captcha-dsgvo, M126-booking-time-slot, M127-booking-verfuegbarkeit, M128-booking-bestaetigung, M129-booking-stornierung, M130-angebot-erstellung, M131-angebot-self-hosted-e-signatur, M132-angebot-status-tracking
|
||||
> Scope-Schnitt 2026-06-12: **M122 gedünnt** — personalisierte Links (Tracking/Ablauf/Nutzungs-Limit) + JS-Snippet → v2; öffentliche URL + QR-Code + iFrame-Embed bleiben (SHK-Kernfall). **M129 bleibt explizit VOLL** (User-Entscheidung: Verschiebung/Re-Assignment/No-Show in v1).
|
||||
|
||||
## Phase 17: HR-Hub
|
||||
@@ -112,14 +112,14 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
## Phase 19: Plattform-Admin
|
||||
**Specs:** `_bmad-output/specs/M37-plattform-admin.md`
|
||||
**Milestone-PLANs (7):** M031-feature-flag-schalter-lifecycle, M032-feature-flag-filter-editor, M034-plattform-admin-auth, M035-plattform-admin-dashboard, M036-plattform-admin-mandanten-verwaltung, M037-plattform-admin-stellvertreter-anmeldung, M041-plattform-admin-system-health-audit-support
|
||||
> Scope-Schnitt 2026-06-11: M033 (Flag-Massen-Statistik) + M040 (Plattform-Lead-Pipeline) → v2. **Neu in P19-Scope:** Tenant-Abo-Zusammenstellung durch Admin (Abo-Baukasten-Mechanik: Feature-Konnektoren pro Tenant schalten + Stripe-Positionen; Self-Service-UI → v2).
|
||||
> Scope-Schnitt 2026-06-11: M033 (Flag-Massen-Statistik) + M040 (Plattform-Lead-Pipeline) → v2. **Neu in P19-Scope:** Tenant-Abo-Zusammenstellung durch Admin (Abo-Baukasten-Mechanik: Feature-Konnektoren pro Tenant schalten + Legacy-Provider-Positionen; Self-Service-UI → v2).
|
||||
> Scope-Schnitt 2026-06-12 (Boilerplate-Recheck): **M038 + M039 (getrennte AdminUser-Identität + AdminRBAC-Parallelwelt) → v2** — v1 nutzt das Boilerplate-Modell (AdminUser als User-Extension + MFA-Step-up-Gate, `admin-mfa-gate.server.ts`). **Bindende Auflagen für M034:** (a) TOTP-Pflicht hart erzwingen, (b) Admin-User bekommen **keinen Tenant-Workspace** und erscheinen in keiner Mandanten-Mitgliederliste (Boilerplate-Default-Provisionierung abschalten!), (c) Host-Routing `admin.hivegrid.de` → Admin-Routen (Boilerplate-Code routet `/admin`-Pfad), (d) **Cookie-Domain-Trennung** App↔Admin. **M035 gedünnt:** Boilerplate-KPIs (Tenants/User/MRR); Churn/ARPU/Health-Kacheln → v2 (bei 1 Launch-Kunden sinnfrei).
|
||||
> M031–M033 (Flag-Admin-UI, 2026-06-10 aus Phase 3 hierher verschoben): Vorbedingung M034 (Admin-Auth) liegt hier; die Flag-**Engine** (M006, `tenantHasFlag()`) liegt in Phase 1 und ist ab dort von allen Phasen nutzbar.
|
||||
|
||||
## Phase 20: Billing, Onboarding & Branding
|
||||
**Specs:** `_bmad-output/specs/M28-subscriptions-billing.md`, `M29-onboarding.md`, `M31-branding.md`
|
||||
**Milestone-PLANs (8):** M030-hell-dunkel-modus, M153-subscriptions-stripe, M154-subscriptions-plan-seat-modell, M155-subscriptions-trial-readonly, M156-subscriptions-metering-pay-as-you-go, M158-onboarding-shk-provisionierung, M159-onboarding-branchen-templates, M160-onboarding-test-daten
|
||||
> ⚠ **M154-Recut bindend (2026-06-11):** Seat-Typ-Modell (Büro/Monteur) ist NICHT zu bauen — ersetzt durch **Abo-Baukasten**: Basis-Preis pro Nutzer + Feature-Module als zubuchbare Stripe-Positionen via Entitlement-Schicht (Flag-Engine `plan-tier`-Targeting × `packages/entitlements`). **Source-Spec:** `~/Documents/GitHub/archive/hivegrid-cloud/app/config/pricing-plans.ts` (Legacy D006: Core/Swarm/Colony, 14 Modul-Gates, 6 Quota-Dimensionen) — Struktur portieren, Preise/Tier-Zuschnitt = Kostenmatrix (Partner, AD-15). M156: Zähler+Drossel v1, Aufschlags-Abrechnung → v2. M157 (Wizard-UI) → v2 — Signup provisioniert SHK-Seed automatisch. M159: v1 nur SHK.
|
||||
**Milestone-PLANs (8):** M030-pwa-theme-color, M153-subscriptions-billing-provider, M154-subscriptions-plan-seat-modell, M155-subscriptions-trial-readonly, M156-subscriptions-metering-pay-as-you-go, M158-onboarding-shk-provisionierung, M159-onboarding-branchen-templates, M160-onboarding-demo-daten
|
||||
> ⚠ **M154-Recut bindend (2026-06-11):** Seat-Typ-Modell (Büro/Monteur) ist NICHT zu bauen — ersetzt durch **Abo-Baukasten**: Basis-Preis pro Nutzer + Feature-Module als zubuchbare Legacy-Provider-Positionen via Entitlement-Schicht (Flag-Engine `plan-tier`-Targeting × `packages/entitlements`). **Source-Spec:** `~/Documents/GitHub/archive/hivegrid-cloud/app/config/pricing-plans.ts` (Legacy D006: Core/Swarm/Colony, 14 Modul-Gates, 6 Quota-Dimensionen) — Struktur portieren, Preise/Tier-Zuschnitt = Kostenmatrix (Partner, AD-15). M156: Zähler+Drossel v1, Aufschlags-Abrechnung → v2. M157 (Wizard-UI) → v2 — Signup provisioniert SHK-Seed automatisch. M159: v1 nur SHK.
|
||||
> Scope-Schnitt 2026-06-12: **M030 gedünnt** — nur Hell/Dunkel-Toggle, **kein System-Modus** (User-Entscheidung); nativer Status-Bar-Teil entfällt durch Mobile-Cut (Expo/Capacitor verworfen — PWA-only, AD-17; `next-themes` in Boilerplate-Deps). **M160 gedünnt** — KI-Walkthrough → v2 (gehört zum Wizard M157 = v2); Demo-Daten-Seed bleibt als Settings-Knopf, einfaches Löschen statt Multi-Step-Bulk; Mitarbeiter-Einladung läuft über M024.
|
||||
|
||||
## Phase 21: DSGVO & Launch-Reife
|
||||
@@ -146,7 +146,7 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
### Phase 24: Self-Service-Billing & Onboarding-Wizard
|
||||
**Specs:** `_bmad-output/specs/M28-subscriptions-billing.md` (Block P), `M29-onboarding.md`
|
||||
**Milestone-PLANs (1+):** M157 + M28-Self-Service-Delta
|
||||
**v1-Andockpunkte:** Stripe/Checkout/Webhook + SubscriptionFeature (P20), TenantUserInvitation (P2), deklaratives Branchen-Template (geteilt P26/P30). Retro: KI-Coach aus P25.
|
||||
**v1-Andockpunkte:** Legacy-Provider/Checkout/Webhook + SubscriptionFeature (P20), TenantUserInvitation (P2), deklaratives Branchen-Template (geteilt P26/P30). Retro: KI-Coach aus P25.
|
||||
|
||||
### Phase 25: ~~KI-Tiefe — HiveMind/RAG~~ → MCP-Server ([[AD-26]])
|
||||
> ⛔ **KI→MCP-Pivot ([[AD-26]]): Phase 25 zurückgestellt → ersetzt durch MCP-Server.** Kein M0xx-Milestone (v2-Neuschnitt); verbindliche Bau-Spec = **[[VR-MCP]]** (Backend) / **[[VD-MCP]]** (Screens). HiveMind/RAG/Whisper sind v3-Kandidat.
|
||||
@@ -241,7 +241,7 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
| Owner | Retro-Integrations-Aufgaben (beim Planen dieser Phase MIT einplanen) |
|
||||
|---|---|
|
||||
| **P6 CRM** | Task↔CRM-Entity-Verknüpfung verdrahten (`M071→M042`; P4 liefert polymorphe EntityLink) |
|
||||
| **P8 Sales** | Auto-Verkaufsfall aus qualifiziertem Lead (M049, hierher verschoben); SHK-Pipelines + Gründe selbst seeden (`M059→M158`, `M062→M158`); Deal-Anlage-Option ins Lead-Promotion-Modal (P6) verdrahten |
|
||||
| **P8 Sales** | Auto-Projekt aus qualifiziertem Lead (M049, hierher verschoben); SHK-Pipelines + Gründe selbst seeden (`M059→M158`, `M062→M158`); Lead→Projekt-Verdrahtung konsumiert das P7-Event |
|
||||
| **P9 Projekte** | Sales-Won-Handoff konsumieren (`M063→M065`; P8 liefert deal.won-Event); Abnahme-Defaults selbst seeden (`M069→M158`) |
|
||||
| **P10 KWP** *(→ v2, erstes v2-Modul = Phase 32 / ROADMAP-V2 v2-P5)* | Retro-Integrationen wandern mit: KWP-Sync in Auftrag-Archiv, Abnahme-Drift-Schutz, Material-Listen, Task-ERP-Sync (M074) — Phase 32 (= ROADMAP-V2 v2-P5, ex-P10) integriert sich rückwirkend; v1 liefert die Extension-Points (Auftrag/Abnahme/Material) |
|
||||
| **P11 Suche/Dashboard** | Hybrid-Suche-Upgrade für Audit-Viewer (`M151→M057`), Datei-Suche (`M055→M056`), CRM-Segmente (`M046→M056`), Kanban-Filter (`M061→M057`); Sales-Reporting-Widgets (`M064→M147`; P8 liefert Aggregat-APIs); Dashboard-Default-Layout selbst seeden (`M148→M158`) |
|
||||
@@ -250,7 +250,7 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
| **P14 Email/Marketing** | Task-Liste im Marketing-Modul (`M072→M104`); Channel-Bedarf von M068 prüfen (`M068→M104` — voraussichtlich durch P1-notify()-Kern gedeckt); Email-Templates selbst seeden (`M100→M158`) |
|
||||
| **P16 Kalender/Booking/Forms** | Kalender in Plantafel (`M067→M119`, `M067→M129`); Kalender-Termin aus Won-Deal (`M063→M119`); Form-Templates für Abnahme (`M069→M121`); Task-Liste im Forms-Modul (`M072→M121`); Form-Templates selbst seeden (`M125→M158`) |
|
||||
| **P17 HR** | Abwesenheiten in Plantafel anzeigen (`M067→M135`) |
|
||||
| **P20 Billing/Branding** | Stripe ersetzt Subscription-Stub (`M026→M153`, `M036→M153`, `M041→M153`); ~~AI-Credits-Kauf (`M088→M153`, `M088→M156`)~~ → **entfällt ([[AD-26]]: kein LLM-Gateway → keine AI-Credits; M088 gelöscht)** |
|
||||
| **P20 Billing/Branding** | Legacy-Provider ersetzt Subscription-Stub (`M026→M153`, `M036→M153`, `M041→M153`); ~~AI-Credits-Kauf (`M088→M153`, `M088→M156`)~~ → **entfällt ([[AD-26]]: kein LLM-Gateway → keine AI-Credits; M088 gelöscht)** |
|
||||
| **v2 (deferred Owner)** | Diese Retro-Integrationen wandern mit ihren Owner-Modulen nach v2: Push-Kanal-Aktivierung + Settings-Erweiterung (Mobile), Theme→Portal, Portal-DSGVO-Löschung, Help-Desk→Admin-Inbox, Abwesenheiten→Plantafel bleibt v1 (HR bleibt!) |
|
||||
|
||||
### Extension-Point-Pflichten (frühe Phase MUSS liefern, damit die Inversion trägt)
|
||||
@@ -258,7 +258,7 @@ Beim Planen einer Phase liest der Planner/Researcher diese Specs + Milestone-PLA
|
||||
| Phase | Extension-Point |
|
||||
|---|---|
|
||||
| P1 | `notify()` mit Kanal-Abstraktion inkl. ruhendem Push-Kanal; Search-Registry; Flag-Engine (bereits Success Criteria) |
|
||||
| P2 | TenantSubscription-Status-Stub (manuell setzbar) — Frozen-Modus (M026) liest nur den Status, Stripe füllt ihn ab P20 |
|
||||
| P2 | TenantSubscription-Status-Stub (manuell setzbar) — Frozen-Modus (M026) liest nur den Status, Legacy-Provider füllt ihn ab P20 |
|
||||
| P3 | Notification-Settings-UI kanal-erweiterbar (Push-Sektion erscheint in P21) |
|
||||
| P4 | Polymorphe EntityLink-Tabelle (`entity_type` + `entity_id`, kein harter FK); Task-Listen-Pattern als wiederverwendbare Komponente |
|
||||
| P7 | Lead-Status-Event `lead.qualified` (Extension-Point für M049 in P8) |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
HiveGrid 2.0 ist eine Multi-Tenant-SaaS-Plattform für SHK-Handwerksbetriebe (Sanitär, Heizung, Klima). Sie bündelt CRM, Vertriebs- und Auftragspipeline, Team-Chat, visuelle KI-Automation und die Anbindung an das KWP-ERP in einem EU-gehosteten System.
|
||||
|
||||
HiveGrid ist ein Multi-Kunden-SaaS für den gesamten SHK-Markt. Die **Geerkens GmbH** (Kontakt: Noah) ist erster Kunde und Investor — Validierungspartner, nicht alleiniger Zweck. Das Produkt ersetzt dort HubSpot (CRM + Sales-Pipeline), Quiply (Team-Chat), Zapier/n8n (Automation) sowie mehrere Excel-Prozesse. Die Codebase ist ein Git-Downstream der ToolCTRL-Boilerplate (~35–45 % Abdeckung: Fundament high, Fachdomäne greenfield).
|
||||
HiveGrid ist ein Multi-Kunden-SaaS für den gesamten SHK-Markt. Die **Geerkens GmbH** (Kontakt: Noah) ist erster Kunde und Investor — Validierungspartner, nicht alleiniger Zweck. Das Produkt ersetzt dort HubSpot (CRM + Sales-Pipeline), Quiply (Team-Chat), Zapier/n8n (Automation) sowie mehrere Excel-Prozesse. Die Codebase wird als bespoke HiveGrid-Anwendung weitergeführt: historisches Fundament wird genutzt, produktfremde Altlasten werden entfernt.
|
||||
|
||||
> 🔴 **MODELL-PIVOT (2026-06-13):** CRM/Pipeline-Modell ist **Projekt-zentrisch** — ein `Projekt`-Kern, Pipelines = frei konfigurierbare Views; „Deal"/„Auftrag" sind Pipeline-Typen, keine eigenen Entitäten. Single Source: `.planning/ENTITY-MODEL.md`. ENTITY-MODEL/REQUIREMENTS/SCREEN-MAP/PHASE-SOURCES werden gegen den Anker neu gefasst (ER-Modell wird gemeinsam gebaut).
|
||||
|
||||
@@ -54,8 +54,8 @@ Vollständige Liste mit 79 v1-Requirements über 36 Module: siehe `.planning/REQ
|
||||
|
||||
- **SHK-Domäne:** Geerkens = Heizungsanlagen/Wartung/Reparatur. Entitäten: B2C/B2B-Kunden mit Anlagen-Zustand (Custom-Felder), mehrstufige Anlagen-Verkäufe, Wartungsverträge, Reparatur-Aufträge. Monteure arbeiten offline auf Baustellen (AD-17).
|
||||
- **SHK-Seeds beim Tenant-Anlegen:** 6 Standard-Rollen + Permission-Bundles, SHK-Sales-Pipelines, Custom-Felder für Heizungsanlagen, Projekt-Pipelines, Automation-Templates, Onboarding-Branchen-Zweig — deklarativ, von Noah iterativ validiert (AD-12).
|
||||
- **Boilerplate-Reuse:** Geteilte Pakete behalten `@toolctrl/*`-Namen (`packages/ui-app`, `ui-subframe`, `ui-mock`, `@toolctrl/search`, `@toolctrl/foundation`). Neue HiveGrid-Querschnittsdienste im `@hivegrid/*`-Namespace (activity, audit, notifications, entitlements, search, i18n).
|
||||
- **Phase-0-Pflichten (vor jedem Fachmodul):** (1) Boilerplate als Code-Basis einziehen — Repo ist heute Docs-only, (2) Naming-Drift beheben (AD-10), (3) EU-Sterilisation (GitHub/Google-OAuth-Reste raus), (4) M00 + 4 Querschnitts-Neubauten (logActivity, Search-Registry, Subframe-Adapter, Mass-Action-Schutz), (5) fehlende Dependencies ergänzen.
|
||||
- **Workspace-Naming:** Alle Runtime-, UI-, Config-, DB-, Mail-, Queue- und View-Model-Pakete laufen unter `@hivegrid/*`; CLI = `hivegrid-cli`; technische Produkt-Contracts nutzen `HIVEGRID_*`, `hivegrid_*`, `x-hivegrid-*` und `hivegrid.*`.
|
||||
- **Phase-0-Pflichten (vor jedem Fachmodul):** (1) Codebase lauffähig halten, (2) Naming-Drift beheben (AD-10), (3) EU-Sterilisation (GitHub/Google-OAuth-Reste raus), (4) M00 + 4 Querschnitts-Neubauten (logActivity, Search-Registry, Subframe-Adapter, Mass-Action-Schutz), (5) fehlende Dependencies ergänzen.
|
||||
- **Spec-Stil:** Alle 36 Specs sind „requirements-pur" (benötigte UI-Elemente als Liste, keine Layouts — Designer hat freie Hand) und im Status `detail-spec-draft`. Vor Modulbau: Noah-Verifizierung, Index-/Migrations-SQL, SHK-Defaults finalisieren, Subframe-UI-Designs.
|
||||
|
||||
## Constraints
|
||||
@@ -67,7 +67,7 @@ Vollständige Liste mit 79 v1-Requirements über 36 Module: siehe `.planning/REQ
|
||||
- **Sessions**: Postgres als Source-of-Truth + Redis-Cache; Cookie hält nur Session-ID (AD-4)
|
||||
- **Realtime**: WebSocket für Chat (M13), SSE für Read-Updates (M03/M22) — AD-9
|
||||
- **Tech-Stack**: React Router 7, Node 22.12.x (dev/test) / node:20 (runtime), Prisma/Postgres, Redis; **Subframe-UI ist Pflicht** für jede Route
|
||||
- **Repo/Naming**: Git-Downstream der @toolctrl-Boilerplate, upstream-Remote erhalten, `@toolctrl/*`-Naming behalten (AD-10)
|
||||
- **Repo/Naming**: Bespoke HiveGrid-Codebase; `@hivegrid/*` ohne Alt-Aliase, Estate-Slugs bleiben externe Infrastruktur (AD-10)
|
||||
- **Go-Live**: Big-Bang — erst live mit vollständigem Phase-1-Produkt (AD-19); interne Bau-Reihenfolge nach AD-11
|
||||
|
||||
## Key Decisions
|
||||
@@ -85,7 +85,7 @@ Vollständige Extraktion mit Scope/Begründung: `.planning/intel/decisions.md`.
|
||||
| AD-7 | Captcha: **mCaptcha self-hosted** (infra/ vorhanden) | ✓ locked (11.06.) |
|
||||
| AD-8 | Verschlüsselung: Per-Tenant-DEK + Envelope, Crypto-Shredding | ✓ locked |
|
||||
| AD-9 | Realtime: WebSocket (Chat) + SSE (Read-Updates) | ✓ locked |
|
||||
| AD-10 | Naming: `@toolctrl/*` behalten, Docs anpassen | ✓ locked |
|
||||
| AD-10 | Naming: `@hivegrid/*` behalten, Docs anpassen | ✓ locked |
|
||||
| AD-11 | Bau-Strategie: Hybrid (Fundament-Kern → SHK-Kernpfad → Breite) | ✓ locked |
|
||||
| AD-12 | SHK-Defaults als deklaratives Seed, Noah validiert | ✓ locked |
|
||||
| AD-13 | Go-Live-Scope: breiter Launch (Großteil der 36 Module) | ✓ locked |
|
||||
|
||||
@@ -34,7 +34,7 @@ Dies ist das **aktive Planungs-Zuhause**. Wenn du planst, steuerst oder verstehe
|
||||
- **DESIGNER-FIRST-PLAYBOOK.md** — Designer-first-Methodik + Archetypen
|
||||
|
||||
## Externe Abhängigkeiten
|
||||
- **EXTERNALS-CHECKLISTE.md** — Konten/APIs (KWP, Mistral, Mailjet, AirCall, Stripe, …)
|
||||
- **EXTERNALS-CHECKLISTE.md** — Konten/APIs (KWP, Mistral, Mailjet, AirCall, Legacy-Provider, …)
|
||||
|
||||
## Unterordner
|
||||
- **handouts/** — personalisierte Briefings (David, Akeshan, Dejan, Haris, Noah, Raffael)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Requirements: HiveGrid 2.0
|
||||
|
||||
**Defined:** 2026-06-07
|
||||
**Core Value:** Jeder SHK-Betrieb (Mandant) wickelt die komplette Wertschöpfungskette — Lead → Projekt (frei konfigurierbare Pipelines) (→ KWP-Sync ab v2) — inklusive visueller KI-Automation in einem einzigen, DSGVO-konformen EU-System ab. Wenn diese Kette nicht durchgängig funktioniert, scheitert das Produkt. *(Scope-Schnitt #12, 12.06.: KWP = erstes v2-Modul — API-Zugang des Herstellers erst ~19.07., nach v1-Launch.)*
|
||||
**Core Value:** Jeder SHK-Betrieb (Mandant) wickelt die komplette Wertschöpfungskette — Lead → Projekt (frei konfigurierbare Pipelines) (→ KWP-Sync ab v2) — inklusive visueller Automation in einem einzigen, DSGVO-konformen EU-System ab. Wenn diese Kette nicht durchgängig funktioniert, scheitert das Produkt. *(Scope-Schnitt #12, 12.06.: KWP = erstes v2-Modul — API-Zugang des Herstellers erst ~19.07., nach v1-Launch. AD-26: kein eigenes LLM/RAG/Whisper; MCP-Server statt KI-Layer.)*
|
||||
|
||||
> ✅ **MODELL-PIVOT 2026-06-13 (AD-20) — Module 1–4 nachgezogen (R-REFIT, 16.06.):** Projekt-zentrisch (ein `Projekt`-Kern, Pipelines = Views). Umgesetzt: **REQ-crm-lead-promotion entfällt → REQ-lead-projekt-konvertierung** (Lead → Projekt bei Weiterverarbeitungsfreigabe, kein Modal, E4); **Sales (Modul 3) + Projekt (Modul 4) konsolidiert** zu „Projekt & Pipelines" mit Pipeline-Feld-Config-/Rechte-Layer (PipelineAccess/-FieldConfig/-FieldVisibility); **REQ-rechnung-sub-entity (NEU, E3)** angelegt; Kontaktmodell (Kontakttyp Privatperson/Organisation, -status, -art als Relation); Abnahme bleibt v1 als Checklisten/Protokoll-Sub-Area (E2). Single Source: `.planning/ENTITY-MODEL.md`.
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
- [ ] **REQ-form-datenmodell**: Form, FormField, FormSubmission und FormAction als Datenmodell mit 12 Feld-Typen und konfigurierbaren Post-Submit-Aktionen. *(Scope-Schnitt 12.06.: FormLink — personalisierte Links mit Tracking/Ablauf/Limit — + JS-Snippet → v2; öffentliche URL, QR-Code und iFrame-Embed bleiben.)*
|
||||
- [ ] **REQ-form-editor**: Drag-&-Drop-Form-Editor mit Echtzeit-Vorschau, Conditional Logic, SHK-Form-Templates und iFrame-Embed-Code.
|
||||
- [ ] **REQ-form-actions**: Form-Actions nach Submission (Lead/Deal/Task anlegen, Webhook, Email, Automation-Trigger) mit Field-Mapping und ActionLog.
|
||||
- [ ] **REQ-form-actions**: Form-Actions nach Submission (Lead/Projekt/Task anlegen, Webhook, Email, Automation-Trigger) mit Field-Mapping und ActionLog.
|
||||
|
||||
### Modul 6 — Booking
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
|
||||
### Modul 7 — Angebot + Signatur
|
||||
|
||||
- [ ] **REQ-signatur-datenmodell**: DealSignatureDocument, DealSignatureSigner und SignatureProviderConnection für Multi-Signer-Workflows mit PandaDoc/DocuSign.
|
||||
- [ ] **REQ-signatur-provider**: PandaDoc/DocuSign-Integration via Apps-Engine (M33a) mit OAuth-Connect-Flow, Webhook-Verarbeitung und Status-Updates.
|
||||
- [ ] **REQ-signatur-datenmodell**: SignatureDocument, SignatureSigner und SignatureProviderConnection für Multi-Signer-Workflows im Projekt-/Angebots-Kontext.
|
||||
- [ ] **REQ-signatur-provider**: Self-hosted E-Signatur als Standardpfad (SES, AD-25); PandaDoc/DocuSign nur optionale BYOK-/Opt-in-Adapter via Apps-Engine (M33a) mit Connect-Flow, Webhook-Verarbeitung und Status-Updates.
|
||||
|
||||
### Modul 9 — Email-Inbox
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
- [ ] **REQ-chat-datenmodell**: TeamChat, TeamChatMessage, Threads, Reactions und TeamChatRead als Datenmodell für drei Channel-Typen (public, private, entity-linked).
|
||||
- [ ] **REQ-chat-realtime**: WebSocket-basiertes Real-Time-Messaging mit Presence, Typing-Indicator, Read-Receipts und Redis-Pub/Sub für Scale-Out.
|
||||
- [ ] **REQ-chat-features**: Link-Previews, File-Uploads, @Mentions, Chat-Verlaufs-Suche und Entity-linked Channels im Deal/Projekt-Detail.
|
||||
- [ ] **REQ-chat-features**: Link-Previews, File-Uploads, @Mentions, Chat-Verlaufs-Suche und Entity-linked Channels im Projekt-Detail.
|
||||
|
||||
### Modul 14 — HR-Hub
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
- [ ] **REQ-automation-datenmodell**: AutomationRule (JSON-Graph), AutomationExecution, AutomationStepExecution und AutomationTemplate als Datenmodell für den Flow-Editor.
|
||||
- [ ] **REQ-automation-trigger-library**: Phase-1-Trigger-Library über alle Module (CRM, Sales, Projekte, Email, Forms, Kalender, Zeit-Trigger, Webhooks).
|
||||
- [ ] **REQ-automation-action-library**: Phase-1-Action-Library über alle Module (CRM-Aktionen, Notifications, KWP-Push, HTTP-Request, Tasks, If/Else, Delay, Loop).
|
||||
- [ ] **REQ-automation-flow-editor**: Graph-basierter visueller Flow-Editor mit KI-Erstellung per natürlicher Sprache (DE), Dry-Run und 24h-Undo.
|
||||
- [ ] **REQ-automation-flow-editor**: Graph-basierter visueller Flow-Editor mit manueller Flow-Erstellung, Dry-Run und 24h-Undo. KI-/Sprach-Erstellung entfällt nach AD-26.
|
||||
- [ ] **REQ-automation-schutz**: Loop-Guard, Rate-Limit, Idempotenz-Key, Resume-Mode und Trigger-Volumen-Metering pro Tenant.
|
||||
|
||||
### Modul 18 — ~~KI-Layer~~ → MCP-Server ([[AD-26]])
|
||||
@@ -125,7 +125,7 @@
|
||||
### Modul 20 — Dashboard
|
||||
|
||||
- [ ] **REQ-dashboard-datenmodell**: Dashboard, DashboardLayout und WidgetInstance als Datenmodell mit Role-Default-Layouts (SHK-Template geseedet). *(Scope-Schnitt 12.06.: 3 feste Rollen-Layouts GF/Büro/Monteur; Drag&Drop + User-Layout-Persistenz → v2.)*
|
||||
- [ ] **REQ-dashboard-widgets**: Widget-Katalog mit Datenquellen aus Sales, Projekte, Tasks, HR und KI; Realtime-Updates via WebSocket/SSE und Aggregat-Cache.
|
||||
- [ ] **REQ-dashboard-widgets**: Widget-Katalog mit Datenquellen aus Sales, Projekte, Tasks und HR; Realtime-Updates via WebSocket/SSE und Aggregat-Cache. KI-Widgets sind Backlog/v3 nach AD-26.
|
||||
|
||||
### Modul 21 — Audit-Log
|
||||
|
||||
@@ -145,8 +145,8 @@
|
||||
|
||||
### Modul 24 — Globale Suche
|
||||
|
||||
- [ ] **REQ-suche-datenmodell**: SearchIndexEntry, RecentSearch und Outbox-Pattern für Index-Jobs nach Entity-Mutationen. *(Scope-Schnitt 11.06., AD-5 sequenziert: pgvector-Embedding-Spalte → v2 mit HiveMind; Outbox-Events sind der v1-Andock-Adapter.)* **⚠️ KI→MCP-Pivot ([[AD-26]]): Die KI/Embedding-Hälfte ruht — RAG/pgvector entfällt; das `embedding`-Feld bleibt ungenutzt. Meilisearch-Keyword-Suche bleibt unverändert.**
|
||||
- [ ] **REQ-suche-hybrid**: Keyword-Suche via Meilisearch mit permission-awarem SearchAdapter-Interface (INTEGRATION-CONTRACTS §8). *(Scope-Schnitt 11.06., AD-5 sequenziert: pgvector/Hybrid-RRF-Fusion → v2 mit HiveMind.)* **⚠️ KI→MCP-Pivot ([[AD-26]]): pgvector/Hybrid-RRF-Fusion ruht (RAG entfällt); v1+v2 = reine Meilisearch-Keyword-Suche.**
|
||||
- [ ] **REQ-suche-datenmodell**: SearchIndexEntry, RecentSearch und Outbox-Pattern für Index-Jobs nach Entity-Mutationen. AD-26: keine Embedding-/RAG-Spalte in v1/v2; Outbox-Events bleiben für Meilisearch-Indexierung und spätere MCP-Resource-Aktualität.
|
||||
- [ ] **REQ-suche-hybrid**: Keyword-Suche via Meilisearch mit permission-awarem SearchAdapter-Interface (INTEGRATION-CONTRACTS §8). AD-26: pgvector/Hybrid-RRF entfällt; v1+v2 = reine Meilisearch-Keyword-Suche.
|
||||
- [ ] **REQ-suche-ui**: Cmd+K-Modal (global Hotkey, debounced) und Vollbild-Such-View mit persistierter Suche und RecentSearch-Anzeige.
|
||||
|
||||
### Modul 25 — Calendar-Sync
|
||||
@@ -156,26 +156,26 @@
|
||||
|
||||
### Modul 26 — Tasks
|
||||
|
||||
- [ ] **REQ-tasks-datenmodell**: Task mit Status, Priorität, Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Deal/Project) und TaskWatcher.
|
||||
- [ ] **REQ-tasks-reminder-archivierung**: Reminder-System via M22 (konfigurierbare Vorlaufzeit) und Auto-Archivierung abgeschlossener Tasks nach 30 Tagen per Cron.
|
||||
- [x] **REQ-tasks-datenmodell**: Task mit Status, Priorität, Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Project) und TaskWatcher. — AK-P4b (auf main; CRM-Entity-Wiring folgt in P6/R-P6).
|
||||
- [x] **REQ-tasks-reminder-archivierung**: Reminder-System via M22 (konfigurierbare Vorlaufzeit) und Auto-Archivierung abgeschlossener Tasks nach 30 Tagen per Cron. — AK-P4b (auf main; Reminder+Archiv-Cron auf Staging funktional verifiziert).
|
||||
|
||||
### Modul 27 — Auth / Multi-Tenancy / Permissions
|
||||
|
||||
- [ ] **REQ-auth-multi-tenancy**: Self-Service-Signup mit auto-generiertem Tenant-Slug, Reserved-Words-Validierung und URL-Struktur (app.hivegrid.de, admin.hivegrid.de).
|
||||
- [ ] **REQ-auth-methoden**: Email+Passwort (NIST-konform, Breach-Check), TOTP-2FA (RFC 6238) und Magic-Link exklusiv für Customer-Portal (AD-3).
|
||||
- [ ] **REQ-auth-rbac**: Rollen-basiertes Permission-System (Role ↔ User M:N) mit Permission-Keys in packages/entitlements und 6 SHK-Standard-Rollen geseedet.
|
||||
- [ ] **REQ-auth-rbac**: Rollen-basiertes Permission-System (Role ↔ User M:N) mit Permission-Keys in packages/entitlements und 6 SHK-Standard-Rollen geseedet *(Seed nur Staging/Dev — Produktion startet admin-only, AD-29)*.
|
||||
- [ ] **REQ-auth-user-lifecycle**: User-Einladung (48h-Token), Soft-Deactivation und DSGVO-Löschung via PII-Anonymisierung ohne Audit-Trail-Verlust.
|
||||
- *(→ v2, Scope-Schnitt 2026-06-12)* **REQ-auth-api-keys**: API-Key-System mit SHA-256-Hash-Speicherung, fein-granularen Scopes und Rate-Limit (1000 Req/Min Default). *(Wandert mit der Public-API; Boilerplate-`ApiKey`-Modell bleibt ungenutzt im Code.)*
|
||||
|
||||
### Modul 28 — Subscriptions/Billing
|
||||
|
||||
- [ ] **REQ-billing-datenmodell**: TenantSubscription mit Stripe-Integration, Frozen-Modus (Read-Only-Middleware), AI-Credits-Konto und Stripe-Webhook-Verarbeitung.
|
||||
- [ ] **REQ-billing-datenmodell**: TenantSubscription mit Lago/Mollie-Recut nach AD-27: Lago als Billing-/Invoice-Engine, Mollie nur als Payment-Processor, lokaler HiveGrid-Billing-State fuer Entitlements und Frozen-Modus (Read-Only-Middleware). Kein v1-Usage-Billing, keine Credits; AI-Credits/AiTokenUsage entfallen nach AD-26.
|
||||
- [ ] **REQ-billing-trial-dunning**: 14-Tage-Trial ohne Kreditkarte, Dunning-Email-Sequenz (Tag 1/3/7) bei fehlgeschlagener Zahlung und anschließender Frozen-Modus.
|
||||
|
||||
### Modul 29 — Onboarding
|
||||
|
||||
- *(→ v2, Scope-Schnitt 2026-06-11)* **REQ-onboarding-wizard**: Branchen-getriebener Onboarding-Wizard nach Self-Service-Signup mit SHK-Template-Vorauswahl und OnboardingSession.
|
||||
- [ ] **REQ-onboarding-demo-daten**: Demo-Daten-Provisioning auf Wunsch (5 Contacts, 3 Deals, 2 Projekte, Tasks) mit Lösch-Funktion via is_demo_data-Flag. *(Scope-Schnitt 12.06.: KI-Walkthrough → v2 mit Wizard-UI; Settings-Knopf statt Wizard-Schritt, einfaches Löschen.)*
|
||||
- [ ] **REQ-onboarding-demo-daten**: Demo-Daten-Provisioning auf Wunsch (Kontakte, Projekte in Sales-/Auftragssichten, Tasks) mit Lösch-Funktion via is_demo_data-Flag. *(Scope-Schnitt 12.06. + AD-26: KI-Walkthrough entfällt; Settings-Knopf statt Wizard-Schritt, einfaches Löschen.)*
|
||||
|
||||
### Modul 30 — i18n
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
### Modul 33a — Apps-Engine
|
||||
|
||||
- [ ] **REQ-apps-engine-datenmodell**: DataConnection, AppSyncRun, AppSyncItem und type-safe Adapter-Registry als zentrales Integrations-Framework.
|
||||
- [ ] **REQ-apps-engine-lifecycle**: OAuth2-Connect-Flow (Gmail, Outlook, AirCall, PandaDoc), API-Key-Connect (KWP, Stripe), Connection-Health-Monitoring und Settings-UI.
|
||||
- [ ] **REQ-apps-engine-lifecycle**: OAuth2-Connect-Flow (Gmail, Outlook, AirCall, PandaDoc), API-Key-Connect (KWP, Payment-Provider-APIs), Connection-Health-Monitoring und Settings-UI.
|
||||
|
||||
### Modul 34 — Public API & Webhooks
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
### Modul 35 — DSGVO-Export / Account-Löschung
|
||||
|
||||
- [ ] **REQ-dsgvo-export**: GDPR-Daten-Export als ZIP via BullMQ-Worker mit 72h-signed-Download-URL (S3-kompatibles Object Storage, Hetzner) und Download-Link-Email.
|
||||
- [ ] **REQ-dsgvo-tenant-deletion**: Tenant-Account-Löschung mit Crypto-Shredding (Per-Tenant-DEK löschen), Stripe-Subscription-Cancel und vollständiger Storage/Cache-Bereinigung.
|
||||
- [ ] **REQ-dsgvo-tenant-deletion**: Tenant-Account-Löschung mit Crypto-Shredding (Per-Tenant-DEK löschen), Lago-Subscription-Cancel/Mollie-Zahlungsreferenz-Bereinigung und vollständiger Storage/Cache-Bereinigung.
|
||||
|
||||
### Modul 36 — Mobile-PWA (PWA-only, kein nativer Client)
|
||||
|
||||
@@ -257,10 +257,11 @@
|
||||
|
||||
Bereits zuvor v2 (in Phase-1-Specs referenziert, nicht vollständig spezifiziert):
|
||||
|
||||
- **M11 WhatsApp**: Von M13 (Team-Chat) und M18 (KI-Layer/send-whatsapp-Action) referenziert; vollständige Spec und Implementierung verschoben auf Phase 2.
|
||||
- **M11 WhatsApp**: Von M13 (Team-Chat) referenziert; vollständige Spec und Implementierung verschoben auf spätere Phase.
|
||||
- **REQ-branding-tenant-phase2**: Tenant-Branding, White-Label und Custom-Domain (TenantBranding: logo, Farben, Custom-Domain) — Feature-Gate branding.white_label nur in Enterprise-Plan; verschoben auf Phase 2.
|
||||
- **REQ-signatur-provider Fallback**: Einfacher PDF-Upload mit manueller Signatur-Erfassung als Fallback ohne externen Signatur-Provider — Phase 2.
|
||||
- **REQ-signatur-provider Fallback**: Einfacher PDF-Upload mit manueller Signatur-Erfassung bleibt Fallback; Standard ist self-hosted SES (AD-25), externe Provider nur BYOK/Opt-in.
|
||||
- **DeepL-Übersetzungs-Pipeline (M30)**: Automatische Übersetzungsvorschläge via DeepL für Locale-Files — Phase 2.
|
||||
- **KI/HiveMind/RAG/Whisper**: kein v1/v2-Scope nach AD-26; v2 liefert stattdessen `REQ-mcp-server`.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
@@ -301,8 +302,8 @@ Phase-Mapping aus ROADMAP.md (Stufe 2, revidiert auf 21 Phasen). Bau-Reihenfolge
|
||||
| REQ-feature-flags-system | Phase 1 | Pending |
|
||||
| REQ-files-datenmodell | Phase 4 | Pending |
|
||||
| REQ-files-upload-preview | Phase 4 | Pending |
|
||||
| REQ-tasks-datenmodell | Phase 4 | Pending |
|
||||
| REQ-tasks-reminder-archivierung | Phase 4 | Pending |
|
||||
| REQ-tasks-datenmodell | Phase 4 | Done |
|
||||
| REQ-tasks-reminder-archivierung | Phase 4 | Done |
|
||||
| REQ-apps-engine-datenmodell | Phase 5 | Pending |
|
||||
| REQ-apps-engine-lifecycle | Phase 5 | Pending |
|
||||
| REQ-crm-datenmodell | Phase 6 | Pending |
|
||||
@@ -373,8 +374,9 @@ Phase-Mapping aus ROADMAP.md (Stufe 2, revidiert auf 21 Phasen). Bau-Reihenfolge
|
||||
| REQ-feedback-widget | Phase 21 | Pending |
|
||||
|
||||
**Coverage:**
|
||||
- v1 requirements: 90 total (Scope-Schnitte 2026-06-11/12: 19 → v2; **Pivot-Neuzählung 16.06.: REQ-crm-lead-promotion entfällt −1, REQ-rechnung-sub-entity NEU +1 → Netto unverändert 90**)
|
||||
- Mapped to phases: 90 (100 %)
|
||||
- v1 requirements: 89 ausführbare v1-Requirements (Scope-Schnitte 2026-06-11/12: 19 → v2; **Pivot-Neuzählung 16.06.: REQ-crm-lead-promotion entfällt −1, REQ-rechnung-sub-entity NEU +1; AD-26 entfernt den v1-KI-Layer**)
|
||||
- v2 adds: `REQ-mcp-server` in v2-P4 / Phase 25 (ersetzt den alten KI-Tiefe-Scope)
|
||||
- Mapped to phases: 89 v1 + 1 v2-MCP (100 %)
|
||||
- Unmapped: 0
|
||||
|
||||
**Pro-Phase-Verteilung:**
|
||||
@@ -393,7 +395,7 @@ Phase-Mapping aus ROADMAP.md (Stufe 2, revidiert auf 21 Phasen). Bau-Reihenfolge
|
||||
| Phase 10 — KWP-Integration | 0 (→ v2, erstes v2-Modul) |
|
||||
| Phase 11 — Globale Suche & Dashboard | 5 |
|
||||
| Phase 12 — Automation-Engine | 5 |
|
||||
| Phase 13 — KI-Layer | 1 |
|
||||
| Phase 13 — KI-Layer | 0 (entfällt nach AD-26; `REQ-mcp-server` liegt in v2-P4) |
|
||||
| Phase 14 — Email & Marketing | 4 |
|
||||
| Phase 15 — Telefonie & Team-Chat | 5 |
|
||||
| Phase 16 — Kalender, Booking, Forms & Signatur | 10 |
|
||||
@@ -402,7 +404,7 @@ Phase-Mapping aus ROADMAP.md (Stufe 2, revidiert auf 21 Phasen). Bau-Reihenfolge
|
||||
| Phase 19 — Plattform-Admin | 2 |
|
||||
| Phase 20 — Billing, Onboarding & Branding | 4 |
|
||||
| Phase 21 — DSGVO & Launch-Reife | 3 |
|
||||
| **Summe** | **90** |
|
||||
| **Summe** | **89 v1 + 1 v2-MCP** |
|
||||
|
||||
---
|
||||
*Requirements definiert: 2026-06-07 via /gsd:ingest-docs (Stufe 1). Phase-Mapping ergänzt: 2026-06-07 via /gsd:new-project (Stufe 2 — ROADMAP). Re-Mapping auf 21 Phasen: 2026-06-07 (Roadmap-Revision — dicke Buckets aufgeteilt). Scope-Schnitte 2026-06-11/12 (#1–#12): 19 REQs → v2 (90 v1, davon 3 KWP = erstes v2-Modul), Dünnungs-Notes an 13 REQs, AD-6/7-Lock nachgetragen. **R-REFIT 2026-06-16 (Pivot AD-20): Module 1–4 projekt-zentrisch nachgezogen; REQ-crm-lead-promotion → REQ-lead-projekt-konvertierung; Sales/Projekt-Reqs zu „Projekt & Pipelines" konsolidiert (REQ-projekt-kern-datenmodell, REQ-pipeline-rechte-config u.a.); REQ-rechnung-sub-entity NEU; Req-Summe neu gezählt = 90 (Netto unverändert: −1 lead-promotion / +1 rechnung).** *
|
||||
*Requirements definiert: 2026-06-07 via /gsd:ingest-docs (Stufe 1). Phase-Mapping ergänzt: 2026-06-07 via /gsd:new-project (Stufe 2 — ROADMAP). Re-Mapping auf 21 Phasen: 2026-06-07 (Roadmap-Revision — dicke Buckets aufgeteilt). Scope-Schnitte 2026-06-11/12 (#1–#12): 19 REQs → v2, Dünnungs-Notes an 13 REQs, AD-6/7-Lock nachgetragen. **R-REFIT 2026-06-16 (Pivot AD-20): Module 1–4 projekt-zentrisch nachgezogen; REQ-crm-lead-promotion → REQ-lead-projekt-konvertierung; Sales/Projekt-Reqs zu „Projekt & Pipelines" konsolidiert (REQ-projekt-kern-datenmodell, REQ-pipeline-rechte-config u.a.); REQ-rechnung-sub-entity NEU. AD-26: v1-KI-Layer entfernt, `REQ-mcp-server` als v2-P4 aufgenommen.** *
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
v2 bringt die eigentlichen USP-/Revenue-Hebel, die in v1 bewusst geschnitten wurden: **KWP-Sync** (das Haupt-Verkaufsargument), **Self-Service-Abo**, **Postfach-Sync**, **Customer-Portal**, **Mobile-PWA-Ausbau**, **HiveMind/RAG** und **Public-API**. v1 hat die tragfähige Kette + alle **Andockpunkte** geliefert; v2 dockt rückwirkend an (Dependency-Inversion bleibt das Prinzip).
|
||||
v2 bringt die eigentlichen USP-/Revenue-Hebel, die in v1 bewusst geschnitten wurden: **KWP-Sync** (das Haupt-Verkaufsargument), **Self-Service-Abo**, **Postfach-Sync**, **Customer-Portal**, **Mobile-PWA-Ausbau**, **MCP-Server** und **Public-API**. v1 hat die tragfähige Kette + alle **Andockpunkte** geliefert; v2 dockt rückwirkend an (Dependency-Inversion bleibt das Prinzip).
|
||||
|
||||
**Bau-Strategie:** Wie v1 zwei dependency-frei parallele Tracks (Raffael + Akeshan), Designer (David) 1–2 Phasen voraus. v2-Sprint in 3 Wochen: **v2-W1** 13.–19.07. · **v2-W2** 20.–26.07. · **v2-W3** 27.–31.07.
|
||||
|
||||
@@ -49,7 +49,7 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
| v2-P1 | Phase 22 | — | Public-API & Webhooks |
|
||||
| v2-P2 | Phase 23 | — | Postfach-Sync (Email-Inbox) |
|
||||
| v2-P3 | Phase 24 | — | Self-Service-Billing & Onboarding-Wizard |
|
||||
| v2-P4 | Phase 25 | — | KI-Tiefe — HiveMind/RAG |
|
||||
| v2-P4 | Phase 25 | — | MCP-Server |
|
||||
| **v2-P5** | **Phase 32** | **ex-P10** | **KWP-Integration (RECUT)** |
|
||||
| v2-P6 | Phase 26 | ex-P18 | Customer-Portal |
|
||||
| v2-P7 | Phase 27 | — | Mobile-PWA-Ausbau |
|
||||
@@ -67,11 +67,13 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
**Milestones:** M161 (OpenAPI + Read-Endpoints), M162 (API-Key-Auth + Scopes + Rate-Limit), M163 (Webhooks signiert), M027 (API-Keys + Tarif-Gates)
|
||||
**Source-Specs:** `_bmad-output/specs/M34-public-api.md`
|
||||
**Depends on:** v1-`ApiKey`-Modell (M027-Vorbereitung lag in v1 ungenutzt bereit) · Redis (Token-Bucket) · v1-Frozen/past_due-Middleware (Writes 402) · RLS via `app.tenant_id` · v1-P12 Automation (`external_event`-Trigger + ausgehende Webhook-Action)
|
||||
**Host-Contract:** Public API nur `https://api.hivegrid.de/v1/...` (staging: `https://staging-api.hivegrid.de/v1/...`);
|
||||
keine Public-API-Routen unter `app.hivegrid.de/api`.
|
||||
**Success Criteria:**
|
||||
1. GET contacts/companies/deals/projects/tasks/leads — cursor-paginiert, Filter/Sort/Sparse-Fieldsets/Embed (Tiefe 1); `read_only`-Scope lässt sensible Felder aus (nicht 403).
|
||||
2. Eingehende Lead-Webhooks (`POST /webhooks/incoming/{tenant}/{slug}`) legen Leads an (API-Key/HMAC/URL-Token + Payload-Mapping + Auto-Match).
|
||||
2. Eingehende Lead-Webhooks (`POST https://api.hivegrid.de/v1/webhooks/incoming/{tenant}/{slug}`) legen Leads an (API-Key/HMAC/URL-Token + Payload-Mapping + Auto-Match).
|
||||
3. Ausgehende WebhookSubscriptions: HMAC-SHA256-Signatur, Idempotenz-ID, Retry mit exp. Backoff (max 5), Auto-Disable nach 10 Fehlern.
|
||||
4. OpenAPI-3.1 auto-generiert (`/api/v1/openapi.json`) + Swagger-UI; Rate-Limit (1k/h Key, 10k/h Tenant) mit `X-RateLimit-*` + Klartext-429.
|
||||
4. OpenAPI-3.1 auto-generiert (`https://api.hivegrid.de/v1/openapi.json`) + Swagger-UI; Rate-Limit (1k/h Key, 10k/h Tenant) mit `X-RateLimit-*` + Klartext-429.
|
||||
**Screens (David):** API-Key-Verwaltung (Erweiterung) · Incoming-Webhook-Slug-Verwaltung + Ingestion-Log · Outgoing-Subscription-Liste/Detail (Secret-einmal-sichtbar, Delivery-Stats, Replay/Test) · API-Request-Logs + Usage-Aggregat
|
||||
**Recut/Risiko:** Bewusst **Read-only + Webhooks** (Voll-CRUD/GraphQL/3-Legged-OAuth = später). Vor Bau: OpenAPI-Tooling wählen, HMAC-Format dokumentieren.
|
||||
|
||||
@@ -79,11 +81,11 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
**Owner:** Akeshan · **Slot:** W1 (13.–17.07.) · **Goal:** Gmail/Outlook-Postfächer werden bidirektional ins CRM gespiegelt, auto-gematcht und im Activity-Stream sichtbar.
|
||||
**Milestones:** M096 (Gmail-OAuth via Apps-Engine), M097 (Sync + Inbox-UI + Threading + Auto-Verknüpfung), M098 (Versand + Vorlagen)
|
||||
**Source-Specs:** `_bmad-output/specs/M09-email-inbox.md` (OAuth-Sync/Inbox/Threading-Teile)
|
||||
**Depends on:** v1-Sende-Domain DKIM/SPF/DMARC (P14, NICHT neu bauen) · `EmailProviderInterface` (Apps-Engine-Pattern) · v1-Activity-Stream + Contact/Deal/Project-FKs (Matching) · Webhook-Ingest-Infra (geteilt mit P1) · v1-Files (Anhänge) · Token-Encryption (DEK)
|
||||
**Depends on:** v1-Sende-Domain DKIM/SPF/DMARC (P14, NICHT neu bauen) · `EmailProviderInterface` (Apps-Engine-Pattern) · v1-Activity-Stream + Contact/Project-FKs (Matching) · Webhook-Ingest-Infra (geteilt mit P1) · v1-Files (Anhänge) · Token-Encryption (DEK)
|
||||
**Success Criteria:**
|
||||
1. OAuth-Connect Gmail (Pub/Sub-Watch) + Microsoft (Graph-Subscription, 3-Tage-Renewal-Worker); Token-Refresh + Revoke.
|
||||
2. Push-Sync (kein Primär-Polling) spiegelt Mails; Initial-Sync 90 Tage, resumebar mit Progress.
|
||||
3. Auto-Match: From-Adresse → Contact/Lead, Subject-Pattern (`[Deal #123]`), 1-offener-Deal-Heuristik; manueller Override sticht.
|
||||
3. Auto-Match: From-Adresse → Contact/Lead, Subject-Pattern (`[Projekt #123]`), 1-offenes-Projekt-Heuristik; manueller Override sticht.
|
||||
4. Inbox-Liste (Filter/Suche/Bulk) + Detail mit Thread, sicherem HTML-Render (CSP, externe Bilder blockiert), Reply/Forward via Mailbox.
|
||||
5. `email.read_own` vs. `email.read_all` filtern Liste + Activity-Stream.
|
||||
**Screens (David):** Mailbox-Connect-Settings (Sync-Status/Reconnect) · Inbox-Liste · Mail-Detail (Thread/Verknüpfungs-Editor/Reply) · Outbound-Compose
|
||||
@@ -93,7 +95,7 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
**Owner:** Akeshan · **Slot:** W1→W2 (Abo-UI 13.–17., Wizard 18./19.) · **Goal:** Tenants buchen/wechseln Pläne self-service über Web-Checkout und durchlaufen einen branchen-getriebenen Onboarding-Wizard.
|
||||
**Milestones:** M157 (Wizard-Framework + State-Machine) + M28-Self-Service-Delta (In-App-Subscription, Block P)
|
||||
**Source-Specs:** `_bmad-output/specs/M28-subscriptions-billing.md` (Block P), `M29-onboarding.md`
|
||||
**Depends on:** v1-Stripe-Checkout + Webhook-Handler + StripeWebhookLog · v1-`SubscriptionFeature`/`ProductFeature` (Feature-Gate-Modell) · v1-Read-Only-Middleware · v1-TenantUserInvitation · **deklaratives Branchen-Template-Verzeichnis** (geteilt mit P6/P10) · ~~v2-P4 (KI-Walkthrough-Coach)~~ → KI-Coach entfällt ([[AD-26]]: kein eigenes LLM; v2-P4 = MCP-Server)
|
||||
**Depends on:** v1-Payment-Checkout + Webhook-Handler + Legacy-ProviderWebhookLog · v1-`SubscriptionFeature`/`ProductFeature` (Feature-Gate-Modell) · v1-Read-Only-Middleware · v1-TenantUserInvitation · **deklaratives Branchen-Template-Verzeichnis** (geteilt mit P6/P10) · ~~v2-P4 (KI-Walkthrough-Coach)~~ → KI-Coach entfällt ([[AD-26]]: kein eigenes LLM; v2-P4 = MCP-Server)
|
||||
**Success Criteria:**
|
||||
1. Self-Service-Trial ohne Vertriebs-Freigabe (`requires_manual_activation=false`) mit Zahlungsmethoden-Pflicht.
|
||||
2. Plan-Wechsel/Add-On-Käufe laufen über den Web-Checkout-Flow (PWA-only → kein App-Store, keine Store-Billing-Auflage).
|
||||
@@ -108,12 +110,16 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
**Owner:** Raffael · **Slot:** W1→W2 (Start 18./19.07.) · **Goal:** Ein produktiver, tenant-gescopeter Remote-MCP-Server stellt HiveGrid-Daten als Read-Ressourcen und freigegebene Aktionen als Action-Tools bereit — auditiert, permission-gegated, ohne eigenes LLM.
|
||||
**Milestones:** kein M0xx (v2-Neuschnitt) → siehe [[VR-MCP]]/[[VD-MCP]]
|
||||
**Source-Specs:** [[VR-MCP]] / [[VD-MCP]] · [[AD-26]]
|
||||
**Depends on:** v1-`ApiKey`-Auth + Scopes + Rate-Limit aus der Public-API ([[VR-P22]]) · v1-`withTenantScope()`/RLS · v1-`withPermission()` (M004) · v1-M001-Audit · `@modelcontextprotocol/sdk`
|
||||
**Depends on:** v1-OAuth 2.1/PKCE für MCP + Scopes + Rate-Limit aus der Public-API-Grundlage ([[VR-P22]]) · v1-`withTenantScope()`/RLS · v1-`withPermission()` (M004) · v1-M001-Audit · `@modelcontextprotocol/sdk`
|
||||
**Host-/Runtime-Contract:** MCP nur `https://mcp.hivegrid.de` (staging: `https://staging-mcp.hivegrid.de`)
|
||||
als eigener Service/Container `hivegrid-mcp` (`apps/mcp`); kein `/mcp`-Pfad, nicht in `app.hivegrid.de` oder `api.hivegrid.de`.
|
||||
**Success Criteria:**
|
||||
1. Remote-MCP-Server (Streamable HTTP, `@modelcontextprotocol/sdk`) exponiert Read-Ressourcen (Kontakte/Firmen/Leads/Projekte/Aufträge u.a.) und Action-Tools (definierte Schreib-Aktionen).
|
||||
2. Authentifizierung per **API-Key + Scopes + Rate-Limit** aus der Public-API ([[VR-P22]]); Tenant wird aus dem Token abgeleitet, jeder Zugriff läuft durch `withTenantScope()`/RLS.
|
||||
2. Authentifizierung per **OAuth 2.1 + PKCE + Scopes + Rate-Limit**; Tenant wird aus dem Access Token abgeleitet,
|
||||
jeder Zugriff läuft durch `withTenantScope()`/RLS. API-Key bleibt nur Fallback/Operator-Modus.
|
||||
3. Jede Action ist `withPermission()`-gegated, verlangt bei schreibenden Operationen eine **Confirmation** und wird via M001-Audit protokolliert (Actor-Kind „MCP/Agent").
|
||||
4. Tenant-Admin verwaltet MCP-Zugänge/Scopes (UI aus [[VD-MCP]]); keine neuen KI-Entitäten — Nutzung von `ApiKey`/`Credential` (ggf. kleine `McpGrant`/Scope-Tabelle).
|
||||
4. Tenant-Admin verwaltet MCP-Verbindungen/Scopes (UI aus [[VD-MCP]]); OAuth-Clients/Tokens/Grants sind getrennt
|
||||
nachvollziehbar, keine neuen KI-Entitäten.
|
||||
**Screens (David):** siehe [[VD-MCP]] (MCP-Connection-Verwaltung, Scope-Auswahl, Audit-/Aktivitäts-Sicht der MCP-Zugriffe).
|
||||
**Recut/Risiko:** Kein LLM-Hosting → **kein Provider-AVV** und kein DSGVO-Provider-Constraint mehr; Sicherheit liegt vollständig in Tenant-Scope/RLS, Scope-Minimierung und Confirmation-Grenze für Schreib-Aktionen.
|
||||
|
||||
@@ -129,7 +135,7 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
**Owner:** Raffael · **Slot:** W2 (20.–26.07., E2E mit Geerkens Fr 24.07.) · **Goal:** KWP wird regulärer REST/OAuth-Adapter über die Apps-Engine — bidirektionaler Sync (Aufträge/Stammdaten/Belege/Plantafel/Dokumente) mit „KWP gewinnt"-Konfliktregel.
|
||||
**Milestones:** M089 (API-Connector), M090 (Connection-Health), M091 (Push-Queue + Replay), M092 (Contract-Sentinel), M093 (Konflikt „KWP gewinnt"), M094 (Plantafel-Sync), M095 (Dokumenten-Archiv-Sync), M074 (Task-ERP-Sync)
|
||||
**Source-Specs:** `_bmad-output/specs/M19-kwp-integration.md` + **`docs/milestones/M19-KWP-API-RECUT.md` (bindend)**
|
||||
**Depends on:** **HART: v1-M33a Apps-Engine-Adapter-Interface (M008–M010)** · v1-Cloud-Outbox + Idempotency-Wrapper (M007) · v1-`@toolctrl/storage` + FileEntityLink · v1-Extension-Points an Auftrag/Abnahme/Material (P9) · v1-P12 Automation (`kwp.push_auftrag`-Action) · AVV/Subprozessoren (P11)
|
||||
**Depends on:** **HART: v1-M33a Apps-Engine-Adapter-Interface (M008–M010)** · v1-Cloud-Outbox + Idempotency-Wrapper (M007) · v1-`@hivegrid/storage` + FileEntityLink · v1-Extension-Points an Auftrag/Abnahme/Material (P9) · v1-P12 Automation (`kwp.push_auftrag`-Action) · AVV/Subprozessoren (P11)
|
||||
**Success Criteria:**
|
||||
1. KWP-API-Connector als erster konkreter Adapter hinterm M33a-Interface; Credentials per-Tenant-DEK-verschlüsselt in `DataConnection.config`.
|
||||
2. Bidirektionaler Sync: Push (Won→Auftrag) via Outbox + Rate-Limited-Replay; Pull via Webhook (falls KWP-Support) oder Cron, idempotent über `AppSyncItem`.
|
||||
@@ -183,7 +189,7 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
**Owner:** Akeshan · **Slot:** W3 (28.–30.07.) · **Goal:** Öffentliches file-basiertes Help-Center mit Deep-Links aus der App + In-App-Feedback-Widget → Plattform-Admin.
|
||||
**Milestones:** M172 (Markdown-Artikel Git-PRs), M173 (Routes + Suche + SEO + kontextuelle Hilfe), M174 (Email-Ticket-System), M175 (In-App-Feedback-Widget)
|
||||
**Source-Specs:** `_bmad-output/specs/M38-help-center.md`, `M39-help-desk.md`
|
||||
**Depends on:** Public-Routing außerhalb Auth-Middleware + IP-Rate-Limiting · v1-Files (Feedback-Attachments) · **v1-Feedback-Widget → Support-Inbox (P21-Andockpunkt, generische Quelle)** · v2-P8 AdminAuditLog + Admin-Permissions · v2-P4 RAG (Help-Artikel als Korpus) · v2-P10 i18n (Sprachsegment)
|
||||
**Depends on:** Public-Routing außerhalb Auth-Middleware + IP-Rate-Limiting · v1-Files (Feedback-Attachments) · **v1-Feedback-Widget → Support-Inbox (P21-Andockpunkt, generische Quelle)** · v2-P8 AdminAuditLog + Admin-Permissions · v2-P4 MCP-Resources optional für Help-Artikel · v2-P10 i18n (Sprachsegment)
|
||||
**Success Criteria:**
|
||||
1. Markdown mit Frontmatter, Build-Time-Index (Volltext + Kategorien + Sitemap), Build-Fail bei kaputten Referenzen.
|
||||
2. Öffentliche Routen `/help/{lang}/{category}/{slug}` ohne Auth (vor Auth-Middleware), SEO-Defaults, Suche.
|
||||
@@ -191,7 +197,7 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
4. Feedback-Widget (überall) → `Feedback` mit Auto-Kontext (tenant/user/route/browser/app-version), Screenshot-Option, Rate-Limit 10/h.
|
||||
5. Plattform-Admin: Feedback-Liste + Detail (Status-Lifecycle/Assignee/Notes/Attachments) mit Audit.
|
||||
**Screens (David):** Help-Home (Hero/Suche/Kategorien) · Kategorie-Seite · Artikel-Detail (TOC/verwandt/Vote) · Such-Ergebnisse · Feedback-Modal (in v1 schon klein da → ausbauen) · Admin-Feedback-Liste/Detail
|
||||
**Recut/Risiko:** Bewusst klein (Git-PRs, kein Editor-UI, kein volles Ticket-System in Phase 1). v2-später: DB-Pflege, KI-RAG-Suche, Mehrsprachigkeit, kontextuelle Inline-Hilfe, SLA/CSAT/Live-Chat. Support-Mailbox-Sync (Mails→Feedback) nutzt M9.
|
||||
**Recut/Risiko:** Bewusst klein (Git-PRs, kein Editor-UI, kein volles Ticket-System in Phase 1). Später: DB-Pflege, MCP-Resource-Expose, Mehrsprachigkeit, kontextuelle Inline-Hilfe, SLA/CSAT/Live-Chat. Support-Mailbox-Sync (Mails→Feedback) nutzt M9.
|
||||
|
||||
### v2-P10: Komfort & Querschnitt
|
||||
**Owner:** Akeshan · **Slot:** W3 (29.–31.07.) · **Goal:** Querschnitts-Komfort — anpassbare Dashboards/Export, Notification-Bündelung/Quiet-Hours, Sprach-Wechsler/EN, HR-Drittanbieter-Onboarding.
|
||||
@@ -210,7 +216,7 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
**Owner:** Raffael · **Slot:** W3 (29.–31.07.) · **Goal:** Zweiten Telefonie-Provider anbinden + DSGVO vertiefen (AVV-Versionierung, Subprozessoren-Workflow, Auskunfts-/Export-Workflow ersetzt manuelles Playbook).
|
||||
**Milestones:** M105 (HalloPetra-Adapter), M166 (AVV + Subprozessoren + Auskunfts-Workflow)
|
||||
**Source-Specs:** `_bmad-output/specs/M12-telefonie.md` (Provider-Pattern), `M35-dsgvo-export.md`
|
||||
**Depends on:** **v1-TelephonyProviderInterface + AirCall-Adapter (P15) als Referenz** · v1-Automation-Trigger (`call.*`) · v1-notify() · v1-Files (Export-ZIP, AVV-PDF) · BullMQ-Worker · v1-Account-Löschung (P2/P21) · v1-Frozen→Deletion (P20) · v2-P8 AdminAuditLog + Legal-Verwaltung · pgvector (Embedding-Löschung beim Hard-Delete)
|
||||
**Depends on:** **v1-TelephonyProviderInterface + AirCall-Adapter (P15) als Referenz** · v1-Automation-Trigger (`call.*`) · v1-notify() · v1-Files (Export-ZIP, AVV-PDF) · BullMQ-Worker · v1-Account-Löschung (P2/P21) · v1-Frozen→Deletion (P20) · v2-P8 AdminAuditLog + Legal-Verwaltung
|
||||
**Success Criteria:**
|
||||
1. HalloPetra als zusätzlicher Provider hinterm `TelephonyProviderInterface` ohne Kern-Umbau; Webhook-Auto-Match → Activity + Automation-Trigger.
|
||||
2. Click-to-Call + Inbound + Logging providerunabhängig (E.164-Normalisierung, Match-Confidence, Notiz-Editor).
|
||||
@@ -218,7 +224,7 @@ M15 (Portal), M37/M32 (Admin), M38/M39 (Help), M20/M22/M30 (Querschnitt) sind in
|
||||
4. Subprozessoren-Liste mit Drittland-Warnung + 14-Tage-Widerspruchs-Workflow.
|
||||
5. User-Daten-Export (Art. 15/20) + Tenant-Voll-Export (ZIP, async, 7-Tage-Window) + Tenant-Lösch-Workflow (30-Tage-Grace + Hard-Delete-Cron).
|
||||
**Screens (David):** Telefonie-Setup (Provider-Connect, User-Bindings, Test) · Call-Session-Detail · DSGVO/Privacy-Hub (Export-Jobs, Tenant-Lösch-Multi-Step + Slug-Confirm, Subprozessoren, AVV-Download/History) · User-Daten-Export/Account-Löschung · Admin: Subprozessoren + AVV-Versions-Verwaltung
|
||||
**Recut/Risiko:** ⚠️ **Gap HalloPetra:** keine Spec — API-Vertrag (Auth/Webhook-Events/Recording/Call-Lifecycle) erst erheben (eigener Vorlauf-Task). AVV-PDF-Inhalt anwaltlich finalisieren (in v1 EXT-03 beauftragt). Voicemail-Transkription via Whisper koppelt an P4.
|
||||
**Recut/Risiko:** ⚠️ **Gap HalloPetra:** keine Spec — API-Vertrag (Auth/Webhook-Events/Recording/Call-Lifecycle) erst erheben (eigener Vorlauf-Task). AVV-PDF-Inhalt anwaltlich finalisieren (in v1 EXT-03 beauftragt). Voicemail-Transkription/Whisper ist nach AD-26 zurückgestellt.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
- [ ] **Phase 9: Projekt & Pipelines (Auftrag)** - Auftrags-Sub-Areas, Rechnung-Sub-Entity, Plantafel, digitale Abnahme (offline-fähig)
|
||||
- [ ] ~~**Phase 10: KWP-Integration**~~ — **→ v2, ERSTES v2-Modul** (deferred 2026-06-12; API-Zugang ~19.07. > Launch; Nummer reserviert, Ausführung überspringt P10)
|
||||
- [ ] **Phase 11: Globale Suche & Dashboard** - Keyword-Suche (Meilisearch, AD-5 sequenziert), Cmd+K, Widget-Dashboard mit festen Rollen-Layouts
|
||||
- [ ] **Phase 12: Automation-Engine** - Visueller Flow-Editor, Trigger/Action-Library, Guards, Sprach-zu-Automation
|
||||
- [ ] **Phase 13: KI-Layer** - EU-LLM-Gateway, Multi-Provider-Fallback, Promptfoo, Sprach-zu-Automation-Unterbau (HiveMind/RAG/Whisper → v2, Andock-Adapter in v1)
|
||||
- [ ] **Phase 12: Automation-Engine** - Visueller Flow-Editor, Trigger/Action-Library, Guards, Dry-Run, 24h-Undo
|
||||
- [ ] ~~**Phase 13: KI-Layer**~~ — **entfällt in v1 (AD-26)**; kein LLM/RAG/Whisper, kein Sprach-zu-Automation-Unterbau. MCP-Server ist v2-P4.
|
||||
- [ ] **Phase 14: Email & Marketing** - Sende-Domain-Verifikation, BCC-Logging-Brücke, DSGVO-konforme Marketing-Kampagnen (Postfach-Sync → v2)
|
||||
- [ ] **Phase 15: Telefonie & Team-Chat** - AirCall-Click-to-Call/Inbound, Real-Time-Team-Chat mit entity-linked Channels
|
||||
- [ ] **Phase 16: Kalender, Booking, Forms & Signatur** - Kalender-Sync, öffentliche Buchungsseiten, Form-Builder, Angebots-Signatur
|
||||
- [ ] **Phase 17: HR-Hub** - Verschlüsselter HR-Hub, Passwort-Safe, Urlaubs-/Krank-Workflow
|
||||
- [ ] ~~**Phase 18: Customer-Portal**~~ — **→ v2** (deferred 2026-06-11; Nummer reserviert, Ausführung überspringt P18)
|
||||
- [ ] **Phase 19: Plattform-Admin** - Getrennte Admin-Welt (admin.hivegrid.de), Tenant-Management + Abo-Zusammenstellung, Queue-Monitoring, Support-Inbox
|
||||
- [ ] **Phase 20: Billing, Onboarding & Branding** - Stripe-Billing/Trial/Dunning (Abo-Baukasten-Recut), Auto-SHK-Provisionierung, Theme-Preference
|
||||
- [ ] **Phase 20: Billing, Onboarding & Branding** - Legacy-Provider-Billing/Trial/Dunning (Abo-Baukasten-Recut), Auto-SHK-Provisionierung, Theme-Preference
|
||||
- [ ] **Phase 21: DSGVO & Launch-Reife** - DSGVO-Export + Lösch-Workflow (Crypto-Shredding), Feedback-Widget, Launch-Checkliste
|
||||
|
||||
### v2-Phasen (Sprint Mo 13.07.–Fr 31.07., nach Launch — 43 Milestones, 2 Tracks dependency-frei parallel)
|
||||
@@ -44,7 +44,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
- [ ] **Phase 22: Public-API & Webhooks** - Versionierte Read-REST-API + signierte Webhooks + Tenant-API-Keys mit Scopes/Rate-Limit
|
||||
- [ ] **Phase 23: Postfach-Sync (Email-Inbox)** - Bidirektionale Gmail/Outlook-Inbox mit CRM-Matching, Threading, Versand über User-Mailbox
|
||||
- [ ] **Phase 24: Self-Service-Billing & Onboarding-Wizard** - Self-Service-Abo-Zusammenstellung (Web-Checkout) + geführter branchen-getriebener Wizard
|
||||
- [ ] **Phase 25: KI-Tiefe — HiveMind/RAG** - RAG über Tenant-Daten, EU-Whisper, pgvector-Hybrid-Suche, autonomer HiveMind-Agent (Action-Boundary)
|
||||
- [ ] **Phase 25: MCP-Server** - Remote-MCP-Server für externe KI-Clients mit Read-Ressourcen und bestätigungspflichtigen Action-Tools (ersetzt HiveMind/RAG, AD-26)
|
||||
- [ ] **Phase 26: Customer-Portal** ⭐ - Endkunden-Portal (eigene Auth/Magic-Link, Projekt-Sicht, Upload, Reklamation) — **kritisch** (ex-P18)
|
||||
- [ ] **Phase 27: Mobile-PWA-Ausbau** - Installierbare PWA (Add-to-Homescreen, Service-Worker-Offline, Web-Push) für den Baustellen-Einsatz — kein nativer Client/App-Store (AD-17, PWA-only)
|
||||
- [ ] **Phase 28: Plattform-Admin-Tiefe** - Getrennte AdminUser-Identität/AdminRBAC + Feature-Flag-Massen-Statistik + interne Lead-Pipeline
|
||||
@@ -66,7 +66,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Source-Specs**: `docs/ARCHITECTURE-DECISIONS.md`, `docs/INTEGRATION-CONTRACTS.md`; Service-Kern-Teile aus `_bmad-output/specs/M21-audit-log.md`, `M22-notifications.md`, `M30-i18n.md`, `M32-feature-flags.md`
|
||||
**Milestone-PLANs**: 15 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 1)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Die ToolCTRL-Boilerplate ist als Git-Downstream eingezogen, baut lokal durch und der upstream-Remote bleibt mergebar erhalten (AD-10). *Planner-Hinweis: Ist-Stand prüfen — laut ADR-Text „Docs-only", real ist die Codebase seit 2026-06-04 eingezogen; bereits Erledigtes nicht neu planen.*
|
||||
1. Die HiveGrid-Codebase baut lokal durch; Workspace-Pakete, CLI, Env-Namen, Cookies, Header und Queue-Namen nutzen HiveGrid-Contracts ohne Alt-Aliase (AD-10). *Planner-Hinweis: historische Boilerplate-Reste sind Arbeitsmaterial und kein Upstream-Merge-Ziel.*
|
||||
2. GitHub/Google-OAuth-Reste und `User.githubId/googleId` sind entfernt; keine US-Kern-Abhängigkeit mehr im Auth-Pfad (EU-Sterilisation).
|
||||
3. `logActivity()`, Search-Registry, Subframe-Adapter und Mass-Action-Schutz existieren als aufrufbare Querschnittsdienste mit Smoke-Test.
|
||||
4. Die Service-Kerne existieren als aufrufbare Dienste mit Smoke-Test: AuditEvent mit SHA-256-Hash-Kette via Prisma-Middleware (default-on), `notify()` mit Kanal-Abstraktion (In-App/Email; Push-Kanal als Stub bis v2), `packages/i18n` mit Build-Lint gegen Hard-Strings und Resolution User → Tenant → DE, Feature-Flag-Engine mit `tenantHasFlag()` + Redis-Cache (Admin-UI folgt in P19).
|
||||
@@ -86,7 +86,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
1. Ein Nutzer kann sich per Self-Service registrieren, erhält einen auto-generierten Tenant-Slug (Reserved-Words geblockt) und landet unter app.hivegrid.de.
|
||||
2. Login mit Email+Passwort (NIST/Breach-Check) plus TOTP-2FA funktioniert; Sessions sind in Postgres serverseitig (Geräte-Liste, Revoke, „von allen Geräten abmelden") mit Redis-Cache (AD-4).
|
||||
3. RLS-Policies (USING/WITH CHECK, `SET LOCAL app.tenant_id`, App-Rolle ohne BYPASSRLS) verhindern nachweisbar mandantenübergreifenden Datenzugriff (AD-1).
|
||||
4. 6 SHK-Standard-Rollen sind geseedet; ein Admin kann Nutzer einladen (48h-Token), deaktivieren und DSGVO-konform per PII-Anonymisierung löschen.
|
||||
4. 6 SHK-Standard-Rollen sind geseedet *(Staging/Dev; Produktion startet admin-only, AD-29)*; ein Admin kann Nutzer einladen (48h-Token), deaktivieren und DSGVO-konform per PII-Anonymisierung löschen.
|
||||
5. *(→ v2, Scope-Schnitt 12.06.)* ~~Ein Nutzer kann API-Keys mit Scopes und Rate-Limit erstellen~~ — wandert mit der Public-API (M161–163) nach v2.
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes
|
||||
@@ -113,19 +113,19 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Ein Nutzer kann Dateien chunked hochladen (TUS) mit RLS auf Storage-Pfaden (S3-kompatibles Hetzner Object Storage, Infra-ADR-009); Previews (PDF→PNG, Bild→WebP, **HEIC→JPG — Pflicht für iPhone-Monteur-Fotos**) sind sichtbar. *(Scope-Schnitt 12.06.: Versionierung, Office-/Video-Previews, Resume-Upload, OCR → v2; Permissions/Sharing + Tags + Archiv-Flag bleiben.)*
|
||||
2. Dateien lassen sich über FileEntityLink an Entitäten hängen; Customer-Portal-Upload ist vorbereitet.
|
||||
3. Ein Nutzer kann Tasks mit Status, Priorität, Fälligkeit und Entitäts-Verknüpfung (Contact/Company/Lead/Deal/Project) anlegen, sich als Watcher eintragen und einen Reminder (via notify()) erhalten.
|
||||
3. Ein Nutzer kann Tasks mit Status, Priorität, Fälligkeit und Entitäts-Verknüpfung (Contact/Company/Lead/Projekt) anlegen, sich als Watcher eintragen und einen Reminder (via notify()) erhalten.
|
||||
4. Abgeschlossene Tasks werden nach 30 Tagen per Cron auto-archiviert.
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 5: Apps-Engine & Integrations-Framework
|
||||
**Goal**: Externe Systeme werden über ein einziges, typsicheres Integrations-Framework angebunden — der Vertrag, über den später Gmail, Outlook, AirCall, PandaDoc, Stripe und KWP laufen.
|
||||
**Goal**: Externe Systeme werden über ein einziges, typsicheres Integrations-Framework angebunden — der Vertrag, über den später Gmail, Outlook, AirCall, Signatur-Adapter (SES-Default/PandaDoc-BYOK), Legacy-Provider und KWP laufen.
|
||||
**Depends on**: Phase 2
|
||||
**Requirements**: REQ-apps-engine-datenmodell, REQ-apps-engine-lifecycle
|
||||
**Source-Specs**: `_bmad-output/specs/M33a-apps-engine.md`
|
||||
**Milestone-PLANs**: 3 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 5)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Ein Admin kann eine DataConnection über einen OAuth2-Connect-Flow (Gmail, Outlook, AirCall, PandaDoc) oder API-Key-Connect (KWP, Stripe) in der Settings-UI herstellen.
|
||||
1. Ein Admin kann eine DataConnection über einen OAuth2-Connect-Flow (Gmail, Outlook, AirCall, optional PandaDoc-BYOK) oder API-Key-Connect (KWP, Legacy-Provider, Signatur-Adapter) in der Settings-UI herstellen.
|
||||
2. Die type-safe Adapter-Registry erlaubt das Registrieren neuer Connection-Typen ohne Änderung am Kern; AppSyncRun/AppSyncItem protokollieren Sync-Läufe.
|
||||
3. Connection-Health-Monitoring zeigt den Status jeder Verbindung; OAuth-Tokens sind per-Tenant-DEK-verschlüsselt gespeichert (AD-8).
|
||||
**Plans**: TBD
|
||||
@@ -155,7 +155,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Depends on**: Phase 6
|
||||
**Requirements**: REQ-lead-inbox-liste, REQ-lead-quellen-auto-anlage, REQ-lead-status-workflow, REQ-lead-anti-spam, REQ-lead-statistiken, REQ-lead-permissions
|
||||
**Source-Specs**: `_bmad-output/specs/M02-lead-inbox.md`
|
||||
**Milestone-PLANs**: 4 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 7); M049 (Auto-Verkaufsfall) → Phase 8 (braucht Deal-Entity); P7 liefert das `lead.qualified`-Event als Extension-Point
|
||||
**Milestone-PLANs**: 4 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 7); M049 (Auto-Projekt) → Phase 8 (braucht den Projekt-Kern); P7 liefert das `lead.qualified`-Event als Extension-Point
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Ein Bearbeiter sieht die Lead-Inbox mit Status-Filter, Quick-Actions, Bulk-Operationen und Real-time-Refresh.
|
||||
2. Leads aus Form-Submission, eingehender Email und AirCall-Inbound werden automatisch angelegt und stoßen Automation-Trigger an (sofern Automation aktiv).
|
||||
@@ -172,7 +172,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Depends on**: Phase 7
|
||||
**Requirements**: REQ-projekt-kern-datenmodell, REQ-pipeline-rechte-config, REQ-projekt-kanban-listen-view, REQ-projekt-pipeline-wechsel, REQ-projekt-forecast-kpis
|
||||
**Source-Specs**: `_bmad-output/specs/M03-sales-pipeline.md`
|
||||
**Milestone-PLANs**: 7 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 8); inkl. M049 (Auto-Verkaufsfall, aus Phase 7 hierher)
|
||||
**Milestone-PLANs**: 7 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 8); inkl. M049 (Auto-Projekt, aus Phase 7 hierher)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Der `Projekt`-Kern (typisierte Superset-Tabelle + CustomFields) existiert mit fortlaufenden Projekt-Nummern (HiveNumberSeq); mehrere Pipelines (Pipeline mit typ_hint sales/auftrag/custom, PipelineStage = Status, Verlust-/Gewinn-Gründe) sind als geseedete SHK-Pipelines konfigurierbar.
|
||||
2. Ein Nutzer kann Projekte im konfigurierbaren Kanban per Drag-&-Drop verschieben (mit Stage-Wechsel-Validation) oder in der Listen-View mit Filter/Bulk-Actions bearbeiten; Updates kommen in Real-Time (SSE, AD-9).
|
||||
@@ -220,23 +220,23 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Source-Specs**: `_bmad-output/specs/M24-globale-suche.md`, `_bmad-output/specs/M20-dashboard.md`
|
||||
**Milestone-PLANs**: 5 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 11); Scope-Schnitt 11.06.: Custom-Widgets (M149) + Export (M150) → v2; 12.06.: M147 gedünnt (feste Rollen-Layouts)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Self-hosted Meilisearch liefert permission-aware Keyword-Treffer über den SearchAdapter. *(Scope-Schnitt 11.06.: pgvector/Hybrid-RRF → v2 mit HiveMind; AD-5 sequenziert.)*
|
||||
1. Self-hosted Meilisearch liefert permission-aware Keyword-Treffer über den SearchAdapter. Kein pgvector/RAG/Embedding in v1/v2 nach AD-26.
|
||||
2. Entity-Mutationen aktualisieren den Index über das Outbox-Pattern; SearchIndexEntry und RecentSearch sind befüllt.
|
||||
3. Ein Nutzer öffnet per Cmd+K ein debounced Such-Modal mit RecentSearch und kann in eine Vollbild-Such-View wechseln.
|
||||
4. Ein Nutzer sieht ein rollenbasiertes Dashboard (3 feste Rollen-Layouts GF/Büro/Monteur, SHK-Default geseedet) mit Widgets aus Sales, Projekte, Tasks, HR und KI. *(Scope-Schnitt 12.06.: Drag&Drop-Grid + User-Layout-Persistenz → v2; Widget-Registry bleibt voll — Inversion `M064→M147`.)*
|
||||
4. Ein Nutzer sieht ein rollenbasiertes Dashboard (3 feste Rollen-Layouts GF/Büro/Monteur, SHK-Default geseedet) mit Widgets aus Sales, Projekte, Tasks und HR. *(Scope-Schnitt 12.06.: Drag&Drop-Grid + User-Layout-Persistenz → v2; KI-Widgets → Backlog/v3 nach AD-26.)*
|
||||
5. Dashboard-Widgets aktualisieren sich live (WebSocket/SSE) mit Aggregat-Cache.
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 12: Automation-Engine
|
||||
**Goal**: Die erste USP-Säule steht in voller Tiefe (AD-16): ein visueller Automation-Editor über alle Module mit vollständiger Trigger-/Action-Library, Guards und Sprach-zu-Automation-Erstellung.
|
||||
**Goal**: Die erste USP-Säule steht in voller Tiefe (AD-16): ein visueller Automation-Editor über alle Module mit vollständiger Trigger-/Action-Library, Guards, Dry-Run und 24h-Undo. Sprach-zu-Automation entfällt nach AD-26.
|
||||
**Depends on**: Phase 11
|
||||
**Requirements**: REQ-automation-datenmodell, REQ-automation-trigger-library, REQ-automation-action-library, REQ-automation-flow-editor, REQ-automation-schutz
|
||||
**Source-Specs**: `_bmad-output/specs/M17-automation-engine.md`
|
||||
**Milestone-PLANs**: 8 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 12)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Ein Nutzer baut im Graph-Editor Flows aus der vollständigen Trigger-Library (CRM, Sales, Projekte, Email, Forms, Kalender, Zeit, Webhooks) und Action-Library (CRM, Notifications, HTTP, Tasks, If/Else, Delay, Loop; KWP-Push registriert sich mit v2-P10 nach — Registry ist erweiterbar gebaut).
|
||||
2. Ein Nutzer erstellt eine Automation per natürlicher Sprache (DE), testet sie als Dry-Run und kann Ausführungen innerhalb von 24h zurückrollen.
|
||||
2. Ein Nutzer erstellt eine Automation im visuellen Editor, testet sie als Dry-Run und kann Ausführungen innerhalb von 24h zurückrollen.
|
||||
3. Loop-Guard, Rate-Limit, Idempotenz-Key, Resume-Mode und Trigger-Volumen-Metering pro Tenant verhindern Runaway-Automationen.
|
||||
4. AutomationRule (JSON-Graph), AutomationExecution, AutomationStepExecution und AutomationTemplate persistieren Flows und Läufe nachvollziehbar.
|
||||
**Plans**: TBD
|
||||
@@ -281,7 +281,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
1. Click-to-Call und Inbound-Anrufe laufen über den AirCall-Adapter (Apps-Engine-Connection, verschlüsselte OAuth-Tokens); unbekannte Nummern legen automatisch einen Lead an; Anruf-Events stehen als Automation-Trigger bereit (verpasster Anruf → Rückruf-Aufgabe).
|
||||
2. Team-Chat bietet Real-Time-Messaging (WebSocket, Presence, Typing, Read-Receipts, Redis-Pub/Sub) für drei Channel-Typen (public, private, entity-linked), AD-9.
|
||||
3. Chat unterstützt @Mentions, File-Uploads (via Files), Link-Previews und Chat-Verlaufs-Suche.
|
||||
4. Entity-linked Channels erscheinen im Deal-/Projekt-Detail und verknüpfen die Konversation mit der CRM-/Projekt-Entität.
|
||||
4. Entity-linked Channels erscheinen im Projekt-Detail und verknüpfen die Konversation mit der CRM-/Projekt-Entität.
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes
|
||||
|
||||
@@ -294,8 +294,8 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Kalender synchronisiert bidirektional mit **Google** (CalendarAdapter, Auto-Token-Refresh, Webhook-Subscriptions) und legt HiveGrid-Termine an. *(Scope-Schnitt 11.06.: Outlook-Adapter → v2; Interface ist provider-agnostisch gebaut. Booking: eine Zuteilungs-Strategie „Erster der frei ist", Round-Robin/Wahl → v2.)*
|
||||
2. Öffentliche Booking-Seiten berechnen Slots timezone-aware (Verfügbarkeitsregeln, Ausnahmen, Kalender-Sync, Puffer) und legen nach Buchung Lead, Activity und Bestätigungs-Email an (iFrame-Embed).
|
||||
3. Ein Nutzer baut Formulare per Drag-&-Drop mit 12 Feld-Typen, Conditional Logic, SHK-Templates und iFrame-Embed; Post-Submit-Actions (Lead/Deal/Task, Webhook, Email, Automation-Trigger) feuern mit Field-Mapping und ActionLog.
|
||||
4. Angebote durchlaufen Multi-Signer-Signaturen via PandaDoc/DocuSign über die Apps-Engine mit Webhook-Verarbeitung und Status-Updates.
|
||||
3. Ein Nutzer baut Formulare per Drag-&-Drop mit 12 Feld-Typen, Conditional Logic, SHK-Templates und iFrame-Embed; Post-Submit-Actions (Lead/Projekt/Task, Webhook, Email, Automation-Trigger) feuern mit Field-Mapping und ActionLog.
|
||||
4. Angebote durchlaufen Multi-Signer-Signaturen über den self-hosted SES-Standardpfad (AD-25); PandaDoc/DocuSign bleiben optionale BYOK-/Opt-in-Adapter via Apps-Engine.
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes
|
||||
|
||||
@@ -326,7 +326,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Depends on**: Phase 2
|
||||
**Requirements**: REQ-plattform-admin-identität, REQ-plattform-admin-features
|
||||
**Source-Specs**: `_bmad-output/specs/M37-plattform-admin.md`, `_bmad-output/specs/M32-feature-flags.md` (Admin-UI-Teile)
|
||||
**Milestone-PLANs**: 7 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 19); Scope-Schnitt 11.06.: M033 (Flag-Massen-Statistik) + M040 (Plattform-Lead-Pipeline) → v2; 12.06.: M038/M039 (getrennte AdminUser-Identität) → v2, M035 gedünnt. **Neu:** Admin stellt Tenant-Abos zusammen (Konnektoren schalten + Stripe-Positionen — Abo-Baukasten-Mechanik; Self-Service-UI → v2)
|
||||
**Milestone-PLANs**: 7 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 19); Scope-Schnitt 11.06.: M033 (Flag-Massen-Statistik) + M040 (Plattform-Lead-Pipeline) → v2; 12.06.: M038/M039 (getrennte AdminUser-Identität) → v2, M035 gedünnt. **Neu:** Admin stellt Tenant-Abos zusammen (Konnektoren schalten + Legacy-Provider-Positionen — Abo-Baukasten-Mechanik; Self-Service-UI → v2)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Ein Plattform-Admin meldet sich auf `admin.hivegrid.de` an (Host-Routing auf die Admin-Routen, **Cookie-Domain-Trennung** zu app.hivegrid.de, TOTP-Pflicht + MFA-Step-up); Admin-User haben **keinen Tenant-Workspace** und erscheinen in keiner Mandanten-Mitgliederliste; kein Generalschlüssel zwischen Plattform und Mandanten. *(Scope-Schnitt 12.06.: getrennte AdminUser-Identität/AdminRBAC-Parallelwelt = M038/M039 → v2; v1 nutzt Boilerplate-Modell User-Extension + `admin-mfa-gate`.)*
|
||||
2. Ein Admin verwaltet Tenants, Subscription-Overrides und Feature-Flags aus einer zentralen Konsole; das Admin-Dashboard zeigt die Boilerplate-KPIs (Tenants/User/MRR). *(Churn/ARPU/Health-Kacheln → v2.)*
|
||||
@@ -335,15 +335,15 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 20: Billing, Onboarding & Branding
|
||||
**Goal**: Ein neuer Betrieb kommt vom Self-Service-Signup per Auto-SHK-Provisionierung ins Produkt (Wizard-UI → v2), läuft auf Stripe-Billing und kann sein Theme wählen.
|
||||
**Goal**: Ein neuer Betrieb kommt vom Self-Service-Signup per Auto-SHK-Provisionierung ins Produkt (Wizard-UI → v2), läuft auf Legacy-Provider-Billing und kann sein Theme wählen.
|
||||
**Depends on**: Phase 19, Phase 6
|
||||
**Requirements**: REQ-billing-datenmodell, REQ-billing-trial-dunning, REQ-onboarding-demo-daten, REQ-branding-theme-phase1
|
||||
**Source-Specs**: `_bmad-output/specs/M28-subscriptions-billing.md` (⚠ M154-Recut: Abo-Baukasten statt Seat-Typen), `_bmad-output/specs/M29-onboarding.md` (Wizard-UI → v2), `_bmad-output/specs/M31-branding.md`; **Legacy-Referenz:** `archive/hivegrid-cloud/app/config/pricing-plans.ts`
|
||||
**Milestone-PLANs**: 8 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 20); Scope-Schnitt 11.06.: M157 (Wizard-UI) → v2; 12.06.: M030 + M160 gedünnt
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Billing läuft über Stripe als **Abo-Baukasten**: Basis-Preis pro Nutzer + Feature-Module als daten-getriebene Positionen (keine Seat-Typen — M154-Recut); Trial mit Dunning (Tag 1/3/7) und Frozen-Modus (Read-Only-Middleware) greift; konkrete Preise geparkt (AD-15, Kostenmatrix folgt). Verbrauchs-Zähler + Drossel aktiv, Aufschlags-Abrechnung → v2.
|
||||
1. Billing läuft über Legacy-Provider als **Abo-Baukasten**: Basis-Preis pro Nutzer + Feature-Module als daten-getriebene Positionen (keine Seat-Typen — M154-Recut); Trial mit Dunning (Tag 1/3/7) und Frozen-Modus (Read-Only-Middleware) greift; konkrete Preise geparkt (AD-15, Kostenmatrix folgt). Verbrauchs-Zähler + Drossel aktiv, Aufschlags-Abrechnung → v2.
|
||||
2. Ein neuer Tenant wird nach Signup automatisch SHK-provisioniert (Seeds M158, ohne Wizard-UI — die folgt v2); Trial-Freischaltung durch Plattform-Admin (wie spec'd).
|
||||
3. Optionale Demo-Daten (5 Contacts, 3 Deals, 2 Projekte, Tasks) lassen sich per Settings-Knopf provisionieren und via is_demo_data-Flag löschen. *(Scope-Schnitt 12.06.: KI-Walkthrough → v2 mit Wizard; einfaches Löschen statt Multi-Step-Bulk.)*
|
||||
3. Optionale Demo-Daten (Kontakte, Projekte in Sales-/Auftragssichten, Tasks) lassen sich per Settings-Knopf provisionieren und via is_demo_data-Flag löschen. *(Scope-Schnitt 12.06. + AD-26: KI-Walkthrough entfällt; einfaches Löschen statt Multi-Step-Bulk.)*
|
||||
4. Ein Nutzer schaltet zwischen Hell und Dunkel um, flash-frei (SSR-Cookie). *(Scope-Schnitt 12.06.: kein System-Modus — bewusst nur 2 Optionen; `next-themes` aus Boilerplate.)*
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes
|
||||
@@ -355,7 +355,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Source-Specs**: `_bmad-output/specs/M35-dsgvo-export.md` (M34/M36/M38/M39 → v2)
|
||||
**Milestone-PLANs**: 2 PLANs in `docs/milestones/` → siehe `.planning/PHASE-SOURCES.md` (Phase 21)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Ein Nutzer kann einen DSGVO-ZIP-Export (72h-signed-URL) anstoßen; eine Tenant-Löschung führt Crypto-Shredding (Per-Tenant-DEK), Stripe-Cancel und Storage/Cache-Bereinigung durch (AD-8).
|
||||
1. Ein Nutzer kann einen DSGVO-ZIP-Export (72h-signed-URL) anstoßen; eine Tenant-Löschung führt Crypto-Shredding (Per-Tenant-DEK), Legacy-Provider-Cancel und Storage/Cache-Bereinigung durch (AD-8).
|
||||
2. DSGVO-Auskunft funktioniert als dokumentiertes manuelles Playbook: Suche → Kontakt-Detail (Activity-Stream) → CSV-Export → Anwalts-Vorlage → Frist-Task (+25 Tage). AVV-Vorlage liegt vor (Externals E6).
|
||||
3. Das In-App-Feedback-Widget (Bug/Feature/Frage, Screenshot, Rate-Limit) ist live und speist die Admin-Support-Inbox (P19, generische Quelle); eine Minimal-FAQ-Seite existiert.
|
||||
4. Monteure arbeiten mobil über die PWA/responsive Web-App (mobile-first Screens + P9-Offline) — der PWA-Ausbau (Installierbarkeit/Web-Push) folgt v2; ein nativer Client ist nicht vorgesehen (AD-17, PWA-only).
|
||||
@@ -387,7 +387,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Source-Specs**: `_bmad-output/specs/M09-email-inbox.md` (OAuth-Sync/Inbox/Threading-Teile)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. OAuth-Connect Gmail (Pub/Sub-Watch) + Microsoft (Graph-Subscription, 3-Tage-Renewal-Worker); Push-Sync, Initial-Sync 90 Tage resumebar.
|
||||
2. Auto-Match (From-Adresse, Subject-Pattern, 1-offener-Deal-Heuristik) mit manuellem Override; Inbox-Liste + Detail mit Thread, sicherem HTML-Render, Reply/Forward.
|
||||
2. Auto-Match (From-Adresse, Subject-Pattern `[Projekt #123]`, 1-offenes-Projekt-Heuristik) mit manuellem Override; Inbox-Liste + Detail mit Thread, sicherem HTML-Render, Reply/Forward.
|
||||
3. `email.read_own`/`read_all` filtern Liste + Activity-Stream.
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes
|
||||
@@ -395,8 +395,8 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
|
||||
### Phase 24: Self-Service-Billing & Onboarding-Wizard (v2)
|
||||
**Goal**: Tenants buchen/wechseln Pläne self-service über Web-Checkout und durchlaufen einen branchen-getriebenen Onboarding-Wizard.
|
||||
**Depends on**: Phase 20 (Stripe/Billing-Fundament)
|
||||
**Retro-Integration**: KI-Walkthrough-Coach wird von Phase 25 rückwirkend in den Wizard eingehängt (kein harter Forward-Dep).
|
||||
**Depends on**: Phase 20 (Legacy-Provider/Billing-Fundament)
|
||||
**Retro-Integration**: Kein KI-Walkthrough-Coach nach AD-26; Phase 25 liefert MCP-Server statt eigenem LLM/Coach.
|
||||
**Milestones**: M157 (+ M28-Self-Service-Delta, Block P)
|
||||
**Source-Specs**: `_bmad-output/specs/M28-subscriptions-billing.md` (Block P), `M29-onboarding.md`
|
||||
**Success Criteria** (what must be TRUE):
|
||||
@@ -407,18 +407,18 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**UI hint**: yes
|
||||
**Risiko**: Grund für Self-Service-v2 sind die noch fehlenden konkreten Preise (v1 EXT-14, Kostenmatrix mit Partner) — **nicht** Apple-Store-Auflagen (PWA-only, kein App-Store, Cut #14/AD-17); Web-Checkout direkt.
|
||||
|
||||
### Phase 25: KI-Tiefe — HiveMind/RAG (v2)
|
||||
> ⛔ **KI→MCP-Pivot ([[AD-26]]): Phase 25 (HiveMind/RAG) zurückgestellt → ersetzt durch MCP-Server ([[VR-MCP]] / [[VD-MCP]]).** HiveGrid hostet kein eigenes RAG/Embedding/Whisper, sondern wird Remote-MCP-Server für externe KI-Clients. v2-P4 ist auf MCP umgeschnitten (siehe [[ROADMAP-V2]] v2-P4). Die HiveMind/RAG/Whisper-Inhalte unten sind als **v3-Kandidat zurückgestellt** und nur historisch erhalten.
|
||||
**Goal**: ~~Aus dem v1-LLM-Gateway wird semantische Tiefe — RAG über Tenant-Daten, EU-Whisper, perspektivisch der autonome HiveMind-Agent.~~ **→ ersetzt durch Remote-MCP-Server ([[VR-MCP]]).**
|
||||
**Depends on**: ~~Phase 13 (LLM-Gateway)~~ (entfällt — [[AD-26]]), Phase 11 (Outbox/Suche)
|
||||
**Retro-Integration**: ~~Phase 29 (Help) registriert seine Artikel rückwirkend als RAG-Korpus~~ → RAG entfällt; Help-Artikel können optional als MCP-Resource exponiert werden.
|
||||
**Milestones**: ~~M087 (+ Whisper-EU, pgvector-Hybrid-Suche, zweiter EU-Provider)~~ → gelöscht ([[AD-26]]); neuer v2-Neuschnitt = [[VR-MCP]] (kein M0xx-Milestone)
|
||||
**Source-Specs**: ~~`_bmad-output/specs/M18-ki-layer.md` (RAG/HiveMind/Whisper-Teile)~~ (entfällt — [[AD-26]])
|
||||
**Success Criteria** ~~(what must be TRUE)~~ — **zurückgestellt (v3-Kandidat), überholt durch [[AD-26]]**:
|
||||
1. ~~Embedding-Pipeline → `TenantEmbedding` (pgvector), Re-Embed per Hash-Diff, Permission-Pre-Filter vor Vector-Search; hybride globale Suche (Volltext+Vektor, RRF).~~
|
||||
2. ~~Whisper-EU transkribiert Voice-Notes async; Multi-Provider-Failover (Mistral→Scaleway→AlephAlpha) + Promptfoo in CI.~~
|
||||
3. ~~HiveMind-Agent liefert proaktive Vorschläge mit Action-Boundary (keine schreibenden Aktionen ohne Bestätigung), Audit-Trail, Scheduled-Actions.~~
|
||||
**Plans**: ~~TBD~~ → siehe [[VR-MCP]]/[[VD-MCP]]
|
||||
### Phase 25: MCP-Server (v2)
|
||||
> ⛔ **KI→MCP-Pivot ([[AD-26]]):** HiveGrid hostet kein eigenes LLM/RAG/Whisper. Phase 25 ist der Remote-MCP-Server für externe KI-Clients. HiveMind/RAG/Whisper sind v3-Kandidaten und keine v2-Baugrundlage.
|
||||
**Goal**: Externe KI-Clients können HiveGrid-Daten über tenant-gescopete Read-Ressourcen lesen und freigegebene Schreib-Aktionen über bestätigungspflichtige Action-Tools ausführen.
|
||||
**Depends on**: Phase 22 (API-Key/Auth/Scopes/Rate-Limit), Phase 2 (Tenant/RLS), Phase 1 (Audit/Permissions)
|
||||
**Retro-Integration**: Help-Artikel und andere Daten können optional als MCP-Resources exponiert werden; keine RAG-/Embedding-Pipeline.
|
||||
**Milestones**: [[VR-MCP]] / [[VD-MCP]] (kein M0xx-Milestone)
|
||||
**Source-Specs**: [[VR-MCP]] / [[VD-MCP]] · [[AD-26]]
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Remote-MCP-Server mit Streamable HTTP und `@modelcontextprotocol/sdk` exponiert Read-Ressourcen für Hauptentitäten.
|
||||
2. Auth = API-Key + Scopes + Rate-Limit aus Phase 22; Tenant wird aus dem Token abgeleitet; jeder Zugriff läuft durch `withTenantScope()`/RLS.
|
||||
3. Action-Tools sind `withPermission()`-gegated, verlangen bei Schreib-Operationen Confirmation und schreiben Audit-Events mit Actor-Kind `MCP/Agent`.
|
||||
**Plans**: TBD → siehe [[VR-MCP]]/[[VD-MCP]]
|
||||
**UI hint**: yes
|
||||
**Constraint**: ~~ausschließlich EU-Provider (DSGVO), AVV mit allen Providern.~~ → MCP: kein LLM-Hosting, daher kein Provider-AVV; Tenant-Scope/RLS + API-Key-Auth gelten.
|
||||
|
||||
@@ -462,7 +462,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
### Phase 29: Help-Center & Help-Desk (v2)
|
||||
**Goal**: Öffentliches file-basiertes Help-Center mit Deep-Links aus der App + In-App-Feedback-Widget (Ausbau), das im Plattform-Admin landet.
|
||||
**Depends on**: Phase 21 (Feedback-Widget→Support-Inbox-Andockpunkt), Phase 19 (Admin)
|
||||
**Retro-Integration**: liefert HelpArticle-Embeddings als RAG-Korpus an Phase 25.
|
||||
**Retro-Integration**: Help-Artikel können optional als MCP-Resources in Phase 25 exponiert werden; kein RAG-/Embedding-Korpus.
|
||||
**Milestones**: M172, M173, M174, M175
|
||||
**Source-Specs**: `_bmad-output/specs/M38-help-center.md`, `M39-help-desk.md`
|
||||
**Success Criteria** (what must be TRUE):
|
||||
@@ -519,7 +519,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
**Source-Specs**: `_bmad-output/specs/M12-telefonie.md` (Provider-Pattern) — ⚠ **HalloPetra selbst ist in keiner Spec; Vorlauf-Spec nötig.**
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. HalloPetra-Adapter implementiert `TelephonyProviderInterface` (connect/triggerOutboundCall/subscribeToWebhooks/verifyWebhookSignature/parseWebhookEvent/getRecordingUrl) ohne Kern-Änderung.
|
||||
2. Webhook-Auto-Match auf Contact/Lead/Deal, Activity + Automation-Trigger; Click-to-Call + Inbound providerunabhängig.
|
||||
2. Webhook-Auto-Match auf Contact/Lead/Projekt, Activity + Automation-Trigger; Click-to-Call + Inbound providerunabhängig.
|
||||
**Plans**: TBD
|
||||
**UI hint**: yes (Setup-/Binding-Flächen)
|
||||
|
||||
@@ -568,7 +568,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
|
||||
| 22. Public-API & Webhooks | 0/TBD | Not started (v2) | - |
|
||||
| 23. Postfach-Sync (Email-Inbox) | 0/TBD | Not started (v2) | - |
|
||||
| 24. Self-Service-Billing & Onboarding-Wizard | 0/TBD | Not started (v2) | - |
|
||||
| 25. KI-Tiefe — HiveMind/RAG | 0/TBD | Not started (v2) | - |
|
||||
| 25. MCP-Server | 0/TBD | Not started (v2) | ersetzt alten HiveMind/RAG-Slot |
|
||||
| 26. Customer-Portal ⭐ | 0/TBD | Not started (v2, ex-P18) | - |
|
||||
| 27. Mobile-PWA-Ausbau | 0/TBD | Not started (v2) | - |
|
||||
| 28. Plattform-Admin-Tiefe | 0/TBD | Not started (v2) | - |
|
||||
|
||||
@@ -40,32 +40,32 @@
|
||||
- **HiveMind/RAG (M087): → v2 — v1 baut die Andock-Adapter:** (a) Outbox-Index-Events (existiert via P11-Indexer), (b) `RetrievalProvider`-Interface im KI-Unterbau, (c) Bestätigungs-Grenze („KI schreibt nie ohne User-OK") entsteht v1 via M086 und wird von HiveMind geerbt. REQ-ki-hivemind + REQ-ki-rag-embeddings → v2.
|
||||
- **Whisper: → v2** (REQ-ki-whisper). „Cool, aber aktuell nicht nötig."
|
||||
- **Unterbau bleibt v1:** M083 Gateway, M084 Multi-Provider-Fallback, M085 Promptfoo, M088 Token-Limits (Pay-as-you-go-Abrechnung weiterhin per Inversion → P20/v2; lokales Credits-Konto bleibt).
|
||||
- **Folge-Entscheidung (Veto möglich): P11-Suche wird v1 Keyword-only** (Meilisearch); pgvector/Hybrid-RRF → v2 mit HiveMind. AD-5 wird sequenziert (Keyword v1, Hybrid v2). REQ-suche-hybrid wird entsprechend gedünnt.
|
||||
- **Folge-Entscheidung:** P11-Suche bleibt Keyword-only (Meilisearch); pgvector/Hybrid-RRF/HiveMind entfällt nach AD-26 bzw. ruht als v3-Kandidat. REQ-suche-hybrid ist entsprechend auf Meilisearch gedünnt.
|
||||
|
||||
## Entscheidung #6 — Keep-Entscheidungen (2026-06-11)
|
||||
|
||||
- **Team-Chat (M110–M114): bleibt v1** — „ultra wichtig". WebSocket-Layer (AD-9) damit ebenfalls v1.
|
||||
- **HR-Hub (M133–M140): bleibt v1 komplett** — „genau wie HR".
|
||||
- **Booking + Kalender-Sync (M118–M120, M126–M129): bleiben v1** („und ähnliches" → Signatur M130–M132 vermutlich auch — Bestätigung ausstehend).
|
||||
- **Stripe (M153–M156): bleibt v1.** ⚠ Offen: Rücksprache mit Partner zu **pauschalem Pricing** — falls pauschal, dünnt M156 (Metering nur als Datensammlung, keine Aufschlags-Abrechnung; Seat-Quota-Logik vereinfacht).
|
||||
- **Konsequenz Externals:** Google-OAuth-Verifizierung (A2) bleibt im Sprint relevant (Kalender-Lesepfad braucht Calendar-Scope!), Stripe-Live (A5) bleibt, PandaDoc (B4) bleibt (falls Signatur bestätigt).
|
||||
- **Legacy-Provider (M153–M156): bleibt v1.** ⚠ Offen: Rücksprache mit Partner zu **pauschalem Pricing** — falls pauschal, dünnt M156 (Metering nur als Datensammlung, keine Aufschlags-Abrechnung; Seat-Quota-Logik vereinfacht).
|
||||
- **Konsequenz Externals:** Google-OAuth-Verifizierung (A2) bleibt im Sprint relevant (Kalender-Lesepfad braucht Calendar-Scope!), Legacy-Provider-Live (A5) bleibt, PandaDoc (B4) bleibt (falls Signatur bestätigt).
|
||||
|
||||
## Entscheidung #7 — Kommunikation & Billing-Modell (2026-06-11)
|
||||
|
||||
- **Mail-Inbox/Postfach-Sync → v2:** M096-OAuth-Teil, M097 (Inbox/Threading/Auto-Verknüpfung), M098-Provider-Versand, M099-Teile → v2. **Bleibt v1:** Sende-Domain-Verifikation (aus M096 herausgelöst), Mailjet-Versand, Bounce/Sperrliste (Marketing-Anteil), Inbound-Adresse→Lead. **Empfohlen (klein, ~½ MS): BCC-Logging-Brücke** (`crm@tenant…` als BCC → Mail wird am Kontakt protokolliert, nutzt Inbound-Infrastruktur, kein Google-OAuth). → **Google-Verifizierung (A2) nur noch für Kalender-Scopes nötig, Gmail-Scopes raus.**
|
||||
- **Telefonie bleibt v1** (AirCall M106–M109, Anruf-Trigger für Automation). ⚠ **~~inkl. HalloPetra~~ überholt durch #10:** HalloPetra (M105) → v2 (Phase 33). AirCall bleibt v1.
|
||||
- **Marketing bleibt voll** (Re-Bestätigung von #4).
|
||||
- **Stripe: einheitliches Billing** — keine Büro-/Monteur-**Seat**-Unterscheidung mehr (M154 kollabiert; Interpretation „Seats" vom User rückbestätigen lassen — NICHT die SHK-Seeds!). Varianten-Entscheidung (pro Nutzer einheitlich / pauschal pro Mandant / Größenstaffel) liegt beim Partner-Gespräch. M156-Aufschlags-Billing entfällt; Verbrauchs-Counter + Fair-Use-Drossel bleiben als Kostenschutz.
|
||||
- **Legacy-Provider: einheitliches Billing** — keine Büro-/Monteur-**Seat**-Unterscheidung mehr (M154 kollabiert; Interpretation „Seats" vom User rückbestätigen lassen — NICHT die SHK-Seeds!). Varianten-Entscheidung (pro Nutzer einheitlich / pauschal pro Mandant / Größenstaffel) liegt beim Partner-Gespräch. M156-Aufschlags-Billing entfällt; Verbrauchs-Counter + Fair-Use-Drossel bleiben als Kostenschutz.
|
||||
|
||||
## Entscheidung #8 — Abschluss der Streich-Session (2026-06-11)
|
||||
|
||||
- **Signatur (M130–M132): bleibt v1** (bestätigt — war Teil von „Booking, Kalender und ähnliches").
|
||||
- **Onboarding-Wizard-UI (M157): → v2.** Seeds (M158) + Demo-Daten (M160) bleiben; Branchen-Framework (M159) v1 nur SHK. Signup → Auto-SHK-Seed ohne Wizard.
|
||||
- **Billing-Modell final: „Abo-Baukasten"** — Basis-Preis pro Nutzer (keine Seat-Typen) + Feature-Module als zubuchbare Stripe-Positionen. **M154-Recut:** Seat-Typ-Modell ersetzt durch Entitlement-Schicht = Feature-Flag-Engine (P1) × `packages/entitlements` × daten-getriebene Stripe-Items (M153). **v1 = Mechanik** (Admin stellt Tenant-Abos in P19 zusammen), **v2 = Self-Service-Baukasten-UI** (Kunde toggelt Features mit Preis-Vorschau). **Kostenmatrix** = Business-Aufgabe mit Partner, blockt Bau nicht (AD-15). Zähler + Drossel bleiben v1; Extra-Usage-Abrechnung wird separat besprochen (offen, blockt nicht).
|
||||
- **Billing-Modell final: „Abo-Baukasten"** — Basis-Preis pro Nutzer (keine Seat-Typen) + Feature-Module als zubuchbare Legacy-Provider-Positionen. **M154-Recut:** Seat-Typ-Modell ersetzt durch Entitlement-Schicht = Feature-Flag-Engine (P1) × `packages/entitlements` × daten-getriebene Legacy-Provider-Items (M153). **v1 = Mechanik** (Admin stellt Tenant-Abos in P19 zusammen), **v2 = Self-Service-Baukasten-UI** (Kunde toggelt Features mit Preis-Vorschau). **Kostenmatrix** = Business-Aufgabe mit Partner, blockt Bau nicht (AD-15). Zähler + Drossel bleiben v1; Extra-Usage-Abrechnung wird separat besprochen (offen, blockt nicht).
|
||||
|
||||
## Entscheidung #9 — Konnektor-Pflicht + Legacy-Plan-Referenz (2026-06-11)
|
||||
|
||||
- **Konnektor-Pflicht pro Feature (v1, hart):** Jedes Modul ist eigenständig enable-/disable-bar und plan-gebunden schaltbar. Wird **neuer Integration-Contract in P1** („Entitlement-Gate-Pflicht": Modul registriert Gate, Sidebar/Routes/Actions prüfen `tenantHasModule()`), Pflicht-Checkpunkt jeder Modul-Phase. Mechanik: Boilerplate-Flag-Engine (hat bereits `plan-tier`-Targeting!) × `packages/entitlements` × Stripe-Items.
|
||||
- **Konnektor-Pflicht pro Feature (v1, hart):** Jedes Modul ist eigenständig enable-/disable-bar und plan-gebunden schaltbar. Wird **neuer Integration-Contract in P1** („Entitlement-Gate-Pflicht": Modul registriert Gate, Sidebar/Routes/Actions prüfen `tenantHasModule()`), Pflicht-Checkpunkt jeder Modul-Phase. Mechanik: Boilerplate-Flag-Engine (hat bereits `plan-tier`-Targeting!) × `packages/entitlements` × Legacy-Provider-Items.
|
||||
- **Legacy-Plan-Namen gefunden:** **Core / Swarm / Colony** (D006, 29€/49€/Contact-Sales pro Seat) in `~/Documents/GitHub/archive/hivegrid-cloud/app/config/pricing-plans.ts`. Datei enthält komplette Konnektor-Blaupause: 14 gegatete Module, 6 Quota-Dimensionen (inkl. aiTokensPerMonth), 6 Plan-Flags. **→ Als Source-Spec für den P20-Recut (M154→Abo-Baukasten) übernehmen**; Struktur portieren, Inhalte (Preise/Tiers vs. À-la-carte) = Kostenmatrix-Gespräch mit Partner.
|
||||
|
||||
## Entscheidung #10 — Feature-Tiefe-Runde (2026-06-11)
|
||||
@@ -84,9 +84,9 @@
|
||||
|
||||
**→ v2 (committed, sequenziert):** Mobile-PWA-Ausbau (PWA-Recut, M167–171; Expo/Capacitor verworfen — AD-17) · Customer-Portal (M141–146) · HiveMind/RAG (M087) + Whisper + pgvector/Hybrid-Suche · Postfach-Sync (M096-OAuth/M097/M098; Domain-Verifikation bleibt) · Onboarding-Wizard-UI (M157) · Self-Service-Abo-Baukasten-UI · AlephAlpha-Provider · Pay-as-you-go-Abrechnung
|
||||
|
||||
**Bleibt v1 (User-Entscheidung):** komplette Kern-Kette P1–P10 · Suche (Keyword) + Dashboard · Automation VOLL · KI-Unterbau + Sprach-zu-Automation (exklusiv Automation) · **Marketing voll** · **Telefonie (AirCall) voll** ~~(inkl. HalloPetra)~~ *(überholt durch #10: HalloPetra/M105 → v2, Phase 33)* · **Team-Chat voll** · **HR voll** · Booking + Kalender + Forms + **Signatur** · Stripe (Abo-Baukasten-Mechanik) · Plattform-Admin · Seeds + Demo-Daten
|
||||
**Bleibt v1 (User-Entscheidung):** komplette Kern-Kette P1–P10 · Suche (Keyword) + Dashboard · Automation VOLL · KI-Unterbau + Sprach-zu-Automation (exklusiv Automation) · **Marketing voll** · **Telefonie (AirCall) voll** ~~(inkl. HalloPetra)~~ *(überholt durch #10: HalloPetra/M105 → v2, Phase 33)* · **Team-Chat voll** · **HR voll** · Booking + Kalender + Forms + **Signatur** · Legacy-Provider (Abo-Baukasten-Mechanik) · Plattform-Admin · Seeds + Demo-Daten
|
||||
|
||||
**Bilanz: v1 ≈ 160 Milestones** (–16 vs. 176) ≈ 2,7 MS/Dev/Tag — weiterhin straff; Gates + neue Notfall-Leiter zwingend. Verbleibende harte Externals: **KWP, Google (nur noch Calendar-Scopes), Stripe-Live, PandaDoc/DocuSign, AirCall/HalloPetra, Mailjet-Domain-Warm-up.**
|
||||
**Bilanz: v1 ≈ 160 Milestones** (–16 vs. 176) ≈ 2,7 MS/Dev/Tag — weiterhin straff; Gates + neue Notfall-Leiter zwingend. Verbleibende harte Externals: **KWP, Google (nur noch Calendar-Scopes), Legacy-Provider-Live, PandaDoc/DocuSign, AirCall/HalloPetra, Mailjet-Domain-Warm-up.**
|
||||
|
||||
---
|
||||
|
||||
@@ -109,12 +109,12 @@
|
||||
- [ ] Marketing (P14-Hälfte, 5 MS)
|
||||
- [ ] Public-API + Help-Center/Desk (P21, 7 MS)
|
||||
- [ ] Booking / Signatur / Kalender-Sync (P16-Teile, 10 MS)
|
||||
- [ ] Billing/Stripe + Onboarding-Wizard (P20-Teile, ~7 MS)
|
||||
- [ ] Billing/Legacy-Provider + Onboarding-Wizard (P20-Teile, ~7 MS)
|
||||
- [ ] USP-Tiefe P12/P13 (RAG/HiveMind/Whisper dünnen?)
|
||||
|
||||
## Entscheidung #11 — Boilerplate-Recheck-Runde (2026-06-12)
|
||||
|
||||
Basis: Sweep über alle 37 Reports in `docs/boilerplate-mapping/` + Code-Stichproben. Erkenntnis: Boilerplate-HIGH-Coverage-Milestones (Auth, Billing, i18n, Mail, Worker, Impersonation) NICHT cutten — fast geschenkt. Geschnitten/gedünnt werden die teuren Spec-Deltas.
|
||||
Basis: historischer Sweep über die inzwischen entfernten Boilerplate-Mapping-Reports + Code-Stichproben. Erkenntnis: Boilerplate-HIGH-Coverage-Milestones (Auth, Billing, i18n, Mail, Worker, Impersonation) NICHT cutten — fast geschenkt. Geschnitten/gedünnt werden die teuren Spec-Deltas.
|
||||
|
||||
**→ v2 (5 ganze Milestones):**
|
||||
- **M027 API-Keys & Tarif-Gates** — Public-API ist v2; Keys ohne Endpoints sinnlos. Boilerplate-`ApiKey`-Modell + Routen bleiben ungenutzt im Code liegen (Vorbereitung gratis). Lead-Push von Fremd-Websites läuft über M047-Quellen-Token, nicht über API-Keys.
|
||||
|
||||
@@ -139,7 +139,7 @@ Buttons (primär/sekundär/destruktiv/ghost) · Badges + Status-Pills · Tags (f
|
||||
| Screen | Inhalt | Typ |
|
||||
|---|---|---|
|
||||
| Admin-Dashboard | **Basis-KPIs: Tenants, aktive User, MRR** + System-Health-Ampel + Queue-Status. *(Churn/ARPU/Sales-Pipeline — v2)* | [KPI] |
|
||||
| Tenant-Liste + Detail | Liste: Name, Slug, Status, Plan, MRR, User-Anzahl · Detail: Stammdaten, Status-Wechsler, **Abo-Zusammenstellung (Feature-Konnektoren togglen + Stripe-Positionen — Abo-Baukasten!)**, Trial-Verlängerung, Notizen (Markdown, gepinnt), **Impersonate mit Begründungs-Pflicht** | [A2/A3+A6] |
|
||||
| Tenant-Liste + Detail | Liste: Name, Slug, Status, Plan, MRR, User-Anzahl · Detail: Stammdaten, Status-Wechsler, **Abo-Zusammenstellung (Feature-Konnektoren togglen + Legacy-Provider-Positionen — Abo-Baukasten!)**, Trial-Verlängerung, Notizen (Markdown, gepinnt), **Impersonate mit Begründungs-Pflicht** | [A2/A3+A6] |
|
||||
| Manuelle Tenant-Anlage | Name, Slug, Branche, Admin-Email, Initial-Module, Trial-Dauer, Demo-Daten-Häkchen, Begründung | [A4] |
|
||||
| User-Liste + Detail | Email, Name, Tenants, 2FA-Status, Sessions · Ops: 2FA-Reset, Session-Revoke, Deaktivieren | [A2/A3] |
|
||||
| Feature-Flags [`/admin/feature-flags`] | Flag-Liste (Key, Status, Targeting), Filter-Editor (plan-tier!), Tenant-Override mit Ablauf. *(Massen-Statistik — v2)* | [A2+A6] |
|
||||
@@ -184,10 +184,10 @@ Buttons (primär/sekundär/destruktiv/ghost) · Badges + Status-Pills · Tags (f
|
||||
| AI-Credits [`/admin/usage/ai`] | Saldo, Verbrauch je Use-Case (Donut), Verlauf, Top-User, **80%-Warnbanner**, Hochrechnung | [KPI+Charts] |
|
||||
| AirCall-Setup + Anrufe | Connect-Card + **User-Binding** (AirCall-User ↔ HiveGrid-User mit Auto-Match) · Anruf-Liste: Nummer, Richtung, Dauer, gematchter Kontakt, Notiz-Snippet · Anruf-Detail: Match-Override, Notiz-Editor (Markdown), Aufnahme-Link · Click-to-Call-Button + eingehender-Anruf-Notification | [A6+A2/A3] |
|
||||
| Sende-Domain [`/settings/integrations/email`] | Domain-Eingabe, **DNS-Records (DKIM/SPF/DMARC) mit Copy-Button**, Verifikations-Status-Ampel, 5-Min-Prüf-Indikator · **BCC-Logging:** Toggle + angezeigte BCC-Adresse + Klartext „Mails erscheinen am Kontakt" *(kein Postfach/Inbox/Compose — v2!)* | [A6] |
|
||||
| Abo-Übersicht [`/admin/subscription`] | Basis-Plan + **gebuchte Feature-Module als Positionsliste mit Preisen** · Status/Trial-Countdown · Verbrauch: Automation-Läufe, AI-Tokens, Speicher mit Quota-Balken · Stripe-Portal-Embed (Rechnungen, Zahlungsmethode) · Pausieren/Kündigen *(kein Self-Service-Modul-Toggle — Admin macht das, v2)* | [A6+KPI] |
|
||||
| Abo-Übersicht [`/admin/subscription`] | Basis-Plan + **gebuchte Feature-Module als Positionsliste mit Preisen** · Status/Trial-Countdown · Verbrauch: Automation-Läufe, AI-Tokens, Speicher mit Quota-Balken · Legacy-Provider-Portal-Embed (Rechnungen, Zahlungsmethode) · Pausieren/Kündigen *(kein Self-Service-Modul-Toggle — Admin macht das, v2)* | [A6+KPI] |
|
||||
| Demo-Daten [`/admin/onboarding/demo-data`] | **Settings-Knopf „Beispieldaten anlegen"** (25 Datensätze, Klartext-Liste was kommt) · Demo-Liste mit Zähler + Lösch-Funktion · „Demo ausblenden"-Filter erscheint in allen Listen *(kein Wizard, kein KI-Walkthrough — v2)* | [A6] |
|
||||
| Erste-Schritte-Checkliste [`/onboarding/checklist`] | Items mit Status-Häkchen, Klartext, CTA-Link je Item (KWP verbinden → v2-Item ausgeblendet), manuell abhakbar, ausblendbar | [A2] |
|
||||
| Privacy-Hub [`/admin/privacy`] | Export: Job-Liste mit Status/Progress/Download (72h-Link) · **Tenant-Löschung: Multi-Step** (Erklärung 30-Tage-Grace → Export-Angebot → Stripe-Hinweis → Passwort+2FA → **Slug als Bestätigungs-Phrase**) + Grace-Countdown mit Abbruch · Subprozessoren-Tabelle (Name, Land, EU-Marker, Zweck, DPA-Link) · AVV: aktuelle Version als PDF + Akzeptanz-Historie | [A2+A5] |
|
||||
| Privacy-Hub [`/admin/privacy`] | Export: Job-Liste mit Status/Progress/Download (72h-Link) · **Tenant-Löschung: Multi-Step** (Erklärung 30-Tage-Grace → Export-Angebot → Legacy-Provider-Hinweis → Passwort+2FA → **Slug als Bestätigungs-Phrase**) + Grace-Countdown mit Abbruch · Subprozessoren-Tabelle (Name, Land, EU-Marker, Zweck, DPA-Link) · AVV: aktuelle Version als PDF + Akzeptanz-Historie | [A2+A5] |
|
||||
| User-Daten-Export [`/settings/account/data-export`] | Klartext was exportiert wird, Start-Button, Job-Status mit Download | [A6] |
|
||||
| **Feedback-Widget** [global, klein] | Schwebender Button → Mini-Form (Kategorie, Text, Screenshot-Häkchen) → landet in Admin-Support-Inbox | [A5-klein] |
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ css: |-
|
||||
|---|---|---|---|
|
||||
| A1 | **KWP:** Callback-URL zurückmelden (`https://app.hivegrid.de/integrations/kwp/oauth/callback`) + **bnWin.net auf 8.1 N3** bei Geerkens-IT | v2-P10 (erstes v2-Modul) | ✅ entschärft — Zugang lt. KWP-Mail ~**19.07.** (nach Launch); kein Sprint-Risiko mehr, nur Vorbereitung |
|
||||
| A2 | **Google Cloud OAuth-App: nur noch Calendar-API-Scope** beantragen | P16 Kalender | 🟠 Verifizierung Tage–Wochen; Gmail-Scopes durch Postfach-Cut NICHT mehr nötig |
|
||||
| A5 | **Stripe-Konto + Live-Verifizierung** | P20 | 🟠 Tage; Test-Mode reicht für Dev |
|
||||
| A5 | **Legacy-Provider-Konto + Live-Verifizierung** | P20 | 🟠 Tage; Test-Mode reicht für Dev |
|
||||
| A6 | **Sende-Domain: SPF/DKIM/DMARC für Mailjet + Warm-up** | P1/P2/P14 — **Marketing-Kampagnen brauchen Reputation!** | 🔴 sofort, Warm-up braucht Laufzeit |
|
||||
| B2 | **AirCall**-Konto + Sandbox + Webhooks | P15 | 🟡 Tage |
|
||||
| B4 | **PandaDoc** (Standard) Konto + Sandbox; DocuSign optional | P16 Signatur | 🟡 Tage |
|
||||
@@ -114,7 +114,7 @@ Vorab beschlossen wie die Notfall-Leiter; Detail in `TIMELINE.md`. Kurzform:
|
||||
```
|
||||
Sprosse 1: nächste eigene Phase sofort ziehen (9:00-Sync)
|
||||
Sprosse 2: Track-übergreifend aushelfen (kritischer Pfad: P16, P8/P9)
|
||||
Sprosse 3: Puffer ≥3 Tage → Launch-Vorzieh-Prüfung mit Noah (Stripe live + Staging-Abnahme)
|
||||
Sprosse 3: Puffer ≥3 Tage → Launch-Vorzieh-Prüfung mit Noah (Legacy-Provider live + Staging-Abnahme)
|
||||
Sprosse 4: v2-Vorzieher: (a) KWP-Recut-Vorarbeit (Doku, kein Mock-E2E) → (b) Self-Service-Abo-UI → (c) Postfach-Sync
|
||||
Pflicht: pro gewonnener Woche ≥1 Tag Härtung (Bug-Bash/Lasttest/Security)
|
||||
NIEMALS: Cut-Rückholung in Eigenregie · Goldrand an internen Screens · Gates verschieben (außer Sprosse 3)
|
||||
|
||||
@@ -26,14 +26,14 @@ css: |-
|
||||
|
||||
# HiveGrid 2.0 — Sprint-Tagesplan (ticket-genau)
|
||||
|
||||
> ⛔ **KI→MCP-Pivot ([[AD-26]], 2026-06-22) — Tagesplan-Delta:** **R-P13** (KI-Layer) + **D-P13** + **EXT-11** (Mistral-Key) + Milestones **M083–M088** entfallen. **R-P12b** ist neues letztes v1-Glied (Sprach→Automation gestrichen → reiner Flow-Editor); der **09.07.** wird Härtungstag (Freeze/Launch unverändert). In v2: **VR-P25/VD-P25** (HiveMind/RAG) zurückgestellt → **[[VR-MCP]]/[[VD-MCP]]** (MCP-Server) im Slot 18.–20.07. Die KI-Zeilen in den Tabellen unten sind insoweit überholt — autoritativ sind [[AD-26]] + die Ticket-Docs.
|
||||
> ⛔ **KI→MCP-Pivot ([[AD-26]], 2026-06-22):** **R-P13** (KI-Layer), **D-P13**, **EXT-11** (Mistral-Key) und Milestones **M083–M088** entfallen. **R-P12b** ist das letzte v1-Glied der Raffael-Kette; Sprach→Automation ist gestrichen. In v2 ersetzt **[[VR-MCP]]/[[VD-MCP]]** den alten P25-HiveMind/RAG-Slot.
|
||||
|
||||
<p class="lead">Jedes Ticket an seinem Start-Tag · ID · Wer · Fenster (Start→Ziel) · Aufgabe · 152 Sprint-Tickets (v1: 110 · v2: 42) + 2 v3-Backlog</p>
|
||||
<p class="lead">Jedes Ticket an seinem Start-Tag · ID · Wer · Fenster (Start→Ziel) · Aufgabe · 149 Sprint-Tickets (v1: 107 · v2: 42) + 2 v3-Backlog</p>
|
||||
<p class="meta">Generiert aus Plane (Single Source) · Arbeitsrhythmus Mo–So ≥8 h · „bis" = Ziel-/Fertig-Datum (Mehrtages-Tickets laufen bis dahin) · rot = Meilenstein/Gate-Tag · <span style="color:#b91c1c">rote Ticket-ID</span> = Block 2 (Partner-API offen, agil). Neu erzeugen: <code>python3 .planning/plane-sync/gen_dayplan.py</code></p>
|
||||
|
||||
## Woche 1 · Di 16.–Fr 19.06. — Fundament & Identität · 40 Tickets
|
||||
*→ Gate W1 (Fr 19.06.)*
|
||||
### Di 16.06. · 24 Ticket(s)
|
||||
### Di 16.06. · 23 Ticket(s)
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
|---|---|---|---|
|
||||
@@ -45,11 +45,10 @@ css: |-
|
||||
| EXT-04 | Raffael | 17.06. | KWP: Callback-URLs melden + bnWin.net 8.1 N3 |
|
||||
| EXT-05 | Raffael | 16.06. | Noah: 4 Gate-Freitags-Slots blocken |
|
||||
| EXT-06 | Raffael | 17.06. | Hetzner-Projekt HiveGrid: S3+Token + DEK-Master-Key |
|
||||
| EXT-07 | Raffael | 26.06. | Stripe-Konto + Live-Verifizierung |
|
||||
| EXT-07 | Raffael | 26.06. | Legacy-Provider-Konto + Live-Verifizierung |
|
||||
| EXT-08 | Raffael | 30.06. | AirCall-Konto + Sandbox + Webhooks |
|
||||
| EXT-09 | Raffael | 03.07. | PandaDoc-Sandbox (Provider-Wahl) |
|
||||
| EXT-09 | Raffael | 03.07. | Signatur-Provider-Entscheid: SES default, PandaDoc optional BYOK |
|
||||
| EXT-10 | Raffael | 18.06. | Google-Cloud OAuth + Calendar-Scope-Antrag |
|
||||
| EXT-11 | Raffael | 18.06. | Mistral API-Key |
|
||||
| EXT-12 | Raffael | 19.06. | Geo-IP + HIBP prüfen/anbinden |
|
||||
| H-LISTE | Haris | 17.06. | Zielkundenliste SHK (KWP-Nutzer-Fokus) |
|
||||
| H-PAIN | Haris | 17.06. | Painpoint-Gespräch mit Noah |
|
||||
@@ -143,7 +142,7 @@ css: |-
|
||||
| AK-DEMO1 | Akeshan | 26.06. | Demo-Tenant + SHK-Demo-Daten für Noah-Demo 1 |
|
||||
| AK-P17b | Akeshan | 26.06. | P17 HR Urlaub/Krank + Surveys + Self-Service |
|
||||
| D-ADMIN | David | 26.06. | Plattform-Admin-Compose |
|
||||
| H-PAYLINK | Haris | 26.06. | Stripe Payment Link (Founding-Deal) |
|
||||
| H-PAYLINK | Haris | 26.06. | Mollie Payment Link (Founding-Deal) |
|
||||
| N-DEMO1 | Dejan/Haris | 26.06. | Demo 1 abnehmen (CRM klickbar) |
|
||||
|
||||
## Woche 3 · Sa 27.06.–Fr 03.07. — Kern-Kette + Kommunikation · 23 Tickets
|
||||
@@ -155,7 +154,7 @@ css: |-
|
||||
| AK-P14a | Akeshan | 28.06. | P14 Sende-Domain-Verifikation + BCC-Brücke + Tracking |
|
||||
| D-P14 | David | 29.06. | Screens P14 — Email & Marketing (ohne Kampagnen-Editor) |
|
||||
| D-P8 | David | 29.06. | Screens P8 — Projekt & Pipelines (Kern, ohne Kanban) |
|
||||
| R-P8a | Raffael | 28.06. | P8 Pipeline-Engine + Projekt-CRUD + Auto-Verkaufsfall |
|
||||
| R-P8a | Raffael | 28.06. | P8 Pipeline-Engine + Projekt-CRUD + Auto-Projekt |
|
||||
### So 28.06. · 2 Ticket(s)
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
@@ -220,7 +219,7 @@ css: |-
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| AK-P16d | Akeshan | 07.07. | P16 Angebot/Signatur (PandaDoc) |
|
||||
| AK-P16d | Akeshan | 07.07. | P16 Angebot/Signatur (Self-hosted SES; PandaDoc optional BYOK) |
|
||||
| D-P12 | David | 07.07. | Screens P12 — Automation (ohne Flow-Editor) |
|
||||
| D-POLISH | David | 09.07. | Politur extern sichtbarer Flächen + Visual-Reg-Baselines |
|
||||
| H-WEBINAR | Haris | 10.07. | Webinar-Einladungen + Moderation |
|
||||
@@ -230,23 +229,21 @@ css: |-
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| R-P12b | Raffael | 08.07. | P12 Visueller Flow-Editor + Dry-Run + 24h-Undo + Sprach-Andock |
|
||||
| R-P12b | Raffael | 08.07. | P12 Visueller Flow-Editor + Dry-Run + 24h-Undo |
|
||||
### Mi 08.07. · 2 Ticket(s)
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| AK-P20 | Akeshan | 08.07. | P20 Billing (Stripe Abo-Baukasten) + Onboarding + Branding |
|
||||
| AK-P20 | Akeshan | 08.07. | P20 Billing (Legacy-Provider Abo-Baukasten) + Onboarding + Branding |
|
||||
| D-P20 | David | 08.07. | Screens P20 — Billing/Onboarding/Branding (ex-D-REST) |
|
||||
<h3 class="gate">Do 09.07. · 5 Ticket(s)</h3>
|
||||
<h3 class="gate">Do 09.07. · 3 Ticket(s)</h3>
|
||||
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| AK-P21 | Akeshan | 09.07. | P21 DSGVO (Export + Lösch-Workflow) + Feedback-Widget |
|
||||
| D-P13 | David | 09.07. | Screens P13 — KI-Layer (ex-D-REST) |
|
||||
| D-P21 | David | 09.07. | Screens P21 — DSGVO/Launch (ex-D-REST) |
|
||||
| H-FOLLOW | Haris | 10.07. | Trial-Follow-ups + Bilanz |
|
||||
| R-P13 | Raffael | 09.07. | P13 KI-Layer (Mistral-Gateway) |
|
||||
<h3 class="gate">Fr 10.07. · 1 Ticket(s)</h3>
|
||||
|
||||
|
||||
@@ -254,7 +251,7 @@ css: |-
|
||||
|---|---|---|---|
|
||||
| N-GONOGO | Dejan/Haris | 10.07. | Voll-Walkthrough + Go/No-Go |
|
||||
|
||||
## v2-Woche 1 · Mo 13.–So 19.07. — Public-API / Postfach / Abo / KI · 11 Tickets
|
||||
## v2-Woche 1 · Mo 13.–So 19.07. — Public-API / Postfach / Abo / MCP · 11 Tickets
|
||||
*→ Webinar Nr. 1 (Di/Mi 14./15.07.)*
|
||||
### Mo 13.07. · 7 Ticket(s)
|
||||
|
||||
@@ -276,13 +273,13 @@ css: |-
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| VD-P25 | David | 20.07. | Screens P25 — KI-Tiefe/HiveMind |
|
||||
| VD-MCP | David | 20.07. | Screens MCP — MCP-Connection-Verwaltung + Scope/Audit |
|
||||
### Sa 18.07. · 2 Ticket(s)
|
||||
|
||||
| Ticket | Wer | bis | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| VA-P24b | Akeshan | 20.07. | Onboarding-Wizard-UI (M157) |
|
||||
| VR-P25 | Raffael | 20.07. | KI-Tiefe: HiveMind/RAG + Whisper + pgvector-Hybrid (M087) |
|
||||
| VR-MCP | Raffael | 20.07. | MCP-Server: Read-Ressourcen + bestätigungspflichtige Action-Tools |
|
||||
|
||||
## v2-Woche 2 · Mo 20.–So 26.07. — KWP / Portal / Mobile · 15 Tickets
|
||||
*→ KWP-Sync-E2E mit Geerkens (Fr 24.07.)*
|
||||
@@ -378,7 +375,7 @@ css: |-
|
||||
<h2 class="idx">Ticket-Index — alle 152 Tickets (nach Track)</h2>
|
||||
*Zum Nachschlagen, je Track nach Start sortiert. •B2 = Block 2 (Partner-API offen, agil).*
|
||||
|
||||
### Raffael · 42 Tickets
|
||||
### Raffael · 40 Tickets
|
||||
|
||||
| Ticket | Start | bis | v | Aufgabe |
|
||||
|---|---|---|---|---|
|
||||
@@ -386,11 +383,10 @@ css: |-
|
||||
| EXT-04 | 16.06. | 17.06. | v1 | KWP: Callback-URLs melden + bnWin.net 8.1 N3 |
|
||||
| EXT-05 | 16.06. | 16.06. | v1 | Noah: 4 Gate-Freitags-Slots blocken |
|
||||
| EXT-06 | 16.06. | 17.06. | v1 | Hetzner-Projekt HiveGrid: S3+Token + DEK-Master-Key |
|
||||
| EXT-07 | 16.06. | 26.06. | v1 | Stripe-Konto + Live-Verifizierung |
|
||||
| EXT-07 | 16.06. | 26.06. | v1 | Legacy-Provider-Konto + Live-Verifizierung |
|
||||
| EXT-08 | 16.06. | 30.06. | v1 | AirCall-Konto + Sandbox + Webhooks |
|
||||
| EXT-09 | 16.06. | 03.07. | v1 | PandaDoc-Sandbox (Provider-Wahl) |
|
||||
| EXT-09 | 16.06. | 03.07. | v1 | Signatur-Provider-Entscheid: SES default, PandaDoc optional BYOK |
|
||||
| EXT-10 | 16.06. | 18.06. | v1 | Google-Cloud OAuth + Calendar-Scope-Antrag |
|
||||
| EXT-11 | 16.06. | 18.06. | v1 | Mistral API-Key |
|
||||
| EXT-12 | 16.06. | 19.06. | v1 | Geo-IP + HIBP prüfen/anbinden |
|
||||
| R-P1a | 16.06. | 17.06. | v1 | P1 EU-Sterilisation + Contracts + URL-Schemata |
|
||||
| R-P1c | 16.06. | 17.06. | v1 | P1 Hot-Swap-Maschine + Walking-Skeleton + PWA |
|
||||
@@ -402,7 +398,7 @@ css: |-
|
||||
| R-P6 | 22.06. | 24.06. | v1 | P6 CRM-Kern |
|
||||
| EXT-14 | 24.06. | 25.06. | v1 | Kostenmatrix/Preise mit Partner fixieren |
|
||||
| R-P7 | 25.06. | 26.06. | v1 | P7 Lead-Inbox |
|
||||
| R-P8a | 27.06. | 28.06. | v1 | P8 Pipeline-Engine + Projekt-CRUD + Auto-Verkaufsfall |
|
||||
| R-P8a | 27.06. | 28.06. | v1 | P8 Pipeline-Engine + Projekt-CRUD + Auto-Projekt |
|
||||
| R-P8b | 28.06. | 29.06. | v1 | P8 Konfigurierbarer Kanban + Forecast/Reporting |
|
||||
| EXT-13 | 29.06. | 29.06. | v1 | v2-Vorlauf-Externals (Gmail-Scope/Entra) |
|
||||
| R-P9a | 30.06. | 01.07. | v1 | P9 Projekt-Pipeline + Sub-Areas + Rechnung (Sub-Entity v1) |
|
||||
@@ -410,12 +406,11 @@ css: |-
|
||||
| R-P11a | 03.07. | 04.07. | v1 | P11 Globale Suche (Meilisearch, Cmd+K) |
|
||||
| R-P11b | 04.07. | 05.07. | v1 | P11 Dashboard (3 Rollen-Layouts) |
|
||||
| R-P12a | 06.07. | 07.07. | v1 | P12 Automation-Engine + Trigger/Action-Registry + Guards |
|
||||
| R-P12b | 07.07. | 08.07. | v1 | P12 Visueller Flow-Editor + Dry-Run + 24h-Undo + Sprach-Andock |
|
||||
| R-P13 | 09.07. | 09.07. | v1 | P13 KI-Layer (Mistral-Gateway) |
|
||||
| R-P12b | 07.07. | 08.07. | v1 | P12 Visueller Flow-Editor + Dry-Run + 24h-Undo |
|
||||
| VEXT-5 | 13.07. | 19.07. | v2 | Gmail-Restricted-Scope-Verifizierung nachhalten |
|
||||
| VR-P22a | 13.07. | 15.07. | v2 | Public-API: OpenAPI + Read-Endpoints (M161) |
|
||||
| VR-P22b | 15.07. | 17.07. | v2 | Public-API: API-Key-Auth/Scopes/Rate-Limit (M162/M027) + Webhooks (M163) |
|
||||
| VR-P25 | 18.07. | 20.07. | v2 | KI-Tiefe: HiveMind/RAG + Whisper + pgvector-Hybrid (M087) |
|
||||
| VR-MCP | 18.07. | 20.07. | v2 | MCP-Server: Read-Ressourcen + bestätigungspflichtige Action-Tools |
|
||||
| VEXT-1 •B2 | 20.07. | 20.07. | v2 | KWP-API-Zugang nutzen + Recut-Detailplanung gegen API-Doku |
|
||||
| VR-P32a •B2 | 20.07. | 22.07. | v2 | KWP: Connector + Health + Push-Queue (M089/090/091) |
|
||||
| VR-P32b •B2 | 22.07. | 24.07. | v2 | KWP: Contract-Sentinel + Konflikt 'KWP gewinnt' (M092/093) |
|
||||
@@ -445,8 +440,8 @@ css: |-
|
||||
| AK-P16a | 03.07. | 04.07. | v1 | P16 Kalender-Sync (Google) |
|
||||
| AK-P16b | 04.07. | 05.07. | v1 | P16 Booking (Slots + Verfügbarkeit + Storno) |
|
||||
| AK-P16c | 05.07. | 06.07. | v1 | P16 Form-Builder (Editor + Submission + Actions) |
|
||||
| AK-P16d | 06.07. | 07.07. | v1 | P16 Angebot/Signatur (PandaDoc) |
|
||||
| AK-P20 | 08.07. | 08.07. | v1 | P20 Billing (Stripe Abo-Baukasten) + Onboarding + Branding |
|
||||
| AK-P16d | 06.07. | 07.07. | v1 | P16 Angebot/Signatur (Self-hosted SES; PandaDoc optional BYOK) |
|
||||
| AK-P20 | 08.07. | 08.07. | v1 | P20 Billing (Legacy-Provider Abo-Baukasten) + Onboarding + Branding |
|
||||
| AK-P21 | 09.07. | 09.07. | v1 | P21 DSGVO (Export + Lösch-Workflow) + Feedback-Widget |
|
||||
| VA-P23 | 13.07. | 17.07. | v2 | Postfach-Sync: Gmail/Outlook OAuth + Inbox + Threading + Versand (M096-098) |
|
||||
| VA-P24a | 13.07. | 15.07. | v2 | Self-Service-Abo-Baukasten-UI (M28-Delta) |
|
||||
@@ -462,7 +457,7 @@ css: |-
|
||||
| VA-P34 •B2 | 29.07. | 31.07. | v2 | HR-Drittanbieter-Onboarding Memberspot/Boobora (M138) |
|
||||
| VA-P30b | 30.07. | 31.07. | v2 | Querschnitt: Notif-Bündelung/Quiet-Hours (M117) + Sprach-Wechsler/EN (M029) |
|
||||
|
||||
### David · 42 Tickets
|
||||
### David · 41 Tickets
|
||||
|
||||
| Ticket | Start | bis | v | Aufgabe |
|
||||
|---|---|---|---|---|
|
||||
@@ -494,12 +489,11 @@ css: |-
|
||||
| D-P12 | 06.07. | 07.07. | v1 | Screens P12 — Automation (ohne Flow-Editor) |
|
||||
| D-POLISH | 06.07. | 09.07. | v1 | Politur extern sichtbarer Flächen + Visual-Reg-Baselines |
|
||||
| D-P20 | 08.07. | 08.07. | v1 | Screens P20 — Billing/Onboarding/Branding (ex-D-REST) |
|
||||
| D-P13 | 09.07. | 09.07. | v1 | Screens P13 — KI-Layer (ex-D-REST) |
|
||||
| D-P21 | 09.07. | 09.07. | v1 | Screens P21 — DSGVO/Launch (ex-D-REST) |
|
||||
| VD-P22 | 13.07. | 17.07. | v2 | Screens P22 — Public-API-Keys (ex-VD-5) |
|
||||
| VD-P23 | 13.07. | 17.07. | v2 | Screens P23 — Postfach-Inbox (ex-VD-1) |
|
||||
| VD-P24 | 13.07. | 15.07. | v2 | Screens P24 — Abo-Baukasten + Wizard (ex-VD-1) |
|
||||
| VD-P25 | 16.07. | 20.07. | v2 | Screens P25 — KI-Tiefe/HiveMind |
|
||||
| VD-MCP | 16.07. | 20.07. | v2 | Screens MCP — MCP-Connection-Verwaltung + Scope/Audit |
|
||||
| VD-P26 | 20.07. | 22.07. | v2 | Screens P26 — Customer-Portal |
|
||||
| VD-P27 | 20.07. | 22.07. | v2 | Screens P27 — Mobile-PWA-Shell (PWA-only) |
|
||||
| VD-P32 | 20.07. | 22.07. | v2 | Screens P32 — KWP-Setup (ex-VD-5) |
|
||||
@@ -538,7 +532,7 @@ css: |-
|
||||
| H-PITCH | 18.06. | 19.06. | v1 | Pitch-Deck + Gesprächsskript |
|
||||
| H-CALLS | 23.06. | 26.06. | v1 | Erste Discovery-Calls |
|
||||
| H-FOUND | 25.06. | 26.06. | v1 | Founding-Member-Angebot strukturieren |
|
||||
| H-PAYLINK | 26.06. | 26.06. | v1 | Stripe Payment Link (Founding-Deal) |
|
||||
| H-PAYLINK | 26.06. | 26.06. | v1 | Mollie Payment Link (Founding-Deal) |
|
||||
| H-DEMO | 30.06. | 03.07. | v1 | Klick-Demos + echte Produkt-Demos |
|
||||
| H-WEBINAR | 06.07. | 10.07. | v1 | Webinar-Einladungen + Moderation |
|
||||
| H-FOLLOW | 09.07. | 10.07. | v1 | Trial-Follow-ups + Bilanz |
|
||||
|
||||
@@ -3,8 +3,8 @@ gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: planning
|
||||
stopped_at: context exhaustion at 75% (2026-06-21)
|
||||
last_updated: "2026-06-21T20:56:25.641Z"
|
||||
stopped_at: context exhaustion at 75% (2026-07-05)
|
||||
last_updated: "2026-07-05T23:26:34.479Z"
|
||||
last_activity: 2026-06-15 — **ER-Modell festgezogen (AD-20, Projekt-zentrisch, David approved).** Kern ENTITY-MODEL.md neu (Domänen 5–8 → eine „Projekt-zentrischer CRM-Kern", Rest erhalten); entity-model.dbml kanonisiert + entity-model.svg + ENTITY-MODEL.pdf neu gerendert. Scratch gelöscht (ruecksprache-Entwurf, Whiteboard-Bild, projekt-zentrisch-Render-Artefakte, MODELL-PIVOT-Anker). Alle Anker-Verweise → ENTITY-MODEL.md umgebogen.
|
||||
progress:
|
||||
percent: 0
|
||||
@@ -57,12 +57,12 @@ Decisions are logged in PROJECT.md Key Decisions table (AD-1..AD-19 + KWP-Recut)
|
||||
Bau-relevante Leitentscheidungen für den aktuellen Stand:
|
||||
|
||||
- AD-11: Bau-Reihenfolge Fundament-Kern → SHK-Kernpfad → Breite (steuert die Phasenfolge 1→21).
|
||||
- Phase-0-Pflichten: Boilerplate einziehen + EU-Sterilisation + 4 Querschnitts-Neubauten gehen jedem Fachmodul voraus (= Phase 1).
|
||||
- Phase-0-Pflichten: Codebase lauffähig halten, EU-Sterilisation + 4 Querschnitts-Neubauten gehen jedem Fachmodul voraus (= Phase 1).
|
||||
- AD-1/AD-2: RLS-Baseline + typisierte Tabellen (Entity-Builder raus) sind ab Phase 2/6 bauliche Vorgabe.
|
||||
- AD-19: Big-Bang-Go-Live — kein phasiertes Produktiv-Schalten; Validierung pro Modul über Noah/Geerkens-Klick-Pfad.
|
||||
- Roadmap-Revision: Querschnitts-Oberflächen (P3, Audit-Viewer/Notif-Center/Sprach-UX) + Files/Tasks (P4) getrennt; Automation (P12) und KI (P13) eigenständig; Kommunikation in Email/Marketing (P14), Telefonie/Chat (P15), Kalender/Booking/Forms/Signatur (P16); HR (P17) + Portal (P18); Plattform/Launch in Admin (P19), Billing/Onboarding/Branding (P20), Public-API/DSGVO/Mobile/Help (P21). Kommunikations-Phasen hängen an Apps-Engine (P5) + CRM (P6), nicht an Automation/KI.
|
||||
- Dependency-Audit 2026-06-10: Die Service-Kerne Audit/i18n/Flags/Notifications (M001/M003/M006/M011) liegen in Phase 1 — der gesamte Auth-Block (P2) setzt sie voraus (entspricht BMAD-Block-1-Ordnung).
|
||||
- **Dependency-Inversion-Prinzip (verbindlich):** Spätere Module integrieren sich rückwirkend in frühere Oberflächen — keine unaufgelösten Vorwärts-Abhängigkeiten. PHASE-SOURCES.md führt das Ownership-Register (welche Phase welche Retro-Integration MIT einplanen muss), die Extension-Point-Pflichten der frühen Phasen (notify()-Kanäle, EntityLink, deal.won/lead.qualified-Events, Subscription-Stub, Credits-Konto, VM-Slots) und das maschinenlesbare Audit-Register (40 invertierte Refs). Audit-Kontrakt: Vorwärts-Refs MINUS Register = leer. M049 (Auto-Verkaufsfall) wurde als einziger echter Entity-Move nach P8 verschoben.
|
||||
- **Dependency-Inversion-Prinzip (verbindlich):** Spätere Module integrieren sich rückwirkend in frühere Oberflächen — keine unaufgelösten Vorwärts-Abhängigkeiten. PHASE-SOURCES.md führt das Ownership-Register (welche Phase welche Retro-Integration MIT einplanen muss), die Extension-Point-Pflichten der frühen Phasen (notify()-Kanäle, EntityLink, `lead.qualified`-Events, Subscription-Stub, VM-Slots) und das maschinenlesbare Audit-Register. Audit-Kontrakt: Vorwärts-Refs MINUS Register = leer. M049 (Auto-Projekt) wurde als einziger echter Entity-Move nach P8 verschoben.
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@@ -90,6 +90,6 @@ Items acknowledged and carried forward from previous milestone close:
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-06-21T20:56:25.638Z
|
||||
Stopped at: context exhaustion at 75% (2026-06-21)
|
||||
Last session: 2026-07-05T23:26:34.477Z
|
||||
Stopped at: context exhaustion at 75% (2026-07-05)
|
||||
Resume file: None
|
||||
@@ -52,7 +52,7 @@ css: |-
|
||||
| P17 | HR-Hub | 7 | Akeshan, 25./26.06. | v1 voll inkl. Surveys. Memberspot in v2 |
|
||||
| P18 | Customer-Portal | 0 | **v2: Akeshan, 20. bis 22.07.** | Andockpunkte liegen in v1 bereit |
|
||||
| P19 | Plattform-Admin | 7 | Akeshan, 22. bis 24.06. | v1: Tenant-Verwaltung + Abo-Zusammenstellung. AdminUser-Welt in v2 |
|
||||
| P20 | Billing & Onboarding | 8 | Akeshan, 08./09.07. | v1: Stripe-Abo-Baukasten (Admin). Self-Service-UI + Wizard in v2 |
|
||||
| P20 | Billing & Onboarding | 8 | Akeshan, 08./09.07. | v1: Legacy-Provider-Abo-Baukasten (Admin). Self-Service-UI + Wizard in v2 |
|
||||
| P21 | DSGVO & Launch | 2 | Akeshan, 09.07. | v1: Export + Lösch-Workflow + Feedback-Widget |
|
||||
|
||||
### v2-Pakete: ALLE 43 MS bis Fr 31.07. (vollumfänglich durchgeplant)
|
||||
@@ -129,14 +129,14 @@ Parallelität funktioniert, weil die Tracks entlang des Dependency-Graphen gesch
|
||||
|---|---|
|
||||
| **Fr 12.06.** | [MAIL] KWP-Mail beantworten (Callback-URLs `app.` + `staging-app.hivegrid.de/integrations/kwp/oauth/callback`) · bnWin.net-8.1-N3-Update an Noah/Geerkens-IT · AVV + Auskunfts-Vorlage + Website-Rechtstexte beim Anwalt · **Dev-Start P1** |
|
||||
| Sa/So 13./14.06. | P1 weiter (volle Arbeitstage) |
|
||||
| **Mo 15.06.** | **Externals-Paket (1 bis 2 h):** Mailjet SPF/DKIM/DMARC + Warm-up-Start · Google-Calendar-OAuth-Antrag · Stripe-Live-Verifizierung · AirCall- + PandaDoc-Sandbox · Hetzner-Projekt `HiveGrid` (S3-Bucket + Token) + Mistral-Key + DEK-Master-Key in Coolify-Secrets · Geo-IP/HIBP · Noah: 4 Freitags-Slots blocken |
|
||||
| **Mo 15.06.** | **Externals-Paket (1 bis 2 h):** Mailjet SPF/DKIM/DMARC + Warm-up-Start · Google-Calendar-OAuth-Antrag · Legacy-Provider-Live-Verifizierung · AirCall- + PandaDoc-Sandbox · Hetzner-Projekt `HiveGrid` (S3-Bucket + Token) + Mistral-Key + DEK-Master-Key in Coolify-Secrets · Geo-IP/HIBP · Noah: 4 Freitags-Slots blocken |
|
||||
| Mo/Di 15./16.06. | P1 fertigstellen (mit Akeshan), Ziel: Di Abend |
|
||||
| Mi bis Fr 17. bis 19.06. | **P2** Auth / RLS / RBAC / Sessions |
|
||||
| **Fr 19.06.** | **Gate W1 führen:** Hot-Swap-Drill + RLS-Cross-Tenant-Beweis + Konnektor-Demo |
|
||||
| Sa/So 20./21.06. | **P5** Apps-Engine (Integrations-Vertrag für 5 Folgephasen) |
|
||||
| Mo bis Mi 22. bis 24.06. | **P6** CRM-Kern |
|
||||
| **Do 25.06.** | [€] **Partner-Gespräch: Kostenmatrix/Preise FIXIEREN.** Blocker für Haris! |
|
||||
| Do/Fr 25./26.06. | **P7** Lead-Inbox · **Noah-Demo 1 präsentieren** (CRM) · Noah-Frage Lagerist (M070) · Stripe Payment Link für Founding-Deal an Haris |
|
||||
| Do/Fr 25./26.06. | **P7** Lead-Inbox · **Noah-Demo 1 präsentieren** (CRM) · Noah-Frage Lagerist (M070) · Mollie Payment Link für Founding-Deal an Haris |
|
||||
| Sa bis Mo 27. bis 29.06. | **P8** Sales-Pipeline (Kanban, deal.won-Event) |
|
||||
| **Mo 29.06.** | **v2-Vorlauf-Externals (30 min):** Web-Push VAPID-Keys · **Gmail-Restricted-Scope-Antrag** (Wochen Vorlauf!) · Microsoft-Entra-App *(kein Google Play Console / FCM/APNs — PWA-only, AD-17)* |
|
||||
| Di bis Do 30.06. bis 02.07. | **P9** Projekte/Plantafel (offline!), ein Tag Puffer vor dem Gate |
|
||||
@@ -236,7 +236,7 @@ Hot-Swap-Prinzip: Kein Screen blockt den Dev-Bau. Daten unten sind Demo-/Politur
|
||||
| Fr 19.06. | Pitch-Deck mit Davids D0-/Auth-Screens bestücken |
|
||||
| **Di 23.06.** | [CALL] **ERSTE ANRUFE:** Discovery/Qualifizierung (Pain-Points, KWP-Nutzung, Team-Größe; noch keine Live-Demo) |
|
||||
| **Do 25.06.** | Preise fix (von Raffael/Partner) → **Founding-Member-Angebot strukturieren** (Early-Bird-Jahresdeal), Noah-Abnahme |
|
||||
| **Mo 30.06.** | [CALL] Klick-Demos (CRM-Stand von Noah-Demo 1) · **Founding-Deal live** via Stripe Payment Link. [€] Erste Abschlüsse VOR Launch möglich |
|
||||
| **Mo 30.06.** | [CALL] Klick-Demos (CRM-Stand von Noah-Demo 1) · **Founding-Deal live** via Mollie Payment Link. [€] Erste Abschlüsse VOR Launch möglich |
|
||||
| Fr 03.07. | [CALL] Echte Produkt-Demos (Kette Lead → Sales → Auftrag läuft E2E) |
|
||||
| Mo 06.07. | **Webinar-Einladungen** raus (mit Dejan; kleine Liste, Domain erst 3 Wochen gewärmt) |
|
||||
| **Fr 10.07.** | [LAUNCH] → zahlende Onboardings (Raffael/Admin stellt Abos zusammen) |
|
||||
@@ -276,7 +276,7 @@ Hot-Swap-Prinzip: Kein Screen blockt den Dev-Bau. Daten unten sind Demo-/Politur
|
||||
|---|---|---|
|
||||
| **1 (Standard)** | eigene Phase früher fertig | Nächste eigene Phase im Track sofort ziehen, der Plan rückt nach vorn |
|
||||
| **2** | anderer Track hängt am kritischen Pfad | Track-übergreifend aushelfen (heißeste Kandidaten: Akeshans P16-Block, Raffaels P8/P9-Kette) |
|
||||
| **3** | Sprint-Puffer 3+ Tage | **Launch-Vorzieh-Prüfung mit Noah:** Go/No-Go früher ansetzen. Voraussetzungen: alle v1-Phasen fertig, Stripe live, Staging-Abnahme grün. Launch ist NICHT kalendergebunden; nur Mailjet-Warm-up (Kampagnen) und Google-Verifizierung laufen auf eigener Timeline und blocken den Launch nicht |
|
||||
| **3** | Sprint-Puffer 3+ Tage | **Launch-Vorzieh-Prüfung mit Noah:** Go/No-Go früher ansetzen. Voraussetzungen: alle v1-Phasen fertig, Legacy-Provider live, Staging-Abnahme grün. Launch ist NICHT kalendergebunden; nur Mailjet-Warm-up (Kampagnen) und Google-Verifizierung laufen auf eigener Timeline und blocken den Launch nicht |
|
||||
| **4** | Launch nicht vorziehbar (Externals) | v2-Vorzieher in fester Reihenfolge: (a) KWP-Recut-Vorarbeit gegen die API-Doku (Mapping + Adapter-Gerüst, KEIN Mock-E2E) → (b) Self-Service-Abo-Baukasten-UI (Revenue ohne Admin-Handarbeit) → (c) Postfach-Sync |
|
||||
|
||||
**Pflicht-Anteil:** Pro gewonnener Woche mindestens 1 Tag in den Härtungs-Pool (Bug-Bash, Lasttest, Security-Pass, Visual-Reg-Abdeckung). Geschwindigkeit kauft Qualität, nicht nur Features.
|
||||
@@ -294,7 +294,7 @@ Hot-Swap-Prinzip: Kein Screen blockt den Dev-Bau. Daten unten sind Demo-/Politur
|
||||
| Fr 12.06. | KWP-Mail (beide Callback-URLs) · bnWin.net 8.1 N3 · AVV + Rechtstexte beim Anwalt | **Raffael** |
|
||||
| Fr 12.06. | Subframe-Lizenzen (David + Devs) · Website-Domain/Hosting | **Dejan** |
|
||||
| Sa/So 13./14.06. | Painpoint-Gespräch + Website-Texte (Noah-Abnahme) | **Haris + Noah** |
|
||||
| Mo 15.06. | Mailjet-DNS + Warm-up · Google-Calendar-OAuth · Stripe-Live · AirCall/PandaDoc-Sandbox · Hetzner-Projekt + Mistral-Key · Noah-Slots | **Raffael** |
|
||||
| Mo 15.06. | Mailjet-DNS + Warm-up · Google-Calendar-OAuth · Legacy-Provider-Live · AirCall/PandaDoc-Sandbox · Hetzner-Projekt + Mistral-Key · Noah-Slots | **Raffael** |
|
||||
| Mi 17.06. | Staging + Postgres/Redis + DNS/Wildcard-TLS + zentrale Dienste (mCaptcha/ClamAV/Meilisearch-Key) | **Akeshan** |
|
||||
| **Do 25.06.** | [€] **Kostenmatrix/Preise mit Partner fixieren. Blocker für Haris!** | **Raffael** |
|
||||
| Fr 26.06. | SHK-Beispieldaten + Lagerist-Antwort | **Noah** |
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Liefert die nutzerseitigen Plattform-Primitive. Files: chunked Upload (TUS) mit RLS auf Storage-Pfaden (Hetzner S3-kompatibel), Preview-Generierung (PDF→PNG, Bild→WebP, **HEIC→JPG Pflicht**), Anhängen an jede Entität über `FileEntityLink` (polymorph), Tags, Archiv-Flag; Customer-Portal-Upload ist vorbereitet. Tasks: leichtgewichtiges, querliegendes Aufgaben-Modul mit Status/Priorität/Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Deal/Project), Watcher, Reminder via `notify()` (24h vor / bei / 24h nach Deadline) und Auto-Archivierung erledigter Tasks nach 30 Tagen per Cron. Tasks entstehen manuell, aus Chat, aus Form-Submission und aus Automation-Actions.
|
||||
Liefert die nutzerseitigen Plattform-Primitive. Files: chunked Upload (TUS) mit RLS auf Storage-Pfaden (Hetzner S3-kompatibel), Preview-Generierung (PDF→PNG, Bild→WebP, **HEIC→JPG Pflicht**), Anhängen an jede Entität über `FileEntityLink` (polymorph), Tags, Archiv-Flag; Customer-Portal-Upload ist vorbereitet. Tasks: leichtgewichtiges, querliegendes Aufgaben-Modul mit Status/Priorität/Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Projekt), Watcher, Reminder via `notify()` (24h vor / bei / 24h nach Deadline) und Auto-Archivierung erledigter Tasks nach 30 Tagen per Cron. Tasks entstehen manuell, aus Chat, aus Form-Submission und aus Automation-Actions.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
@@ -18,7 +18,7 @@ Liefert die nutzerseitigen Plattform-Primitive. Files: chunked Upload (TUS) mit
|
||||
| Upload-View + Datei-Dropzone [`/files/upload`] | Drag&Drop, Multi-Datei-Queue mit Fortschritt je Datei, Fehler+Retry · **HEIC-Hinweis „wird zu JPG konvertiert"** | A4 |
|
||||
| File-Detail [`/files/:id`] | Stammdaten · **Vorschau (Bild mit Zoom / PDF; HEIC→JPG)** mit Fallback-Klartext + Download bei not_supported · Tags · Notizen · verknüpfte Entitäten mit Rolle · Activity (Upload/Verknüpfungs-Änderungen/Downloads) · Download, Ersetzen, Archiv, Vorschau-neu-generieren bei failed (kein Versionsverlauf v1) | A3 |
|
||||
| Task-Liste [`/tasks`] | Titel, Prioritäts-Marker, Fälligkeit (überfällig rot, relativ ≤7 Tage), verknüpfte Entität, Assignee (Person/Gruppe), Status, Quelle-Hinweis · Presets: Meine/Erstellt/Alle (Alle nur mit tasks.read_all) · Such-Funktion · Filter (Status/Priorität/Deadline-Range/Verknüpfungs-Typ/Quelle) · Archiviert-Toggle · Bulk (Status/Priorität/Assignee/Archiv/Löschen) · Inline-Quick-Actions (done/Priorität/Deadline) | A2 — **wiederverwendbare Komponente** |
|
||||
| Task-Detail + Anlage-Modal [`/tasks/:id`, `/tasks/new`] | Titel, Beschreibung (Markdown), Status/Priorität/Fälligkeit inline, Assignee-Picker (Person↔Gruppe), Verknüpfungs-Picker (Auto-Complete Contact/Company/Lead/Deal/Project), Watcher, Quelle (manuell/Chat/Form/Automation mit Link), Activity-Stream der Task, Aktionen (Erledigen/Wieder-öffnen/Duplizieren/Löschen) · Klartext-Banner (überfällig / Assignee deaktiviert / „wird in 30 Tagen archiviert") | A3+A5 |
|
||||
| Task-Detail + Anlage-Modal [`/tasks/:id`, `/tasks/new`] | Titel, Beschreibung (Markdown), Status/Priorität/Fälligkeit inline, Assignee-Picker (Person↔Gruppe), Verknüpfungs-Picker (Auto-Complete Contact/Company/Lead/Projekt), Watcher, Quelle (manuell/Chat/Form/Automation mit Link), Activity-Stream der Task, Aktionen (Erledigen/Wieder-öffnen/Duplizieren/Löschen) · Klartext-Banner (überfällig / Assignee deaktiviert / „wird in 30 Tagen archiviert") | A3+A5 |
|
||||
|
||||
**Eingebettet (kein eigener Screen, aber zu designen):**
|
||||
- Files-Browser vorgefiltert als Sub-Sektion in Entity-Detail-Views
|
||||
@@ -30,7 +30,7 @@ Liefert die nutzerseitigen Plattform-Primitive. Files: chunked Upload (TUS) mit
|
||||
- [ ] Preview-Generierung PDF→PNG, Bild→WebP, **HEIC→JPG (Pflicht)** mit Status (pending/generating/ready/failed/not_supported) + Klartext-Fallback
|
||||
- [ ] Datei-Anhängen über FileEntityLink (polymorph) an jede Entität + Rollen-Label; Tags; Archiv-Flag; Bulk-ZIP; Customer-Portal-Upload vorbereitet
|
||||
- [ ] Files-Browser server-side-Filter (Entität/MIME-Gruppe/Tags/Verantwortlicher), Tabelle/Grid-Umschalter, Inline-Schnell-Vorschau
|
||||
- [ ] Tasks mit Status/Priorität/Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Deal/Project), Person- oder Gruppen-Zuweisung (Constraint: genau eines, Default = Ersteller), Watcher
|
||||
- [ ] Tasks mit Status/Priorität/Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Projekt), Person- oder Gruppen-Zuweisung (Constraint: genau eines, Default = Ersteller), Watcher
|
||||
- [ ] Task-Erstellungs-Quellen: manuell, Chat („Mark as Task"), Form-Submission, Automation-Action — je mit Quelle-Marker + Link
|
||||
- [ ] Reminder via notify() (24h vor / bei / 24h nach Deadline, Eskalation Email) an Assignee/Gruppe/Watcher; Auto-Archivierung erledigter Tasks nach 30 Tagen per Cron
|
||||
- [ ] Permission-Bundle tasks.* (read_own mit Owner-Check / read_all / write / admin) — fremde Tasks 404 statt 403, Action-Buttons permission-aware ausgeblendet/ausgegraut
|
||||
|
||||
+3
-3
@@ -4,11 +4,11 @@
|
||||
**Quellen:** `ROADMAP.md` §Phase 5 · `SCREEN-MAP.md` §2-Anhang (Integrations-Katalog / Connection-Setup-Wizard) · `REQUIREMENTS.md` Modul 33a (REQ-apps-engine-datenmodell, -apps-engine-lifecycle) · `ENTITY-MODEL.md` Domäne 4 (App, DataConnection, AppSyncRun, AppSyncRunLog, AppSyncItem) · `_bmad-output/specs/M33a-apps-engine.md`
|
||||
**Subframe:** Pflicht je Route. Archetypen aus D0 (A6 Settings/Connection-Kit + Connection-Card, A5 Modal/Wizard, A2 Liste).
|
||||
|
||||
> ⚙️ **Recut-Hinweis (KWP → v2):** KWP wird als `DataConnection type='rest-api'` (API-Key-Connect) abgebildet — **kein `kwpsync.exe`-Windows-Client-Flow** (Scope-Schnitt #12; die alte Drei-Komponenten-Architektur aus der M33a-Spec ist NICHT zu bauen). Connection-Type `windows_client` bleibt im Daten-Modell, hat in v1 aber keinen UI-Flow. v1-Connect-Typen in der UI: **OAuth2** (Gmail, Outlook, AirCall, PandaDoc, Google Calendar) und **API-Key** (KWP, Stripe). Apps selbst werden erst in ihren Modul-Phasen (P14/P15/P16/P20) angedockt — P5 liefert das Framework + die generische Katalog-/Wizard-UI, gegen die sich alle Connections registrieren.
|
||||
> ⚙️ **Recut-Hinweis (KWP → v2):** KWP wird als `DataConnection type='rest-api'` (API-Key-Connect) abgebildet — **kein `kwpsync.exe`-Windows-Client-Flow** (Scope-Schnitt #12; die alte Drei-Komponenten-Architektur aus der M33a-Spec ist NICHT zu bauen). Connection-Type `windows_client` bleibt im Daten-Modell, hat in v1 aber keinen UI-Flow. v1-Connect-Typen in der UI: **OAuth2** (Gmail, Outlook, AirCall, PandaDoc, Google Calendar) und **API-Key** (KWP, Legacy-Provider). Apps selbst werden erst in ihren Modul-Phasen (P14/P15/P16/P20) angedockt — P5 liefert das Framework + die generische Katalog-/Wizard-UI, gegen die sich alle Connections registrieren.
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Bindet externe Systeme über ein einziges, typsicheres Integrations-Framework an — der Vertrag, über den später Gmail, Outlook, AirCall, PandaDoc, Stripe und KWP laufen. Ein Admin stellt eine `DataConnection` über OAuth2-Connect oder API-Key-Connect in der Settings-UI her; die type-safe Adapter-Registry erlaubt neue Connection-Typen ohne Kern-Änderung; `AppSyncRun`/`AppSyncItem` protokollieren Sync-Läufe in Klartext; Connection-Health-Monitoring zeigt den Status jeder Verbindung (active/degraded/broken/revoked/pending) mit Klartext-Diagnostik + Lösungs-Hinweisen; OAuth-Tokens sind per-Tenant-DEK-verschlüsselt (AD-8). Mehrere Connections pro App pro Tenant möglich (z. B. drei Gmail-Mailboxen).
|
||||
Bindet externe Systeme über ein einziges, typsicheres Integrations-Framework an — der Vertrag, über den später Gmail, Outlook, AirCall, PandaDoc, Legacy-Provider und KWP laufen. Ein Admin stellt eine `DataConnection` über OAuth2-Connect oder API-Key-Connect in der Settings-UI her; die type-safe Adapter-Registry erlaubt neue Connection-Typen ohne Kern-Änderung; `AppSyncRun`/`AppSyncItem` protokollieren Sync-Läufe in Klartext; Connection-Health-Monitoring zeigt den Status jeder Verbindung (active/degraded/broken/revoked/pending) mit Klartext-Diagnostik + Lösungs-Hinweisen; OAuth-Tokens sind per-Tenant-DEK-verschlüsselt (AD-8). Mehrere Connections pro App pro Tenant möglich (z. B. drei Gmail-Mailboxen).
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
@@ -23,7 +23,7 @@ Bindet externe Systeme über ein einziges, typsicheres Integrations-Framework an
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] DataConnection per OAuth2-Connect-Flow (Gmail, Outlook, AirCall, PandaDoc, Google Calendar) ODER API-Key-Connect (KWP, Stripe) in der Settings-UI herstellbar
|
||||
- [ ] DataConnection per OAuth2-Connect-Flow (Gmail, Outlook, AirCall, PandaDoc, Google Calendar) ODER API-Key-Connect (KWP, Legacy-Provider) in der Settings-UI herstellbar
|
||||
- [ ] Type-safe Adapter-Registry: neue Connection-Typen ohne Kern-Änderung registrierbar (Build-Time-Scan von `app/integrations/`, Drift-Check → Klartext-Warnung im Plattform-Admin)
|
||||
- [ ] AppSyncRun/AppSyncItem/AppSyncRunLog protokollieren Sync-Läufe mit Klartext-Summary („Sync mit KWP — 14 Aufträge gepullt, 2 fehlgeschlagen …")
|
||||
- [ ] Connection-Health-Monitoring: Status active/degraded/broken/revoked/pending mit Klartext + Lösungs-Hinweisen; Health-Check-Intervalle; Recovery-Klartext
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Eingehende Leads aus allen Quellen (Form-Submission, eingehende Email, AirCall-Inbound, manueller Eintrag, Custom-Quellen) landen in einem Triage-Arbeitsplatz, werden automatisch angelegt, gegen Spam/Anomalien geschützt und sind auswertbar. Lead ist die **Vorstufe** des `Projekt`-Kerns: bei Weiterverarbeitungsfreigabe wird der Lead zum Projekt (Lead → Projekt, Domäne 5), der Kontakt separat verknüpft. Das `lead.qualified`-Event ist Extension-Point für P8 (Auto-Verkaufsfall M049).
|
||||
Eingehende Leads aus allen Quellen (Form-Submission, eingehende Email, AirCall-Inbound, manueller Eintrag, Custom-Quellen) landen in einem Triage-Arbeitsplatz, werden automatisch angelegt, gegen Spam/Anomalien geschützt und sind auswertbar. Lead ist die **Vorstufe** des `Projekt`-Kerns: bei Weiterverarbeitungsfreigabe wird der Lead zum Projekt (Lead → Projekt, Domäne 5), der Kontakt separat verknüpft. Das `lead.qualified`-Event ist Extension-Point für P8 (Auto-Projekt M049).
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Baut den `Projekt`-Kern als eine typisierte Tabelle (Superset der Felder, dazu `CustomFieldValue`) und die Pipeline-Engine als frei konfigurierbare Views: Pipeline anlegen/benennen/duplizieren, Statuswerte (Stages) je Pipeline, Zugriff je Rolle/User/Gruppe (`PipelineAccess`), Feld-Sichtbarkeit + Pflichtfelder pro Pipeline+Status (`PipelineFieldConfig`). Darauf der EINE konfigurierbare Kanban (ein Board für alle Pipeline-Typen) plus Listen-View, Projekt-Detail, fortlaufende Projekt-Nummern (HiveNumberSeq), Forecast/KPIs und Lost-Reasons. Konsumiert das `lead.qualified`-Event aus P7 (Auto-Verkaufsfall M049 = Projekt in Sales-Pipeline). Auftrags-Features + Sub-Areas folgen in P9.
|
||||
Baut den `Projekt`-Kern als eine typisierte Tabelle (Superset der Felder, dazu `CustomFieldValue`) und die Pipeline-Engine als frei konfigurierbare Views: Pipeline anlegen/benennen/duplizieren, Statuswerte (Stages) je Pipeline, Zugriff je Rolle/User/Gruppe (`PipelineAccess`), Feld-Sichtbarkeit + Pflichtfelder pro Pipeline+Status (`PipelineFieldConfig`). Darauf der EINE konfigurierbare Kanban (ein Board für alle Pipeline-Typen) plus Listen-View, Projekt-Detail, fortlaufende Projekt-Nummern (HiveNumberSeq), Forecast/KPIs und Lost-Reasons. Konsumiert das `lead.qualified`-Event aus P7 (Auto-Projekt M049 = Projekt in Sales-Pipeline). Auftrags-Features + Sub-Areas folgen in P9.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Designer-Brief · P13 — KI-Layer
|
||||
|
||||
> ⛔ **ABGELÖST (KI→MCP-Pivot, [[AD-26]], 2026-06-22):** Phase 13 (KI-Layer) entfällt — kein KI-Settings-/AI-Credits-Screen. HiveGrid wird MCP-Server; die einzige zugehörige UI ist die Verbindungs-/Settings-Ansicht in **[[VD-MCP]]**. Brief unten = historisch.
|
||||
|
||||
**Phase:** 13/21 · **Modul:** M18 · **Owner-Dev:** Raffael (R-P13) · **Designer:** David (D-KI, Paket Mo 06.07. Compose)
|
||||
**Quellen:** `ROADMAP.md` §Phase 13 · `ENTITY-MODEL.md` Domäne 7 (Automation & KI) · `SCREEN-MAP.md` §6 (KI-Settings / AI-Credits) · `_bmad-output/specs/M18-ki-layer.md` (HiveMind/RAG/Whisper-Teile → v2)
|
||||
**Subframe:** Pflicht je Route. Archetypen aus D0 (A6 Settings/Connection, KPI+Charts). Compose-Paket — wenig Bespoke.
|
||||
|
||||
> 🟡 **v1-Grenze (Scope-Schnitt 11.06.):** HiveMind, RAG/Embeddings, Whisper-Transkription und AlephAlpha sind **v2** (→ P25). v1 baut nur den produktiven **EU-LLM-Gateway** (Mistral La Plateforme primär, Provider-Abstraktion), Token-/AiCredits-Tracking mit harten + Drossel-Limits und die **Andock-Adapter** als Muster. Kein RAG-Chat-UI, kein Whisper-Aufnahme-UI, kein HiveMind-Assistent in v1.
|
||||
> 🔴 **Sprach-zu-Automation gehört sichtbar in den Flow-Editor (P12)** — P13 liefert nur das Gateway dahinter. EU-Datenresidenz wird überall im Klartext kommuniziert; keine US-Kern-Abhängigkeit.
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Der EU-only KI-Unterbau: ein Multi-Provider-LLM-Gateway, das Tokens trackt, gegen das lokale **AiCredits**-Konto bucht und Soft-/Hard-Limits durchsetzt. Sichtbare v1-Oberflächen sind die **KI-Settings** (Use-Case-Toggles, Modell-Wahl, EU-Klartext) und das **AI-Credits/Usage-Dashboard**. Die KI selbst wird über v1-Use-Cases konsumiert (primär Sprach-zu-Automation in P12, plus Lead-Zusammenfassung/Programm-Hilfe/Stuck-Vorschläge als KI-Widgets in bestehenden Detail-/Dashboard-Views). Promptfoo testet die ausgelieferten Funktionen.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
| Screen [Route] | Kern-Inhalt | Archetyp |
|
||||
|---|---|---|
|
||||
| **KI-Settings** [`/admin/ai`] | Use-Case-Toggles (je v1-Use-Case ein/aus) · Modell-Wahl (Mistral Large/Small je Use-Case) · Token-Limit pro Monat · **EU-Datenresidenz-Klartext-Block** · Hinweis Plan-Erweiterung extern | A6 |
|
||||
| **AI-Credits / Usage** [`/admin/usage/ai`] | Saldo (current_balance_tokens) · Verbrauch je Use-Case (Donut) · Verlauf über Zeit (Linie) · Top-User · **80%-Soft-Limit-Warnbanner** · Hochrechnung Monatsende · Plan-Limit-Anzeige | KPI + Charts |
|
||||
| **Soft-/Hard-Limit-Banner** [global] | 80%-Banner („KI-Budget zu 80% verbraucht …") · 100%-Klartext bei Aufruf-Versuch („KI-Budget erschöpft, Erweiterung auf hivegrid.de/pricing") · kritische Funktionen (Klartext-Diagnostik) bleiben aktiv | Banner (A7/Topbar) |
|
||||
| KI-Widget: Lead-Zusammenfassung [im Kontakt-/Lead-Detail] | On-demand-Generierung des Pre-Termin-Briefs · Quelle Activity-Stream/Custom-Felder/Form-Submissions · Cache-Hinweis · Klartext bei Provider-Down | Compose (in A3-Detail) |
|
||||
| KI-Widget: Stage-Stuck-Vorschläge [im Dashboard] | Wöchentlich generierte Vorschläge für Stuck-Deals/-Projekte · Quick-Action-Buttons (Anrufen/Email) · **KI-Vorschlag-Kennzeichnung** | Compose (in Dashboard) |
|
||||
| Programm-Hilfe-Einstieg [In-App, klein] | Frage-Eingabe → KI-Antwort (Klartext + Deep-Link) · Conversation-Historie pro User · **„KI-Vorschlag"-Markierung** bei Fallback-Diagnostik | A5-klein / Drawer |
|
||||
|
||||
*(RAG-Chat, Whisper-Aufnahme-UI, HiveMind-Assistent, Bring-Your-Own-Key → v2.)*
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] EU-LLM-Gateway (Mistral La Plateforme primär, Provider-Abstraktion für zweiten EU-Provider → v2) mit Token-Tracking je Aufruf
|
||||
- [ ] AiCredits-Buchung: AiTokenUsage je Aufruf, Aggregat-Abzug, Soft-Limit (80% Banner + Admin-Mail), Hard-Limit (100% Pause außer kritische Funktionen, Monats-Reset)
|
||||
- [ ] KI-Settings pro Tenant: aktivierte Use-Cases, Default-Modelle, Monats-Token-Limit, EU-Residenz-Klartext
|
||||
- [ ] Usage-Dashboard: Verbrauch je Use-Case (Donut), Zeitverlauf, Top-User, Forecast, Plan-Limit
|
||||
- [ ] v1-Use-Cases sichtbar: Sprach-zu-Automation (Gateway für P12), Lead-Zusammenfassung, Programm-Hilfe/FAQ, Klartext-Diagnostik-Fallback, Stage-Stuck-Vorschläge
|
||||
- [ ] Sicherheits-Layer-Wirkung im UI: schreibende KI-Aktionen erfordern Bestätigungs-Workflow (Action-Boundary); KI-Fallback-Outputs als „KI-Vorschlag" markiert
|
||||
- [ ] Klartext bei Provider-Down / Limit-erreicht statt technischer Fehler
|
||||
- [ ] Promptfoo-Eval-Suite deckt ausgelieferte KI-Funktionen ab (CI-Pflicht, kein UI)
|
||||
- [ ] v2-Andock-Adapter als Muster vorhanden: `RetrievalProvider`-Interface, Outbox-Index-Events (via P11), Bestätigungs-Grenze (kein v1-UI nötig)
|
||||
- [ ] Datenmodell persistiert: LlmGatewayProvider/Model, AiSettings, AiCredits, AiTokenUsage, Conversation/Message, GeneratedObject (Domäne 7)
|
||||
|
||||
## Benötigte UI-Elemente (Liste, KEIN Layout — Anordnung = Davids freie Hand)
|
||||
|
||||
Settings-Sektionen mit Toggle-Zeilen (Klartext-Beschreibung), Modell-Auswahl-Dropdowns je Use-Case, Token-Limit-Eingabe, EU-Datenresidenz-Klartext-Block, KPI-Kacheln (Saldo/Verbrauch/Forecast), Donut-Chart (Verbrauch je Use-Case), Linien-Chart (Verlauf), Top-User-Liste, Soft-/Hard-Limit-Warnbanner (stapelbar mit Topbar-System), KI-Vorschlag-Badge/Kennzeichnung, On-demand-Generieren-Button mit Lade-Zustand, Conversation-Historie-Liste, Bestätigungs-Dialog für schreibende KI-Aktionen, Klartext-Provider-Down-Hinweis, Empty-/Loading-/Permission-Denied-States.
|
||||
|
||||
## Davids Delta (David füllt aus)
|
||||
|
||||
- [ ] Überschriebene/geänderte Spec-Punkte:
|
||||
- [ ] Neue UI/UX-Entscheidungen:
|
||||
- [ ] Subframe-Screen-Link(s):
|
||||
- [ ] Offene Fragen an Dev:
|
||||
|
||||
## Dev-Nachzug (wir nach Davids Delta)
|
||||
|
||||
- [ ] Spec/ENTITY-MODEL an Davids Entscheidungen angepasst:
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen:
|
||||
- [ ] Plane-Ticket (R-P13) aktualisiert:
|
||||
+4
-4
@@ -27,20 +27,20 @@
|
||||
| **AirCall-Setup** [`/settings/integrations/telephony`] | Connect-Card (Status-Ampel connecting/active/error/revoked + Klartext) · **User-Binding-Liste** (AirCall-User ↔ HiveGrid-User mit Email-Auto-Match-Vorschlag + manuelle Korrektur) · „Verbindung testen" · Trennen mit Bestätigung | A6 |
|
||||
| Anruf-Liste + -Detail [`/calls`] | Liste: Nummer, Richtung, Dauer, **gematchter Kontakt** (mit Confidence-Hinweis bei Soft-Match), Notiz-Snippet, verpasst-Marker · Detail: **Match-Override**, Markdown-Notiz-Editor, Aufnahme-Link · Click-to-Call-Button + eingehender-Anruf-Notification | A6 + A2/A3 |
|
||||
|
||||
*(HalloPetra-Adapter-UI, Whisper-Diktat-Aufnahme → v2. Click-to-Call-Buttons leben außerdem in Kontakt-/Lead-/Deal-Detail aus P6/P7/P8.)*
|
||||
*(HalloPetra-Adapter-UI → v2; Whisper-Diktat entfällt nach AD-26. Click-to-Call-Buttons leben außerdem in Kontakt-/Lead-/Projekt-Detail aus P6/P7/P8.)*
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] AirCall-Connect (OAuth, verschlüsselte Tokens, Webhook-Secret) mit Klartext-Status-Diagnose
|
||||
- [ ] User-Bindings mit Email-Auto-Match-Vorschlag + manuelle Zuordnung
|
||||
- [ ] Click-to-Call aus Kontakt-/Lead-/Deal-Detail; Mehrfach-Nummern → Auswahl-Modal; Klartext-Edge-Cases (kein Binding / keine Connection / API-Fehler / ungültige Nummer); `tel:`-Fallback ohne Connection
|
||||
- [ ] Click-to-Call aus Kontakt-/Lead-/Projekt-Detail; Mehrfach-Nummern → Auswahl-Modal; Klartext-Edge-Cases (kein Binding / keine Connection / API-Fehler / ungültige Nummer); `tel:`-Fallback ohne Connection
|
||||
- [ ] Inbound-Anrufe: Auto-Match auf Kontakt/Lead/Deal (Hard/Soft-Match mit Confidence), unbekannte Nummer → Auto-Lead, Push-Notification beim Klingeln
|
||||
- [ ] Anruf-Logging in Activity-Stream (in/out, verpasst), Markdown-Notiz-Editor nach Anruf, Recording-Anzeige, Match-Override
|
||||
- [ ] Anruf-Events als Automation-Trigger (verpasster Anruf → Rückruf-Aufgabe — P12)
|
||||
- [ ] `TelephonyProviderInterface` mit AirCall als Referenz-Adapter (HalloPetra dockt v2 an)
|
||||
- [ ] Chat-Real-Time: WebSocket, Presence, Typing, Read-Receipts, Redis-Pub/Sub, Optimistic-UI, Reconnect
|
||||
- [ ] Drei Channel-Typen: public/private Group, DM, entity-linked (Projekt/Deal) mit ProjectMember-Sync
|
||||
- [ ] @Mentions (User/Projekt/Deal/Contact/Firma) mit Autocomplete, Threads, Reactions, Pinned (max 50), File-Uploads (via Files), Link-Previews, Mark-as-Task
|
||||
- [ ] Drei Channel-Typen: public/private Group, DM, entity-linked (Projekt) mit ProjectMember-Sync
|
||||
- [ ] @Mentions (User/Projekt/Contact/Firma) mit Autocomplete, Threads, Reactions, Pinned (max 50), File-Uploads (via Files), Link-Previews, Mark-as-Task
|
||||
- [ ] Channel-Index mit Filter/Suche/Pin/Mute/Archiv + Channel-Browser; Cross-Channel-Suche; Mentions-Aggregat
|
||||
- [ ] Entity-linked Channel erscheint im Deal-/Projekt-Detail (P8/P9)
|
||||
- [ ] Admin-Audit (`read_all_channels`): Begründungs-Pflicht je Session + DM-Audit-Banner, audit-geloggt
|
||||
|
||||
+4
-4
@@ -4,11 +4,11 @@
|
||||
**Quellen:** `ROADMAP.md` §Phase 16 · `ENTITY-MODEL.md` Domäne 9 (Kalender/Booking/Forms/Signatur) · `SCREEN-MAP.md` §3 (Form-Builder) + §5 (Booking/Kalender/Signatur) + §6 (Signatur) · `_bmad-output/specs/M25-calendar-sync.md`, `M06-booking.md`, `M05-form-builder.md`, `M07-angebot-signatur.md`
|
||||
**Subframe:** Pflicht je Route. Archetypen aus D0 (A2 Liste, A3 Detail, A4 Form/Input, A5 Modal/Wizard, A6 Settings/Connection, A7 Zustände); zusätzlich BESPOKE für Form-Builder-Canvas, Booking-Page (öffentlich), Kalender.
|
||||
|
||||
> 🟡 **Vier Module, vier Screen-Gruppen — bewusst gebündelt:** Termin- und Dokument-Workflows. Bitte je Gruppe (1) Kalender-Sync, (2) Booking, (3) Form-Builder, (4) Angebots-Signatur getrennt halten. **v1-Schnitte (verbindlich):** Kalender-Sync nur **Google** (Outlook-Adapter → v2, Interface bleibt provider-agnostisch); Booking nur **eine** Zuteilungs-Strategie „Erster der frei ist" (Round-Robin/Provider-Wahl → v2); Form-Sharing = **URL + QR + iFrame** (personalisierte Links / JS-Snippet → v2); Signatur über **PandaDoc** (DocuSign-Adapter bleibt möglich). Pivot-Hinweis: Signatur hängt am **`Projekt`**-Kern (nicht „Deal") — `ProjektSignatureDocument`/`ProjektSignatureSigner`; der Signatur-Einstieg lebt im Projekt-Detail (P8/P9). Die Task-Komponente aus P4 wird hier eingebettet, nicht neu gebaut.
|
||||
> 🟡 **Vier Module, vier Screen-Gruppen — bewusst gebündelt:** Termin- und Dokument-Workflows. Bitte je Gruppe (1) Kalender-Sync, (2) Booking, (3) Form-Builder, (4) Angebots-Signatur getrennt halten. **v1-Schnitte (verbindlich):** Kalender-Sync nur **Google** (Outlook-Adapter → v2, Interface bleibt provider-agnostisch); Booking nur **eine** Zuteilungs-Strategie „Erster der frei ist" (Round-Robin/Provider-Wahl → v2); Form-Sharing = **URL + QR + iFrame** (personalisierte Links / JS-Snippet → v2); Signatur über **gehärteten SES-Eigenbau als Default** (Fern-Link/OTP + Touch vor Ort + PAdES + Completion-Certificate), **PandaDoc nur optionaler BYOK-Adapter**. Pivot-Hinweis: Signatur hängt am **`Projekt`**-Kern (nicht „Deal") — `ProjektSignatureDocument`/`ProjektSignatureSigner`; der Signatur-Einstieg lebt im Projekt-Detail (P8/P9). Die Task-Komponente aus P4 wird hier eingebettet, nicht neu gebaut.
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Deckt Termin- und Dokument-Workflows ab: bidirektionaler **Kalender-Sync** mit Google (Settings + Connection-Health), öffentliche **Buchungsseiten** (Calendly-Style, timezone-aware Slots, iFrame-Embed, Auto-Bestätigungsmail, erzeugt Lead/Activity), ein Drag-&-Drop-**Form-Builder** auf Google-Forms-Niveau (12 Feld-Typen, Conditional Logic, SHK-Templates, Post-Submit-Actions) und Multi-Signer-**Angebots-Signatur** über die Apps-Engine.
|
||||
Deckt Termin- und Dokument-Workflows ab: bidirektionaler **Kalender-Sync** mit Google (Settings + Connection-Health), öffentliche **Buchungsseiten** (Calendly-Style, timezone-aware Slots, iFrame-Embed, Auto-Bestätigungsmail, erzeugt Lead/Activity), ein Drag-&-Drop-**Form-Builder** auf Google-Forms-Niveau (12 Feld-Typen, Conditional Logic, SHK-Templates, Post-Submit-Actions) und Multi-Signer-**Angebots-Signatur** über SES-Default plus Apps-Engine-Adapter.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
@@ -27,7 +27,7 @@ Deckt Termin- und Dokument-Workflows ab: bidirektionaler **Kalender-Sync** mit G
|
||||
| Forms-Liste [`/forms`] | Name, Subtyp, Status (Entwurf/Publiziert/Archiviert), Submissions-Anzahl, letzte Submission · SHK-Templates geseedet | A2 |
|
||||
| 🌐 Öffentliches Formular [`/f/{tenant}/{slug}`] | Gerenderte Felder, Conditional Logic live, mCaptcha, Submit → Erfolgs-Text oder Redirect · Tenant-Logo · 4 Zustände | BESPOKE-Public |
|
||||
| Submissions [`/forms/:id/submissions`] | Tabelle der Antworten (Spalten = Felder), Filter, CSV-Export · Detail: alle Antworten in Reihenfolge, Submitter-Metadata, Action-Log mit Status (Re-Run), File-/Foto-Vorschau, PDF-Export | A2 / A3 |
|
||||
| Signatur-Wizard [im Projekt-Detail] | PDF-Upload (aus Files oder neu) → Unterzeichner (Name vorausgefüllt aus Kontakt, Email, Rolle, Reihenfolge sequenziell/parallel) → Provider (PandaDoc, Default) → Senden · Status-Tracking je Unterzeichner (entwurf/gesendet/angesehen/signiert/abgelehnt) + Erinnerung + Widerruf · Original- + Signed-PDF-Download | A5 + A6 |
|
||||
| Signatur-Wizard [im Projekt-Detail] | PDF-Upload (aus Files oder neu) → Unterzeichner (Name vorausgefüllt aus Kontakt, Email, Rolle, Reihenfolge sequenziell/parallel) → Signaturmodus (Fern-Link/OTP oder Touch vor Ort; PandaDoc nur BYOK-Opt-in) → Senden · Status-Tracking je Unterzeichner (entwurf/gesendet/angesehen/signiert/abgelehnt) + Erinnerung + Widerruf · Original-, Signed-PDF- und Completion-Certificate-Download | A5 + A6 |
|
||||
|
||||
*(Lead-Inbox/Lead-Detail → P7. Plantafel/Projekt-Detail → P9. Sende-Domain-Card → P14. Integrations-Katalog/Connection-Setup → P5. Task-Komponente → P4.)*
|
||||
|
||||
@@ -39,7 +39,7 @@ Deckt Termin- und Dokument-Workflows ab: bidirektionaler **Kalender-Sync** mit G
|
||||
- [ ] Booking nur **eine** Zuteilungs-Strategie „Erster der frei ist" (Round-Robin/Provider-Wahl → v2)
|
||||
- [ ] Form-Builder mit Drag-&-Drop, **12 Feld-Typen**, **Conditional Logic**, Validierung pro Feld, SHK-Form-Templates geseedet, iFrame-Embed + QR + öffentliche URL
|
||||
- [ ] Post-Submit-Actions feuern mit Field-Mapping + ActionLog (Lead/Kontakt/Projekt/Task anlegen, Email an Submitter/Team, Webhook, Automation-Trigger, Auto-Match)
|
||||
- [ ] Angebote durchlaufen Multi-Signer-Signatur via **PandaDoc** über die Apps-Engine (M33a) mit Webhook-Verarbeitung, Status-Updates, optionalem Auto-Stage-Wechsel; Signed-PDF landet in Files
|
||||
- [ ] Angebote durchlaufen Multi-Signer-Signatur via **SES-Eigenbau-Default** (Fern-Link/OTP + Touch vor Ort + PAdES + Completion-Certificate); **PandaDoc** bleibt optionaler BYOK-Adapter über die Apps-Engine (M33a). Status-Updates, optionaler Auto-Stage-Wechsel; Signed-PDF landet in Files
|
||||
- [ ] Signatur hängt am **`Projekt`**-Kern (`ProjektSignatureDocument`/`Signer`) — kein eigenes „Deal"-Objekt
|
||||
- [ ] Permission-Bundles: `calendar.*` (connect_own/admin), `bookings.*` (read_own/read_all/write/admin), `forms.*` (basic/builder/publisher/admin), `signature.*` (read/send/revoke/admin) — serverseitig Owner-gefiltert
|
||||
|
||||
|
||||
+6
-6
@@ -4,11 +4,11 @@
|
||||
**Quellen:** `ROADMAP.md` §Phase 19 · `ENTITY-MODEL.md` Domäne 11 (Plattform-Admin) + Domäne 12 (Abo-Baukasten, für Abo-Zusammenstellung) · `SCREEN-MAP.md` §3 (Plattform-Admin-Block) · `_bmad-output/specs/M37-plattform-admin.md`, `M32-feature-flags.md` (Admin-UI-Teile)
|
||||
**Subframe:** Pflicht je Route. **Eigene Admin-Shell** (nicht App-Shell A1) — getrennte Sidebar/Topbar auf `admin.hivegrid.de`; interner Polish reicht (kein 🌐-Niveau). Archetypen-Reuse: A2 Liste, A3 Detail, A4 Form, A5 Modal, A6 Settings, KPI/Charts.
|
||||
|
||||
> 🟡 **Getrennte Welt — eigene Shell, kein App-Reuse der Sidebar.** Läuft auf `admin.hivegrid.de` mit Host-Routing, **Cookie-Domain-Trennung** zu app.hivegrid.de, TOTP-Pflicht + MFA-Step-up. **v1-Schnitt (verbindlich):** Die **getrennte AdminUser-Identität / AdminRBAC-Parallelwelt** (M038/M039) ist **v2** — v1 nutzt das Boilerplate-User-Modell als Extension + `admin-mfa-gate`. Es gibt **keine** AdminUser-/AdminRole-Verwaltungs-Screens in v1, **keine** Plattform-Lead-Pipeline/Deal-Kanban (unser eigenes Sales-Tool → v2), **keine** Feature-Flag-Massen-Statistik (→ v2). Admin-Dashboard zeigt Basis-KPIs (Tenants/User/MRR), Churn/ARPU → v2. **Neu v1:** Admin stellt Tenant-Abos zusammen (Feature-Konnektoren togglen + Stripe-Positionen — **Abo-Baukasten-Mechanik**; Self-Service-UI dafür → v2). KWP-Sync-Status-Screen entfällt v1 (KWP ist erstes v2-Modul).
|
||||
> 🟡 **Getrennte Welt — eigene Shell, kein App-Reuse der Sidebar.** Läuft auf `admin.hivegrid.de` mit Host-Routing, **Cookie-Domain-Trennung** zu app.hivegrid.de, TOTP-Pflicht + MFA-Step-up. **v1-Schnitt (verbindlich):** Die **getrennte AdminUser-Identität / AdminRBAC-Parallelwelt** (M038/M039) ist **v2** — v1 nutzt das Boilerplate-User-Modell als Extension + `admin-mfa-gate`. Es gibt **keine** AdminUser-/AdminRole-Verwaltungs-Screens in v1, **keine** Plattform-Lead-Pipeline/Deal-Kanban (unser eigenes Sales-Tool → v2), **keine** Feature-Flag-Massen-Statistik (→ v2). Admin-Dashboard zeigt Basis-KPIs (Tenants/User/MRR), Churn/ARPU → v2. **Neu v1:** Admin stellt Tenant-Abos zusammen (Feature-Konnektoren togglen + Legacy-Provider-Positionen — **Abo-Baukasten-Mechanik**; Self-Service-UI dafür → v2). KWP-Sync-Status-Screen entfällt v1 (KWP ist erstes v2-Modul).
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Die Plattform ist aus einer getrennten Admin-Welt betreibbar: Tenant-Management (Stammdaten, Status-Wechsel, Notizen, Impersonate mit Begründung), **Abo-Zusammenstellung** je Tenant (Feature-Konnektoren + Stripe-Positionen + Trial-Verlängerung), User-Verwaltung (2FA-Reset, Session-Revoke), Feature-Flag-Verwaltung (UI zur Engine aus Phase 1), Queue-/Health-Monitoring und eine Support-Inbox, die sich aus dem Feedback-Widget (P21) speist. Alle Admin-Aktionen laufen ins `AdminAuditLog` (cross-tenant, tenant_id=null).
|
||||
Die Plattform ist aus einer getrennten Admin-Welt betreibbar: Tenant-Management (Stammdaten, Status-Wechsel, Notizen, Impersonate mit Begründung), **Abo-Zusammenstellung** je Tenant (Feature-Konnektoren + Legacy-Provider-Positionen + Trial-Verlängerung), User-Verwaltung (2FA-Reset, Session-Revoke), Feature-Flag-Verwaltung (UI zur Engine aus Phase 1), Queue-/Health-Monitoring und eine Support-Inbox, die sich aus dem Feedback-Widget (P21) speist. Alle Admin-Aktionen laufen ins `AdminAuditLog` (cross-tenant, tenant_id=null).
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
@@ -17,10 +17,10 @@ Die Plattform ist aus einer getrennten Admin-Welt betreibbar: Tenant-Management
|
||||
| Admin-Login + 2FA [`admin.hivegrid.de/login`, `/2fa`] | Email + Passwort (NIST/HIBP, Captcha ab 3. Fehler), TOTP-Pflicht, kein Magic-Link, 8h-Session | A4 / A5 |
|
||||
| Admin-Dashboard [`/dashboard`] | **Basis-KPIs: Tenants, aktive User, MRR** + System-Health-Ampel + Queue-Status · Klick je Kachel → Drill-Down · Zeitraum-Wechsler *(Churn/ARPU/Sales-Pipeline → v2)* | KPI |
|
||||
| Tenant-Liste [`/tenants`] | Name, Slug, Status (Klartext-Farbe), Plan, MRR, User-Anzahl, letzte Aktivität, Branche/Größe · Filter + Suche + Bulk (Tag/Email) | A2 |
|
||||
| **Tenant-Detail** [`/tenants/:id`] | Stammdaten, Status-Wechsler (Reason-Pflicht), **Abo-Zusammenstellung (Feature-Konnektoren togglen + Stripe-Positionen — Abo-Baukasten!)**, Trial-Verlängerung (Datum + Reason), Notizen (Markdown, kategorisiert, gepinnt), User-/Activity-/Audit-/Feature-Flag-Sektionen, **Impersonate mit Begründungs-Pflicht** | A2 / A3 + A6 |
|
||||
| **Tenant-Detail** [`/tenants/:id`] | Stammdaten, Status-Wechsler (Reason-Pflicht), **Abo-Zusammenstellung (Feature-Konnektoren togglen + Legacy-Provider-Positionen — Abo-Baukasten!)**, Trial-Verlängerung (Datum + Reason), Notizen (Markdown, kategorisiert, gepinnt), User-/Activity-/Audit-/Feature-Flag-Sektionen, **Impersonate mit Begründungs-Pflicht** | A2 / A3 + A6 |
|
||||
| Manuelle Tenant-Anlage [`/tenants/new`] | Name, Slug (Live-Validation), Branche, Größe, Initial-Admin-Email, Initial-Module/Plan, Trial-Dauer, Demo-Daten-Häkchen, Reason-Pflicht | A4 |
|
||||
| User-Liste + Detail [`/users`, `/:id`] | Liste: Email, Name, Tenant-Memberships, 2FA-Status, letzte Aktivität, Status · Detail: Memberships mit Rolle, Sessions, letzte Logins, Ops (2FA-Reset mit Reason, Session-Revoke, Deaktivieren, Lösch im Auftrag) | A2 / A3 |
|
||||
| Subscription-Liste + Detail [`/subscriptions`, `/:id`] | Liste: Tenant, Plan, MRR, Status, Renewal, Stripe-Sync · Detail: Stripe-Customer/Sub-ID (Link), Plan + Positionen + MRR, Trial, Rechnungs-History, manuelle Override (Plan/Sonderpreis/Reason), Stripe-Sync forcieren, Refund | A2 / A3 |
|
||||
| Subscription-Liste + Detail [`/subscriptions`, `/:id`] | Liste: Tenant, Plan, MRR, Status, Renewal, Legacy-Provider-Sync · Detail: Legacy-Provider-Customer/Sub-ID (Link), Plan + Positionen + MRR, Trial, Rechnungs-History, manuelle Override (Plan/Sonderpreis/Reason), Legacy-Provider-Sync forcieren, Refund | A2 / A3 |
|
||||
| Feature-Flags [`/feature-flags`] | Flag-Liste (Key, Status, Targeting), Filter-Editor (plan-tier), Tenant-Override mit Ablauf *(Massen-Statistik → v2)* | A2 + A6 |
|
||||
| Health + Worker-Queues [`/health`, `/health/workers`] | Service-Ampeln (Web/API/Worker/Scheduler), DB/Redis/Storage · BullMQ-Queues: Active/Waiting/Delayed/Failed je Queue, Job-Detail mit Error-Stack, Retry (Bulk/einzeln), Pause/Resume · Error-Rate Top-10 | A2 + KPI |
|
||||
| Support-Inbox + Ticket-Detail [`/support`, `/:id`] | Liste: Betreff, Tenant, Source, Status, Priorität, Zugewiesen · Detail: Konversation, Antwort (Markdown + Anhänge), interne Notiz, Status/Priority/Tag/Assignee-Wechsler, Reaktions-Templates · **speist sich aus Feedback-Widget (P21)!** | A2 / A3 |
|
||||
@@ -32,7 +32,7 @@ Die Plattform ist aus einer getrennten Admin-Welt betreibbar: Tenant-Management
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] Plattform-Admin meldet sich auf `admin.hivegrid.de` an (Host-Routing, **Cookie-Domain-Trennung**, TOTP-Pflicht + MFA-Step-up); Admin-User haben **keinen** Tenant-Workspace und erscheinen in keiner Mitgliederliste; kein Generalschlüssel zwischen Plattform und Mandanten *(getrennte AdminUser-Identität/AdminRBAC = M038/M039 → v2; v1 = Boilerplate-User-Extension + `admin-mfa-gate`)*
|
||||
- [ ] Admin verwaltet Tenants, Subscription-Overrides und Feature-Flags aus zentraler Konsole; **Abo-Zusammenstellung** = Feature-Konnektoren togglen + Stripe-Positionen (Abo-Baukasten-Mechanik)
|
||||
- [ ] Admin verwaltet Tenants, Subscription-Overrides und Feature-Flags aus zentraler Konsole; **Abo-Zusammenstellung** = Feature-Konnektoren togglen + Legacy-Provider-Positionen (Abo-Baukasten-Mechanik)
|
||||
- [ ] Admin-Dashboard zeigt Boilerplate-KPIs (Tenants/User/MRR) + System-Health + Queue-Status *(Churn/ARPU/Health-Kacheln → v2)*
|
||||
- [ ] Admin überwacht BullMQ-Queues (Retry/Pause/Resume, Job-Detail mit Error-Stack) und bearbeitet eine Support-Inbox
|
||||
- [ ] `AdminAuditLog` protokolliert plattformweite Aktionen (tenant_id=null) über die Audit-Hash-Kette; sensitive Aktionen (Status-Wechsel, Impersonate, Subscription-Override) mit **Reason-Pflicht** + Klartext-Bestätigung
|
||||
@@ -41,7 +41,7 @@ Die Plattform ist aus einer getrennten Admin-Welt betreibbar: Tenant-Management
|
||||
|
||||
## Benötigte UI-Elemente (Liste, KEIN Layout — Anordnung = Davids freie Hand)
|
||||
|
||||
Eigene Admin-Sidebar + -Topbar (getrennt von A1), KPI-Kacheln mit Drill-Down + Zeitraum-Wechsler, System-Health-Ampel, Daten-Tabelle (sortierbar, paginiert, Spalten-Konfig), Filter-Bar, Bulk-Action-Toolbar, Status-Wechsler mit Reason-Pflicht-Modal, **Abo-Baukasten-Panel** (Feature-Konnektor-Toggles + Stripe-Positionsliste mit Preisen), Trial-Verlängerungs-Datum-Picker, Markdown-Notizen-Editor mit Pin + Kategorie, Impersonate-Button mit Begründungs-Modal, Stripe-Link-Badge, manuelle-Override-Formular, Queue-Status-Tabelle mit Retry/Pause/Resume, Job-Detail-Drawer mit Error-Stack, Feature-Flag-Liste + Targeting-Filter-Editor + Tenant-Override-mit-Ablauf, Support-Konversations-Ansicht + Markdown-Reply + interne-Notiz-Toggle + Reaktions-Templates, Audit-Tabelle mit Diff-Drawer + Export, Subprozessor-Tabelle + AVV-Versions-Liste mit PDF-Upload, Bestätigungs-Dialoge (Confirm-Phrase für destruktive Aktionen), Empty-/Loading-/Fehler-/Permission-Denied-States (Klartext mit Lösungs-Knopf).
|
||||
Eigene Admin-Sidebar + -Topbar (getrennt von A1), KPI-Kacheln mit Drill-Down + Zeitraum-Wechsler, System-Health-Ampel, Daten-Tabelle (sortierbar, paginiert, Spalten-Konfig), Filter-Bar, Bulk-Action-Toolbar, Status-Wechsler mit Reason-Pflicht-Modal, **Abo-Baukasten-Panel** (Feature-Konnektor-Toggles + Legacy-Provider-Positionsliste mit Preisen), Trial-Verlängerungs-Datum-Picker, Markdown-Notizen-Editor mit Pin + Kategorie, Impersonate-Button mit Begründungs-Modal, Legacy-Provider-Link-Badge, manuelle-Override-Formular, Queue-Status-Tabelle mit Retry/Pause/Resume, Job-Detail-Drawer mit Error-Stack, Feature-Flag-Liste + Targeting-Filter-Editor + Tenant-Override-mit-Ablauf, Support-Konversations-Ansicht + Markdown-Reply + interne-Notiz-Toggle + Reaktions-Templates, Audit-Tabelle mit Diff-Drawer + Export, Subprozessor-Tabelle + AVV-Versions-Liste mit PDF-Upload, Bestätigungs-Dialoge (Confirm-Phrase für destruktive Aktionen), Empty-/Loading-/Fehler-/Permission-Denied-States (Klartext mit Lösungs-Knopf).
|
||||
|
||||
## Davids Delta (David füllt aus)
|
||||
|
||||
|
||||
+5
-5
@@ -11,7 +11,7 @@
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Ein neuer Betrieb kommt vom Self-Service-Signup per Auto-SHK-Provisionierung ins Produkt, läuft auf Stripe-Billing als Abo-Baukasten (Basis pro Nutzer + Feature-Module als Positionen, Trial mit Dunning + Frozen-Modus), kann optional Demo-Daten anlegen/löschen, sieht eine Erste-Schritte-Checkliste und schaltet flash-frei zwischen Hell und Dunkel um.
|
||||
Ein neuer Betrieb kommt vom Self-Service-Signup per Auto-SHK-Provisionierung ins Produkt, läuft auf Legacy-Provider-Billing als Abo-Baukasten (Basis pro Nutzer + Feature-Module als Positionen, Trial mit Dunning + Frozen-Modus), kann optional Demo-Daten anlegen/löschen, sieht eine Erste-Schritte-Checkliste und schaltet flash-frei zwischen Hell und Dunkel um.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
@@ -20,17 +20,17 @@ Ein neuer Betrieb kommt vom Self-Service-Signup per Auto-SHK-Provisionierung ins
|
||||
| Abo-Übersicht [`/admin/subscription`] | Basis-Plan + **gebuchte Feature-Module als Positionsliste mit Preisen** · Status/Trial-Countdown · **KEIN Upgrade-Button** (Hinweis-Box „Plan-Wechsel über Account-Manager / hivegrid.de", extern) · Pausieren (Frozen) + Kündigen-Button | A6 + KPI |
|
||||
| Verbrauchs-Übersicht [`/admin/usage`] | Quota-Balken je Metrik: Automation-Läufe, AI-Tokens, Speicher · aktueller Verbrauch vs. inklusiv · Klartext bei Limit-Erreichen | KPI + Charts |
|
||||
| AI-Credits-Detail [`/admin/usage/ai-credits`] | Saldo, Verbrauch je Use-Case (Donut), Verlauf, 80%-Warnbanner, Hochrechnung, Aufgebraucht-Klartext (Verweis extern) | KPI + Charts |
|
||||
| Rechnungen/Zahlung [`/admin/billing`] | **Stripe-Customer-Portal-Embed** (Rechnungs-History/PDF, Zahlungsmethode, Steuer-/USt-Daten) | A6 |
|
||||
| Rechnungen/Zahlung [`/admin/billing`] | **Legacy-Provider-Customer-Portal-Embed** (Rechnungs-History/PDF, Zahlungsmethode, Steuer-/USt-Daten) | A6 |
|
||||
| Demo-Daten [`/admin/onboarding/demo-data`] | **Settings-Knopf „Beispieldaten anlegen"** (~25 Datensätze, Klartext-Liste was kommt) · Demo-Liste mit Zähler + Lösch-Funktion (Multi-Step-Confirm) · „Demo ausblenden"-Filter erscheint in allen Listen *(kein Wizard, kein KI-Walkthrough — v2)* | A6 |
|
||||
| Erste-Schritte-Checkliste [`/onboarding/checklist`] | Items mit Status-Häkchen (auto-erkannt), Klartext + Beschreibung, CTA-Link je Item, manuell abhakbar, dauerhaft ausblendbar, Glückwunsch bei 100% (KWP-Item ausgeblendet — v2) · auch als Dashboard-Widget (P11) | A2 |
|
||||
| Status-Banner-System (Shell) | Tenant-Status-Banner: Trial-Ende-3-Tage / `past_due` (Zahlung fehlgeschlagen + „Zahlungsmethode aktualisieren") / `frozen` (pausiert + „Reaktivieren") / `deletion_pending` — je eigener Klartext + Action-Button · Read-Only-Toast bei blockiertem Schreib-Button | in A1-Shell integriert (D0) |
|
||||
| Hell/Dunkel-Toggle [im Profil / User-Menü] | Zwei Optionen Hell / Dunkel, sofortige Anwendung ohne Reload, flash-frei (SSR-Cookie) *(kein System-Modus v1)* | A6 (Teil von Profil-Settings P2) |
|
||||
|
||||
*(Signup-Formular → P2-Auth. Mitarbeiter-Einladung → P2/P20-Seeds. Kein Onboarding-Setup-Wizard v1. Stripe-Checkout läuft extern über Marketing-Site.)*
|
||||
*(Signup-Formular → P2-Auth. Mitarbeiter-Einladung → P2/P20-Seeds. Kein Onboarding-Setup-Wizard v1. Payment-Checkout läuft extern über Marketing-Site.)*
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] Billing über Stripe als **Abo-Baukasten**: Basis-Preis pro Nutzer + Feature-Module als daten-getriebene Positionen (keine Seat-Typen — M154-Recut); konkrete Preise geparkt (AD-15)
|
||||
- [ ] Billing über Legacy-Provider als **Abo-Baukasten**: Basis-Preis pro Nutzer + Feature-Module als daten-getriebene Positionen (keine Seat-Typen — M154-Recut); konkrete Preise geparkt (AD-15)
|
||||
- [ ] Trial mit Dunning (Tag 1/3/7) und **Frozen-Modus** (Read-Only-Middleware) greift; Status-Banner pro Zustand mit Klartext + Action; Verbrauchs-Zähler + Drossel aktiv (Aufschlags-Abrechnung → v2)
|
||||
- [ ] **In-App keine** Pricing-Tabelle/Upgrade-Buttons/Plan-Wechsel-UI (LP-8) — Abo-Übersicht read-only + externer Verweis; Pause/Kündigen sind erlaubt
|
||||
- [ ] Neuer Tenant wird nach Signup automatisch **SHK-provisioniert** (Seeds M158: Pipelines, Custom-Felder, Rollen, Form-Templates, Checkliste) — **ohne Wizard-UI** (die folgt v2); Trial-Freischaltung durch Plattform-Admin
|
||||
@@ -40,7 +40,7 @@ Ein neuer Betrieb kommt vom Self-Service-Signup per Auto-SHK-Provisionierung ins
|
||||
|
||||
## Benötigte UI-Elemente (Liste, KEIN Layout — Anordnung = Davids freie Hand)
|
||||
|
||||
Plan-Positionsliste (Basis + Feature-Module mit Preisen, read-only), Trial-Countdown-Badge, KPI-/Quota-Balken (Verbrauch vs. inklusiv), Donut-Chart (AI-Verbrauch je Use-Case), 80%-Warnbanner, externer Verweis-Box („Plan-Wechsel über hivegrid.de", neuer Tab), Pause-Button + Kündigen-Button mit Bestätigungs-Modal, Stripe-Customer-Portal-Embed (iFrame/Redirect), Demo-Daten-Anlege-Knopf mit Klartext-Inhaltsliste, Demo-Zähler + Lösch-Button mit Multi-Step-Confirm, „Demo ausblenden"-Filter-Toggle, Checklisten-Items mit Status-Icon + CTA-Link + manuell-abhaken + ausblenden, Status-Banner-Stack (trial/past_due/frozen/deletion_pending) mit Action-Button, Read-Only-Toast bei blockiertem Schreib-Button, Hell/Dunkel-Toggle (2 Optionen), Empty-/Loading-/Fehler-/Permission-Denied-States (Klartext mit Lösungs-Knopf).
|
||||
Plan-Positionsliste (Basis + Feature-Module mit Preisen, read-only), Trial-Countdown-Badge, KPI-/Quota-Balken (Verbrauch vs. inklusiv), Donut-Chart (AI-Verbrauch je Use-Case), 80%-Warnbanner, externer Verweis-Box („Plan-Wechsel über hivegrid.de", neuer Tab), Pause-Button + Kündigen-Button mit Bestätigungs-Modal, Legacy-Provider-Customer-Portal-Embed (iFrame/Redirect), Demo-Daten-Anlege-Knopf mit Klartext-Inhaltsliste, Demo-Zähler + Lösch-Button mit Multi-Step-Confirm, „Demo ausblenden"-Filter-Toggle, Checklisten-Items mit Status-Icon + CTA-Link + manuell-abhaken + ausblenden, Status-Banner-Stack (trial/past_due/frozen/deletion_pending) mit Action-Button, Read-Only-Toast bei blockiertem Schreib-Button, Hell/Dunkel-Toggle (2 Optionen), Empty-/Loading-/Fehler-/Permission-Denied-States (Klartext mit Lösungs-Knopf).
|
||||
|
||||
## Davids Delta (David füllt aus)
|
||||
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ Deckt den DSGVO-Lebenszyklus ab und macht die Plattform launch-fertig: tenant- u
|
||||
|---|---|---|
|
||||
| Privacy-Hub [`/admin/privacy`] | Übersicht: Export, Löschung, Subprozessoren, AVV, Privacy-Manifest-Link · Einstieg in die Sub-Bereiche | A6 + KPI |
|
||||
| Tenant-Daten-Export [`/admin/privacy/export`, `/new`, `/:jobId`] | Job-Liste mit Status/Progress/Download (72h-Link) · Anstoßen mit Klartext (was, geschätzte Größe/Dauer, 7-Tage-Window) · Job-Status mit Progress | A2 + A5 |
|
||||
| **Tenant-Löschung** [`/admin/privacy/deletion`, `/:requestId`] | Multi-Step: Erklärung (30-Tage-Grace, Read-Only, Hard-Delete) → Export-Angebot → Stripe-Hinweis (Pro-rata) → Passwort + 2FA → **Slug als Confirm-Phrase** · Grace-Countdown mit Abbruch-Button | A5 (Wizard) |
|
||||
| **Tenant-Löschung** [`/admin/privacy/deletion`, `/:requestId`] | Multi-Step: Erklärung (30-Tage-Grace, Read-Only, Hard-Delete) → Export-Angebot → Legacy-Provider-Hinweis (Pro-rata) → Passwort + 2FA → **Slug als Confirm-Phrase** · Grace-Countdown mit Abbruch-Button | A5 (Wizard) |
|
||||
| Subprozessoren [`/admin/privacy/subprocessors`] | Tabelle: Name, Land, EU/EEA-Marker, Zweck, Datenkategorien, DPA-Link, „aktiv seit" · Filter (EU/alle, Kategorie) · Export CSV/PDF | A2 |
|
||||
| AVV [`/admin/privacy/avv`] | Aktuelle Version als PDF-Download + Akzeptanz-History (Version, Datum, Signer) · Re-Akzeptanz-Modal bei Update (Klartext-Änderungen) | A2 + A5 |
|
||||
| User-Daten-Export [`/settings/account/data-export`, `/:jobId`] | Klartext was exportiert wird, Start-Button, Rate-Limit-Hinweis, Job-Status mit Download (signed-URL) | A6 |
|
||||
@@ -32,7 +32,7 @@ Deckt den DSGVO-Lebenszyklus ab und macht die Plattform launch-fertig: tenant- u
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] Nutzer kann **DSGVO-ZIP-Export** anstoßen (User-Self **und** Tenant-Full); asynchroner BullMQ-Job mit Progress, 72h-signed-URL-Download, Rate-Limit (max. 3 User-Self/24h, max. 1 Tenant-Full aktiv); README + manifest.json im ZIP
|
||||
- [ ] **Tenant-Löschung** führt Crypto-Shredding (Per-Tenant-DEK), Stripe-Cancel und Storage/Cache/Index-Bereinigung durch (AD-8); 30-Tage-Grace mit Read-Only + Abbruch-Möglichkeit; **Slug als Confirm-Phrase**; Stripe-Cancel erst beim Hard-Delete-Cron
|
||||
- [ ] **Tenant-Löschung** führt Crypto-Shredding (Per-Tenant-DEK), Legacy-Provider-Cancel und Storage/Cache/Index-Bereinigung durch (AD-8); 30-Tage-Grace mit Read-Only + Abbruch-Möglichkeit; **Slug als Confirm-Phrase**; Legacy-Provider-Cancel erst beim Hard-Delete-Cron
|
||||
- [ ] **DSGVO-Auskunft als dokumentiertes manuelles Playbook** (Suche → Kontakt-Detail → CSV → Anwalts-Vorlage → Frist-Task +25 Tage); AVV-Vorlage liegt vor (Externals E6) — **kein** automatisches Tooling v1
|
||||
- [ ] AVV-Download + Akzeptanz-History + Re-Akzeptanz-Flow bei Updates (14-Tage-Frist, Read-Only-Sperre danach); Subprozessoren-Liste mit Drittland-Klartext-Warnung (v1 EU-only)
|
||||
- [ ] **In-App-Feedback-Widget** (Bug/Feature/Frage, Screenshot, Rate-Limit) live → speist Admin-Support-Inbox (P19, generische Quelle)
|
||||
|
||||
+5
-5
@@ -8,20 +8,20 @@
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Externe Systeme lesen die HiveGrid-Hauptentitäten (Kontakte, Firmen, Deals, Projekte, Tasks, Leads) über eine **versionierte REST-API** (`/api/v1/…`, cursor-paginiert) und tauschen Events über **signierte Webhooks** aus — eingehend (Lead-Ingestion mit Auto-Match) wie ausgehend (HMAC-SHA256, Idempotenz, Retry/Backoff, Auto-Disable). Auth läuft über Tenant-API-Keys mit Scopes und Rate-Limit; die ausgehende Webhook-Action dockt an die v1-Automation (`external_event`-Trigger) an.
|
||||
Externe Systeme lesen die HiveGrid-Hauptentitäten (Kontakte, Firmen, Deals, Projekte, Tasks, Leads) über eine **versionierte REST-API** auf `api.hivegrid.de/v1/...` (staging: `staging-api.hivegrid.de/v1/...`, cursor-paginiert) und tauschen Events über **signierte Webhooks** aus — eingehend (Lead-Ingestion mit Auto-Match) wie ausgehend (HMAC-SHA256, Idempotenz, Retry/Backoff, Auto-Disable). Auth läuft über Tenant-API-Keys mit Scopes und Rate-Limit; die ausgehende Webhook-Action dockt an die v1-Automation (`external_event`-Trigger) an. Keine Public-API-Routen unter `app.hivegrid.de/api`.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
| Screen [Route] | Kern-Inhalt | Archetyp |
|
||||
|---|---|---|
|
||||
| API-Key-Verwaltung [`/admin/api-keys`] | Key-Liste (Name, Scopes, letzte Nutzung, Status, Tarif-Gate) · Key erstellen (Scope-Auswahl, Secret einmal sichtbar) · Rotieren/Widerrufen | A2 + A5 (Erweiterung v1) |
|
||||
| Incoming-Webhook-Verwaltung [`/admin/api-keys/incoming-webhooks`] | Slug-Endpoints (`/webhooks/incoming/{tenant}/{slug}`), Auth-Modus (API-Key/HMAC/URL-Token), Payload-Mapping, Auto-Match-Regeln · Ingestion-Log (Eingang, Match-Ergebnis, Fehler) | A2 + A3 |
|
||||
| Incoming-Webhook-Verwaltung [`/admin/api-keys/incoming-webhooks`] | Slug-Endpoints (`api.hivegrid.de/v1/webhooks/incoming/{tenant}/{slug}`), Auth-Modus (API-Key/HMAC/URL-Token), Payload-Mapping, Auto-Match-Regeln · Ingestion-Log (Eingang, Match-Ergebnis, Fehler) | A2 + A3 |
|
||||
| Outgoing-Subscription-Liste [`/admin/webhooks`] | Ziel-URL, abonnierte Event-Typen, Status (aktiv/auto-disabled), letzte Zustellung, Fehlerzähler | A2 |
|
||||
| Outgoing-Subscription-Detail [`/admin/webhooks/:id`] | Secret (einmal sichtbar), Delivery-Stats, Delivery-Log mit Retry-Status, Replay/Test-senden, Aktivieren/Pausieren | A3 |
|
||||
| API-Request-Logs + Usage [`/admin/api-keys/usage`] | Request-Log (Endpoint, Key, Status, Latenz, Rate-Limit-Treffer) · Usage-Aggregat (Calls/h pro Key + Tenant, Quota-Auslastung) | A2 |
|
||||
| OpenAPI/Swagger-UI [`/api/v1/openapi` · Swagger] | Auto-generierte OpenAPI-3.1-Doku + interaktiver Try-it-out (entwickler-gerichtet, evtl. eingebettete Drittanbieter-UI) | A6/extern |
|
||||
| OpenAPI/Swagger-UI [`api.hivegrid.de/v1/openapi.json` · Swagger auf API-Host] | Auto-generierte OpenAPI-3.1-Doku + interaktiver Try-it-out (entwickler-gerichtet, evtl. eingebettete Drittanbieter-UI) | A6/extern |
|
||||
|
||||
*(Die eigentlichen REST-Endpoints `/api/v1/contacts|companies|deals|projects|tasks|leads` sind serverseitig — kein eigener Screen außer Swagger-UI.)*
|
||||
*(Die eigentlichen REST-Endpoints `api.hivegrid.de/v1/contacts|companies|deals|projects|tasks|leads` sind serverseitig — kein eigener Screen außer Swagger-UI.)*
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
@@ -30,7 +30,7 @@ Externe Systeme lesen die HiveGrid-Hauptentitäten (Kontakte, Firmen, Deals, Pro
|
||||
- [ ] API-Key-Auth mit Scopes + Rate-Limit (1k/h pro Key, 10k/h pro Tenant), `X-RateLimit-*`-Header + Klartext-429
|
||||
- [ ] Eingehende Lead-Webhooks legen Leads an (API-Key/HMAC/URL-Token + Payload-Mapping + Auto-Match)
|
||||
- [ ] Ausgehende WebhookSubscriptions: HMAC-SHA256-Signatur, Idempotenz-ID, Retry mit exp. Backoff (max 5), Auto-Disable nach 10 Fehlern
|
||||
- [ ] OpenAPI-3.1 auto-generiert (`/api/v1/openapi.json`) + Swagger-UI
|
||||
- [ ] OpenAPI-3.1 auto-generiert (`api.hivegrid.de/v1/openapi.json`) + Swagger-UI auf API-Host
|
||||
- [ ] Tarif-Gates (M027): API-Keys/Webhooks nur in passendem Abo verfügbar
|
||||
- [ ] Writes durch v1-Frozen/past_due-Middleware geblockt (402); RLS via `app.tenant_id`
|
||||
|
||||
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
# Designer-Brief · P23 — Postfach-Sync (Email-Inbox) (v2-P2)
|
||||
|
||||
**Phase:** 23/34 (v2-P2) · **Modul:** M09 (M096–M098) · **Owner-Dev:** Akeshan (VA-P23) · **Designer:** David · **Slot:** v2-W1 (13.–17.07.)
|
||||
**Quellen:** `ROADMAP-V2.md` §v2-P2 (Screens/Features = Hauptquelle) · `ROADMAP.md` §Phase 23 · `ENTITY-MODEL.md` (Activity-Stream/Contact-Deal-Project-FKs) · `_bmad-output/specs/M09-email-inbox.md` (OAuth-Sync/Inbox/Threading-Teile)
|
||||
**Quellen:** `ROADMAP-V2.md` §v2-P2 (Screens/Features = Hauptquelle) · `ROADMAP.md` §Phase 23 · `ENTITY-MODEL.md` (Activity-Stream/Contact/Lead/Projekt-FKs) · `_bmad-output/specs/M09-email-inbox.md` (OAuth-Sync/Inbox/Threading-Teile)
|
||||
**Subframe:** Pflicht je Route. Archetypen aus D0 (A2 Liste, A3 Detail, A5 Modal, A6 Settings).
|
||||
|
||||
> 🔵 **Recut-Hinweis:** Nur der **OAuth-Sync/Inbox/Threading-Teil** von M09 ist v2. Die **Sende-Domain (DKIM/SPF/DMARC)** stammt aus v1-P14 und wird **NICHT** neu gebaut. Versand läuft über die **User-Mailbox** (Gmail/Graph), nicht über die Marketing-Sende-Domain. Push-Sync ist Pflicht (kein Primär-Polling). ⚠️ Gmail restricted scopes (`gmail.modify/send`) brauchen Google-Verifizierung/CASA (Antrag lief in v1 EXT-13); Microsoft-Graph-Subscription muss ~alle 3 Tage erneuert werden.
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Gmail- und Outlook-Postfächer der User werden **bidirektional ins CRM gespiegelt**: per OAuth verbunden, per Push-Sync (Gmail Pub/Sub-Watch, Microsoft Graph-Subscription) aktuell gehalten, **auto-gematcht** auf Contact/Lead/Deal und im **Activity-Stream** sichtbar. User lesen Threads in einer Inbox-Liste mit sicherem HTML-Render und antworten/leiten weiter über die eigene Mailbox. Permission-getrennt über `email.read_own` vs. `email.read_all`.
|
||||
Gmail- und Outlook-Postfächer der User werden **bidirektional ins CRM gespiegelt**: per OAuth verbunden, per Push-Sync (Gmail Pub/Sub-Watch, Microsoft Graph-Subscription) aktuell gehalten, **auto-gematcht** auf Contact/Lead/Projekt und im **Activity-Stream** sichtbar. User lesen Threads in einer Inbox-Liste mit sicherem HTML-Render und antworten/leiten weiter über die eigene Mailbox. Permission-getrennt über `email.read_own` vs. `email.read_all`.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
@@ -16,14 +16,14 @@ Gmail- und Outlook-Postfächer der User werden **bidirektional ins CRM gespiegel
|
||||
|---|---|---|
|
||||
| Mailbox-Connect-Settings [`/settings/integrations/email`] | Postfach verbinden (Gmail/Microsoft OAuth), Sync-Status (Initial-Sync-Progress, letzter Sync, Fehler), Reconnect/Token-Refresh, Trennen/Revoke | A6 |
|
||||
| Inbox-Liste [`/emails`] | Mail-Liste (Absender, Betreff, Snippet, Datum, Verknüpfung-Badge, gelesen/ungelesen) · Filter (Mailbox/verknüpft/unverknüpft/Datum) · Volltext-Suche · Bulk-Actions | A2 |
|
||||
| Mail-Detail [`/emails/:id`] | Thread-Ansicht (chronologisch), sicherer HTML-Render (externe Bilder blockiert/optional laden), Anhänge (→ Files), Verknüpfungs-Editor (Contact/Lead/Deal/Project, manueller Override), Reply/Forward | A3 |
|
||||
| Mail-Detail [`/emails/:id`] | Thread-Ansicht (chronologisch), sicherer HTML-Render (externe Bilder blockiert/optional laden), Anhänge (→ Files), Verknüpfungs-Editor (Contact/Lead/Projekt, manueller Override), Reply/Forward | A3 |
|
||||
| Outbound-Compose [`/emails/new`] | Neue Mail / Reply / Forward über User-Mailbox, Empfänger, Betreff, Body, Anhänge, Vorlagen-Auswahl | A5/A3 |
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] OAuth-Connect Gmail (Pub/Sub-Watch) + Microsoft (Graph-Subscription) mit 3-Tage-Renewal-Worker, Token-Refresh + Revoke
|
||||
- [ ] Push-Sync (kein Primär-Polling); Initial-Sync 90 Tage, resumebar mit Progress-Anzeige
|
||||
- [ ] Auto-Match: From-Adresse → Contact/Lead, Subject-Pattern (`[Deal #123]`), 1-offener-Deal-Heuristik; manueller Override sticht
|
||||
- [ ] Auto-Match: From-Adresse → Contact/Lead, Subject-Pattern (`[Projekt #123]`), 1-offenes-Projekt-Heuristik; manueller Override sticht
|
||||
- [ ] Inbox-Liste mit Filter/Suche/Bulk + Detail mit Thread + sicherem HTML-Render (CSP, externe Bilder blockiert)
|
||||
- [ ] Reply/Forward via User-Mailbox (Provider-Quotas beachten) + Vorlagen (M098)
|
||||
- [ ] Anhänge landen in v1-Files; Mails erscheinen im Activity-Stream
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
**Quellen:** `ROADMAP-V2.md` §v2-P3 (Screens/Features = Hauptquelle) · `ROADMAP.md` §Phase 24 · `_bmad-output/specs/M28-subscriptions-billing.md` (Block P) · `_bmad-output/specs/M29-onboarding.md`
|
||||
**Subframe:** Pflicht je Route. Archetypen aus D0 (A2 Liste, A3 Detail, A5 Modal, A6 Settings, Vollbild-Flow).
|
||||
|
||||
> 🔵 **Recut-/PWA-Hinweis:** **PWA-only (AD-17)** → Self-Service-Billing läuft komplett über **Web-Checkout** (v1-Stripe-Checkout wiederverwenden). **Keine App-Store-Billing-Auflage** — Apple-Guideline 3.1.3 entfällt vollständig, Plan-Wechsel direkt im Web. Self-Service ist v2, weil konkrete **Preise feststehen müssen** (v1 EXT-14 fixiert). Das Wizard-Framework ist großteils v1-fähig; v2-Delta = weitere Branchen + KI-Coach (per Retro-Integration aus P25) + Migration-Importer. Branchen-Template-Seeding ist ein **geteilter Mechanismus** (P24/P26/P30).
|
||||
> 🔵 **Recut-/PWA-Hinweis:** **PWA-only (AD-17)** → Self-Service-Billing läuft komplett über **Web-Checkout** (v1-Payment-Checkout wiederverwenden). **Keine App-Store-Billing-Auflage** — Apple-Guideline 3.1.3 entfällt vollständig, Plan-Wechsel direkt im Web. Self-Service ist v2, weil konkrete **Preise feststehen müssen** (v1 EXT-14 fixiert). Das Wizard-Framework ist großteils v1-fähig; v2-Delta = weitere Branchen + KI-Coach (per Retro-Integration aus P25) + Migration-Importer. Branchen-Template-Seeding ist ein **geteilter Mechanismus** (P24/P26/P30).
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
@@ -28,7 +28,7 @@ Tenants buchen und wechseln Pläne **self-service** über den Web-Checkout (Tria
|
||||
- [ ] SHK-Branchen-Template (Pipelines/Rollen/Custom-Felder/Forms/Demo/Checkliste) deklarativ aus YAML geseedet; weitere Branchen „coming soon"
|
||||
- [ ] Erste-Schritte-Checkliste mit Detection-Rules + Demo-Daten-Bulk-Löschung
|
||||
- [ ] KI-Walkthrough-Coach-Andockpunkt (wird von P25 rückwirkend eingehängt — kein harter Forward-Dep)
|
||||
- [ ] Wiederverwendung v1: Stripe-Checkout + Webhook-Handler + StripeWebhookLog + SubscriptionFeature/ProductFeature (Feature-Gate) + Read-Only-Middleware + TenantUserInvitation
|
||||
- [ ] Wiederverwendung v1: Payment-Checkout + Webhook-Handler + Legacy-ProviderWebhookLog + SubscriptionFeature/ProductFeature (Feature-Gate) + Read-Only-Middleware + TenantUserInvitation
|
||||
|
||||
## Benötigte UI-Elemente (Liste, KEIN Layout — Anordnung = Davids freie Hand)
|
||||
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
# Designer-Brief · P25 — KI-Tiefe: HiveMind/RAG (v2-P4)
|
||||
|
||||
> ⛔ **ABGELÖST (KI→MCP-Pivot, [[AD-26]], 2026-06-22):** HiveMind/RAG/Whisper-UI entfällt (zurückgestellt). Ersetzt durch **[[VD-MCP]]** (MCP-Verbindungen/Settings). Brief unten = historisch.
|
||||
|
||||
**Phase:** 25/34 (v2-P4) · **Modul:** M18 (M087 + Whisper-EU + pgvector-Hybrid-Suche + zweiter EU-Provider) · **Owner-Dev:** Raffael (VR-P25) · **Designer:** David · **Slot:** v2-W1→W2 (Start 18./19.07.)
|
||||
**Quellen:** `ROADMAP-V2.md` §v2-P4 (Screens/Features = Hauptquelle) · `ROADMAP.md` §Phase 25 · `_bmad-output/specs/M18-ki-layer.md` (RAG/HiveMind/Whisper-Teile)
|
||||
**Subframe:** Pflicht je Route. Archetypen aus D0 (A2 Liste, A3 Detail, A5 Modal, A6 Settings, Chat-Panel).
|
||||
|
||||
> 🔴 **DSGVO-Hard-Constraint:** **Nur EU-Provider** (kein OpenAI/Anthropic/Gemini), AVV mit allen Providern. Aufbau auf dem **v1-LLM-Gateway** (Multi-Provider-Abstraktion) — **NICHT** neu bauen. Der **HiveMind-Agent** läuft erst nach etabliertem Werkzeug-Modus (Reifegrad-Gate) autonom und immer mit **Action-Boundary**: keine schreibenden Aktionen ohne explizite Bestätigung. Hilfe-Korpus für RAG kommt aus v2-P9 (Help-Center) per Retro-Integration.
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Aus dem v1-LLM-Gateway wird semantische Tiefe: eine **Embedding-Pipeline** (pgvector, permission-vorgefiltert) speist **RAG** für Programm-Hilfe, Lead-Zusammenfassungen und eine **hybride globale Suche** (Volltext + Vektor, RRF). **Whisper-EU** transkribiert Voice-Notes async. Der **HiveMind-Agent** liefert proaktive Vorschläge (Insights) mit Bestätigungs-Workflow, Audit-Trail und Scheduled-Actions. Multi-Provider-Failover (Mistral→Scaleway→AlephAlpha) + Promptfoo-Eval in CI.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
| Screen [Route] | Kern-Inhalt | Archetyp |
|
||||
|---|---|---|
|
||||
| KI-Chat/Assistent-Panel [überall einblendbar / `/assistant`] | Chat mit RAG-Antworten (Quellen-Zitate), Conversation-History, Use-Case-Kontext (aktuelle UI-Sicht), Graceful-Degradation-Hinweis bei Provider-Down (Fallback Help-Volltext) | Chat-Panel |
|
||||
| HiveMind-Insight-Anzeige [Dashboard-Widget / Detail] | Proaktive Vorschläge (Stuck-Deals/Stuck-Projects), Bestätigungs-Workflow (annehmen/verwerfen) mit Action-Boundary, Begründung/Quelle | A2/Widget |
|
||||
| Custom-Tool-Editor + Scheduled-Actions [`/admin/ai/tools`] | Tenant-definierte HiveMind-Tools anlegen/bearbeiten, Cron-basierte Scheduled-Actions, Action-Log (Audit-Trail autonomer Actions) | A2 + A3 |
|
||||
| AI-Usage-Dashboard [`/admin/usage/ai`] | Token-/Credit-Verbrauch (AiCredits/AiTokenUsage), pro Use-Case/Provider, Quota/Failover-Status | A6 |
|
||||
| Whisper-Aufnahme/Transkription [in Abnahme/Notes eingebettet] | Voice-Note aufnehmen/hochladen, async Transkriptions-Status, Resultat + Klartext-Fehlerpfad | A5/eingebettet |
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] Embedding-Pipeline → `TenantEmbedding` (pgvector, HNSW/IVFFlat), Re-Embed per Hash-Diff, Permission-Pre-Filter vor Vector-Search
|
||||
- [ ] RAG bedient Programm-Hilfe (Help-Embeddings aus P9), Lead-Zusammenfassung, hybride globale Suche (Volltext+Vektor, RRF); Graceful Degradation bei Provider-Down
|
||||
- [ ] Whisper-EU (Scaleway-GPU) transkribiert Voice-Notes async mit Klartext-Fehlerpfad
|
||||
- [ ] HiveMind-Agent: proaktive Vorschläge mit Action-Boundary (keine Writes ohne Bestätigung), Audit-Trail, Scheduled-Actions, Screen-Context-Bewusstsein
|
||||
- [ ] Multi-Provider-Failover (Mistral→Scaleway→AlephAlpha) + Promptfoo-Eval in CI (scheitert bei erfolgreicher Injection)
|
||||
- [ ] Permissions: `ai.use_chat` / `ai.use_transcription` / `ai.basic` gaten Chat/Whisper/Read
|
||||
- [ ] Wiederverwendung v1: LLM-Gateway, `RetrievalProvider`-Interface + Outbox-Index-Events, AiCredits/AiTokenUsage, Activity-Stream/Custom-Felder/Forms als RAG-Quellen
|
||||
|
||||
## Benötigte UI-Elemente (Liste, KEIN Layout — Anordnung = Davids freie Hand)
|
||||
|
||||
Chat-Panel (Nachrichten-Stream, Eingabefeld, Streaming-Antwort), Quellen-Zitat-Chips/Aufklapp (RAG-Belege), Conversation-History-Liste, Insight-Karten mit Annehmen/Verwerfen + Begründung, Action-Boundary-Bestätigungs-Dialog (zeigt geplante Action vor Ausführung), Audit-Trail-Timeline (autonome Actions), Custom-Tool-Editor (Tool-Definition-Felder), Scheduled-Action-Konfig (Cron + Tool-Auswahl), Voice-Recorder-Control (Aufnahme/Stop/Upload) + Transkriptions-Status-Indikator, Usage-/Quota-Charts pro Provider/Use-Case, Provider-Down-/Degradation-Banner (Klartext-Fallback-Hinweis), Permission-Denied-/Empty-/Loading-States (Klartext), DSGVO-/EU-Provider-Hinweis (Transparenz).
|
||||
|
||||
## Davids Delta (David füllt aus)
|
||||
|
||||
- [ ] Überschriebene/geänderte Spec-Punkte:
|
||||
- [ ] Neue UI/UX-Entscheidungen:
|
||||
- [ ] Subframe-Screen-Link(s):
|
||||
- [ ] Offene Fragen an Dev:
|
||||
|
||||
## Dev-Nachzug (wir nach Davids Delta)
|
||||
|
||||
- [ ] Spec/ENTITY-MODEL an Davids Entscheidungen angepasst:
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen:
|
||||
- [ ] Plane-Ticket (VR-P25) aktualisiert:
|
||||
+5
-5
@@ -4,11 +4,11 @@
|
||||
**Quellen:** `ROADMAP-V2.md` §v2-P11 (NUR Telefonie-Teil) · `ROADMAP.md` §Phase 33 · `_bmad-output/specs/M12-telefonie.md` (Provider-Pattern)
|
||||
**Subframe:** Pflicht je Route. Setup-/Binding-Flächen + Call-Session-Detail.
|
||||
|
||||
> 🔴 **Block 2, Spec offen (⚠ Vorlauf VEXT-2):** HalloPetra kommt in **keiner Spec** vor — M12 ist eine reine AirCall-Spec. HalloPetra ist ein **zweiter Telefonie-Provider hinter dem vorhandenen `TelephonyProviderInterface`** (connect/triggerOutboundCall/subscribeToWebhooks/verifyWebhookSignature/parseWebhookEvent/getRecordingUrl) — mechanisch klar, inhaltlich unspezifiziert. **Vor Bau:** eigene Vorlauf-Spec (API-Vertrag: Auth/Webhook-Events/Recording/Call-Lifecycle) erheben. Agil vorziehbar, sobald spezifiziert. Voicemail-Transkription via Whisper koppelt an v2-P4. Der AirCall-Adapter (v1-P15) ist die Referenz.
|
||||
> 🔴 **Block 2, Spec offen (⚠ Vorlauf VEXT-2):** HalloPetra kommt in **keiner Spec** vor — M12 ist eine reine AirCall-Spec. HalloPetra ist ein **zweiter Telefonie-Provider hinter dem vorhandenen `TelephonyProviderInterface`** (connect/triggerOutboundCall/subscribeToWebhooks/verifyWebhookSignature/parseWebhookEvent/getRecordingUrl) — mechanisch klar, inhaltlich unspezifiziert. **Vor Bau:** eigene Vorlauf-Spec (API-Vertrag: Auth/Webhook-Events/Recording/Call-Lifecycle) erheben. Agil vorziehbar, sobald spezifiziert. Voicemail-Transkription via Whisper entfällt nach AD-26. Der AirCall-Adapter (v1-P15) ist die Referenz.
|
||||
|
||||
## Was dieses Modul tut (Kurzfassung)
|
||||
|
||||
Bindet HalloPetra als zusätzlichen Provider hinter dem bestehenden `TelephonyProviderInterface` an — ohne Kern-Umbau. Eingehende Webhooks werden auto-gematcht (auf Contact/Lead/Deal), landen im Activity-Stream und triggern Automationen (`call.*`). Click-to-Call, Inbound und Logging bleiben providerunabhängig: E.164-Normalisierung, Match-Confidence, Notiz-Editor. Aus Tenant-Sicht ist HalloPetra einfach eine weitere wählbare Telefonie-Anbindung neben AirCall.
|
||||
Bindet HalloPetra als zusätzlichen Provider hinter dem bestehenden `TelephonyProviderInterface` an — ohne Kern-Umbau. Eingehende Webhooks werden auto-gematcht (auf Contact/Lead/Projekt), landen im Activity-Stream und triggern Automationen (`call.*`). Click-to-Call, Inbound und Logging bleiben providerunabhängig: E.164-Normalisierung, Match-Confidence, Notiz-Editor. Aus Tenant-Sicht ist HalloPetra einfach eine weitere wählbare Telefonie-Anbindung neben AirCall.
|
||||
|
||||
## Benötigte Screens
|
||||
|
||||
@@ -16,14 +16,14 @@ Bindet HalloPetra als zusätzlichen Provider hinter dem bestehenden `TelephonyPr
|
||||
|---|---|---|
|
||||
| Telefonie-Setup [`/settings/telephony`] | Provider-Connect (HalloPetra neben AirCall), Credential-/OAuth-Eingabe, Verbindungs-Test | A6 + Connect |
|
||||
| User-Bindings [`/settings/telephony/bindings`] | Zuordnung User ↔ Telefonie-Nummer/-Account, providerunabhängig | A2/A6 |
|
||||
| Call-Session-Detail [`/calls/:id`] | Anruf-Daten (Richtung, Dauer, Match auf Contact/Lead/Deal, Match-Confidence), Aufnahme-Link, Notiz-Editor | A3 |
|
||||
| Call-Session-Detail [`/calls/:id`] | Anruf-Daten (Richtung, Dauer, Match auf Contact/Lead/Projekt, Match-Confidence), Aufnahme-Link, Notiz-Editor | A3 |
|
||||
|
||||
*(Click-to-Call ist ein Quick-Action-Element auf Contact/Lead/Deal-Detail-Views aus v1 — providerunabhängig, kein neuer Screen.)*
|
||||
*(Click-to-Call ist ein Quick-Action-Element auf Contact/Lead/Projekt-Detail-Views aus v1 — providerunabhängig, kein neuer Screen.)*
|
||||
|
||||
## Geplante Features (muss abgedeckt sein)
|
||||
|
||||
- [ ] HalloPetra-Adapter implementiert `TelephonyProviderInterface` ohne Kern-Änderung
|
||||
- [ ] Webhook-Auto-Match auf Contact/Lead/Deal → Activity + Automation-Trigger (`call.*`)
|
||||
- [ ] Webhook-Auto-Match auf Contact/Lead/Projekt → Activity + Automation-Trigger (`call.*`)
|
||||
- [ ] Click-to-Call + Inbound + Logging providerunabhängig (E.164-Normalisierung, Match-Confidence, Notiz-Editor)
|
||||
- [ ] Provider-Auswahl/-Wechsel im Setup (HalloPetra neben AirCall)
|
||||
- [ ] Recording-Abruf über `getRecordingUrl` (sofern HalloPetra-API liefert)
|
||||
|
||||
+5
-5
@@ -130,7 +130,7 @@ css: |-
|
||||
## AP-5: P19 Plattform-Admin (Mo bis Mi 22. bis 24.06.)
|
||||
|
||||
**Was:** Die Betriebs-Konsole auf `admin.hivegrid.de`. **Harte Auflagen (Scope-Schnitt 12.06.):** Admin-User bekommen KEINEN Tenant-Workspace · Host-Routing admin.hivegrid.de (Code routet bisher `/admin`-Pfad!) · Cookie-Domain-Trennung zu app.* · TOTP-Pflicht + MFA-Step-up (Boilerplate-Modell, keine getrennte Identität bis v2).
|
||||
**Fertig heißt:** Tenant anlegen/verwalten + **Abo-Zusammenstellung** (Konnektoren je Tenant schalten + Stripe-Positionen) + Impersonation mit Begründungs-Pflicht + Queue-Monitoring.
|
||||
**Fertig heißt:** Tenant anlegen/verwalten + **Abo-Zusammenstellung** (Konnektoren je Tenant schalten + Legacy-Provider-Positionen) + Impersonation mit Begründungs-Pflicht + Queue-Monitoring.
|
||||
|
||||
| Milestone | Inhalt |
|
||||
|---|---|
|
||||
@@ -216,13 +216,13 @@ css: |-
|
||||
|
||||
## AP-10: P20 Billing & Onboarding (Mi 08. bis Do 09.07.)
|
||||
|
||||
**Was:** Stripe mit dem **Abo-Baukasten-Recut**: Basis-Preis pro Nutzer + Feature-Module als Stripe-Positionen. KEINE Seat-Typen! Vorlage: `archive/hivegrid-cloud/app/config/pricing-plans.ts` (Core/Swarm/Colony, 14 Gates). Boilerplate-Billing ist HIGH-Coverage (14 Modelle, ~40 Stripe-Funktionen, `packages/entitlements`).
|
||||
**Fertig heißt:** Admin stellt Tenant-Abo zusammen → Konnektoren schalten frei → Stripe rechnet ab · Trial → Frozen-Modus funktioniert · Demo-Daten-Knopf liefert 25 Datensätze.
|
||||
**Was:** Legacy-Provider mit dem **Abo-Baukasten-Recut**: Basis-Preis pro Nutzer + Feature-Module als Legacy-Provider-Positionen. KEINE Seat-Typen! Vorlage: `archive/hivegrid-cloud/app/config/pricing-plans.ts` (Core/Swarm/Colony, 14 Gates). Boilerplate-Billing ist HIGH-Coverage (14 Modelle, ~40 Legacy-Provider-Funktionen, `packages/entitlements`).
|
||||
**Fertig heißt:** Admin stellt Tenant-Abo zusammen → Konnektoren schalten frei → Legacy-Provider rechnet ab · Trial → Frozen-Modus funktioniert · Demo-Daten-Knopf liefert 25 Datensätze.
|
||||
|
||||
| Milestone | Inhalt |
|
||||
|---|---|
|
||||
| M030 hell-dunkel | Nur Hell/Dunkel-Toggle, flash-frei (next-themes aus Boilerplate; KEIN System-Modus) |
|
||||
| M153 stripe | Stripe-Anbindung: Checkout, Webhooks, Portal-Embed; ersetzt den Subscription-Stub aus P2 |
|
||||
| M153 legacyProvider | Legacy-Provider-Anbindung: Checkout, Webhooks, Portal-Embed; ersetzt den Subscription-Stub aus P2 |
|
||||
| M154 abo-baukasten | Recut: Basis-Preis/Nutzer + Module als Positionen via Entitlements × Flag-Engine (Preise aus Kostenmatrix 25.06.) |
|
||||
| M155 trial-readonly | 14-Tage-Trial ohne Karte, Dunning-Sequenz, Frozen = Read-Only-Middleware |
|
||||
| M156 metering | Verbrauchs-Zähler (Automation-Läufe, AI-Tokens, Speicher) + Fair-Use-Drossel (Abrechnung v2) |
|
||||
@@ -248,7 +248,7 @@ css: |-
|
||||
|
||||
| Milestone | Inhalt |
|
||||
|---|---|
|
||||
| Abo-UI (M154-Teil) | Self-Service-Baukasten: Module togglen, Preis-Vorschau, Stripe-Update live |
|
||||
| Abo-UI (M154-Teil) | Self-Service-Baukasten: Module togglen, Preis-Vorschau, Legacy-Provider-Update live |
|
||||
| M096 sync-strategie | OAuth-Postfach-Anbindung (Entra/Outlook zuerst; Gmail-Scopes laufen bei Google) |
|
||||
| M097 inbox-view | Inbox mit Threading, Auto-Verknüpfung an Kontakt/Deal (Match-Confidence + Override) |
|
||||
| M098 provider-versand | Mail-Versand aus HiveGrid über das verbundene Postfach (Compose, Reply, Vorlagen) |
|
||||
|
||||
@@ -38,7 +38,7 @@ css: |-
|
||||
|
||||
### AP-2: Pflege + Conversion (19. bis 28.06.)
|
||||
|
||||
SEO-Basics · Conversion-Feinschliff · Pricing-Seite vorbereiten (Zahlen kommen am 25.06. aus dem Partner-Gespräch) · Founding-Member-Sektion mit Haris (ab 30.06. live, mit Stripe Payment Link).
|
||||
SEO-Basics · Conversion-Feinschliff · Pricing-Seite vorbereiten (Zahlen kommen am 25.06. aus dem Partner-Gespräch) · Founding-Member-Sektion mit Haris (ab 30.06. live, mit Mollie Payment Link).
|
||||
|
||||
### AP-3: Webinar-Funnel (Mo 29.06. bis Fr 03.07.)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Website geht Mi/Do 17./18. live → Lead-Sheet aufsetzen (bis die HiveGrid-Lead-
|
||||
| Datum | Schritt |
|
||||
|---|---|
|
||||
| Do 25.06. | Preise kommen aus Raffaels Partner-Gespräch → **Founding-Member-Angebot strukturieren** (Early-Bird-Jahresdeal, begrenzte Plätze) → Noah-Abnahme |
|
||||
| Fr 26.06. | Stripe Payment Link von Raffael |
|
||||
| Fr 26.06. | Mollie Payment Link von Raffael |
|
||||
| **Mo 30.06.** | [€] **Founding-Deal live**: Klick-Demos (CRM-Stand der Noah-Demo) + Abschluss via Payment Link. **Geld kann VOR dem Launch fließen** |
|
||||
| ab Fr 03.07. | Echte Produkt-Demos (Kette Lead → Sales → Auftrag läuft E2E) |
|
||||
|
||||
@@ -74,7 +74,7 @@ Fr 24.07.: Trial-Follow-ups (14 Tage nach Launch-Onboardings) · ab ~20.07. neue
|
||||
| Noah | Painpoint-Gespräch (dein Rohstoff für Texte + Pitch) · danach laufend Abnahmen | Sa/So 13./14.06. |
|
||||
| Dejan | Website LIVE + Lead-Routing (jede Anfrage landet sofort bei dir) | Mi/Do 17./18.06. |
|
||||
| David | Screens fürs Pitch-Deck | Fr 19.06. |
|
||||
| Raffael | **Preise** (25.06.) · **Stripe Payment Link** für den Founding-Deal (26.06.) | siehe Daten |
|
||||
| Raffael | **Preise** (25.06.) · **Mollie Payment Link** für den Founding-Deal (26.06.) | siehe Daten |
|
||||
| Akeshan | Demo-Tenant auf Staging für deine Klick-Demos | ab Mo 30.06. |
|
||||
| Dejan | Webinar-Landing für deine Einladungen | Fr 03.07. |
|
||||
| Raffael | Live-Demo im Webinar (du moderierst, er zeigt) | 14./15.07. |
|
||||
|
||||
+3
-3
@@ -65,7 +65,7 @@ css: |-
|
||||
| Akeshan | Service-Kerne aus P1 (Audit, notify, Flags, i18n): er baut ab Mi alles darauf | Di 16.06. |
|
||||
| Akeshan | Apps-Engine-Vertrag (P5): seine P14/P15/P16-Anbindungen laufen darüber | So 21.06. |
|
||||
| Akeshan | CRM-Entitäten (P6): Marketing-Segmente + Chat-Verknüpfungen brauchen Contacts | Mi 24.06. |
|
||||
| Haris | Preise (25.06.) · Stripe Payment Link (26.06.) · Live-Demo im Webinar (14./15.07.) | siehe Daten |
|
||||
| Haris | Preise (25.06.) · Mollie Payment Link (26.06.) · Live-Demo im Webinar (14./15.07.) | siehe Daten |
|
||||
| Dejan | Rechtstexte vom Anwalt durchreichen · Preise für die Pricing-Seite | sobald da / 25.06. |
|
||||
| David | Reconciliation-Feedback je Screen (passt jedes Feld zu einer Datenquelle?) | < 24 h je Screen |
|
||||
| Noah | Demo-Führung an den Gate-Freitagen + KWP-E2E-Termin (24.07.) | Gate-Freitage |
|
||||
@@ -75,7 +75,7 @@ css: |-
|
||||
| Wann | Was |
|
||||
|---|---|
|
||||
| Fr 12.06. | KWP-Antwortmail (Callback-URLs) · bnWin 8.1 N3 an Noah · AVV + Rechtstexte an Anwalt |
|
||||
| Mo 15.06. | Mailjet-DNS + Warm-up · Google-Calendar-OAuth · Stripe-Live · AirCall/PandaDoc-Sandbox · Hetzner-Projekt `HiveGrid` + Mistral-Key · Noah-Slots blocken |
|
||||
| Mo 15.06. | Mailjet-DNS + Warm-up · Google-Calendar-OAuth · Legacy-Provider-Live · AirCall/PandaDoc-Sandbox · Hetzner-Projekt `HiveGrid` + Mistral-Key · Noah-Slots blocken |
|
||||
| Do 25.06. | [€] Kostenmatrix/Preise mit Partner FIX (Blocker für Haris!) |
|
||||
| Mo 29.06. | Web-Push-VAPID-Keys · Gmail-Restricted-Scope-Antrag · Microsoft-Entra-App (Google Play Console / FCM/APNs entfallen — PWA-only, AD-17) |
|
||||
| Mo 13.07. | HalloPetra-Konto · Memberspot-Zugänge |
|
||||
@@ -120,7 +120,7 @@ css: |-
|
||||
|
||||
## AP-3: P5 Apps-Engine (Sa/So 20./21.06.)
|
||||
|
||||
**Was:** DER Integrations-Vertrag. Alles Externe (AirCall, PandaDoc, Stripe, Google, später KWP) läuft über dieses eine Framework.
|
||||
**Was:** DER Integrations-Vertrag. Alles Externe (AirCall, PandaDoc, Legacy-Provider, Google, später KWP) läuft über dieses eine Framework.
|
||||
**Fertig heißt:** Eine DataConnection lässt sich per OAuth ODER API-Key herstellen, Status-Ampel + Klartext, Health-Check, Tokens verschlüsselt (DEK aus Coolify-Secret).
|
||||
|
||||
| Milestone | Inhalt |
|
||||
|
||||
@@ -96,7 +96,7 @@ Note: C-15 spans both api-contract and protocol categories.
|
||||
|
||||
Count: 7 topics
|
||||
|
||||
Topics: Projekt-Überblick und Ziel, Phase-0-Pflichten, Delivery-Sequenzierung, SHK-Branchen-Kontext, Boilerplate-Reuse-Struktur, KWP-Recut-Entscheidung, Spec-Status
|
||||
Topics: Projekt-Überblick und Ziel, Phase-0-Pflichten, Delivery-Sequenzierung, SHK-Branchen-Kontext, HiveGrid-Workspace-Struktur, KWP-Recut-Entscheidung, Spec-Status
|
||||
|
||||
---
|
||||
|
||||
|
||||
+4
-3
@@ -18,7 +18,7 @@
|
||||
"WebSocket",
|
||||
"SSE",
|
||||
"KWP integration",
|
||||
"Stripe",
|
||||
"Legacy-Provider",
|
||||
"Automation Editor",
|
||||
"Mobile Offline",
|
||||
"Customer Portal",
|
||||
@@ -29,8 +29,9 @@
|
||||
],
|
||||
"cross_refs": [
|
||||
"AGENTS.md",
|
||||
"docs/boilerplate-mapping/_SUMMARY.md",
|
||||
"docs/boilerplate-mapping/M19-kwp-integration.md",
|
||||
".planning/ENTITY-MODEL.md",
|
||||
".planning/PHASE-SOURCES.md",
|
||||
"docs/milestones/M19-KWP-API-RECUT.md",
|
||||
"docs/INTEGRATION-CONTRACTS.md",
|
||||
"PLAN.md"
|
||||
],
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@
|
||||
"confidence": "high",
|
||||
"manifest_override": true,
|
||||
"title": "Modul 28 — Subscriptions / Billing",
|
||||
"summary": "Technical specification for HiveGrid subscription engine, Stripe billing integration, seat model, trial/dunning lifecycle, AI credits, and service metering.",
|
||||
"summary": "Technical specification for HiveGrid subscription engine, Legacy-Provider billing integration, seat model, trial/dunning lifecycle, AI credits, and service metering.",
|
||||
"scope": [
|
||||
"TenantSubscription",
|
||||
"SubscriptionProduct",
|
||||
@@ -14,9 +14,9 @@
|
||||
"TenantSubscriptionUsageRecord",
|
||||
"CheckoutSessionStatus",
|
||||
"Credit",
|
||||
"Stripe",
|
||||
"Stripe-Checkout",
|
||||
"Stripe-Webhooks",
|
||||
"Legacy-Provider",
|
||||
"Payment-Checkout",
|
||||
"Legacy-Provider-Webhooks",
|
||||
"Seat-Modell",
|
||||
"Trial-Phase",
|
||||
"Dunning",
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
"KWP",
|
||||
"Gmail",
|
||||
"AirCall",
|
||||
"Stripe",
|
||||
"Legacy-Provider",
|
||||
"PandaDoc",
|
||||
"Marketplace"
|
||||
],
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"ZIP export",
|
||||
"Privacy-Hub routes",
|
||||
"Capacitor app privacy manifest",
|
||||
"Stripe subscription",
|
||||
"Legacy-Provider subscription",
|
||||
"pgvector embeddings",
|
||||
"Redis cache",
|
||||
"Scaleway storage"
|
||||
|
||||
@@ -63,7 +63,7 @@ source: docs/ARCHITECTURE-DECISIONS.md (AD-4)
|
||||
type: nfr
|
||||
source: docs/ARCHITECTURE-DECISIONS.md (AD-5), _bmad-output/specs/M24-globale-suche.md
|
||||
|
||||
- Meilisearch (self-hosted): Keyword-Suche; BP-Paket @toolctrl/search weitgehend 1:1 übernommen
|
||||
- Meilisearch (self-hosted): Keyword-Suche; BP-Paket @hivegrid/search weitgehend 1:1 übernommen
|
||||
- pgvector: Semantische Suche; Embedding-Dimension 1024 (Scaleway EU-Endpoint)
|
||||
- Fusion: RRF (Reciprocal Rank Fusion) kombiniert beide Ergebnislisten
|
||||
- SearchIndexEntry muss tenant_id enthalten; Abfragen stets permission-aware (RLS + Permission-Filter)
|
||||
@@ -96,16 +96,16 @@ source: docs/ARCHITECTURE-DECISIONS.md (AD-9)
|
||||
|
||||
---
|
||||
|
||||
## C-08 — Paket-Naming: @toolctrl/* bleibt, @hivegrid/* nur für neue Querschnittsdienste
|
||||
## C-08 — Paket-Naming: @hivegrid/* ohne Alt-Aliase
|
||||
|
||||
type: protocol
|
||||
source: docs/ARCHITECTURE-DECISIONS.md (AD-10), docs/INTEGRATION-CONTRACTS.md
|
||||
|
||||
- @toolctrl/*: Boilerplate-Pakete, unveränderter Name (upstream-mergebar)
|
||||
- @hivegrid/*: einheitlicher Scope fuer alle Workspace-Pakete
|
||||
- packages/ui-app (nicht ui-hivegrid): HiveGrid-UI-Shell
|
||||
- apps/web/app/application/sidebar/: Sidebar-Konfiguration (nicht sidebar.config.ts)
|
||||
- packages/search/: Such-Index / indexedEntities (nicht packages/db/src/search/indexed-entities.ts)
|
||||
- @hivegrid/*: nur neue, genuin HiveGrid-eigene Pakete (z.B. @hivegrid/activity, @hivegrid/audit, @hivegrid/notifications)
|
||||
- keine Alt-Scope-Package-Aliase, kein altes CLI-Binary, keine parallelen technischen Produkt-Contracts
|
||||
|
||||
---
|
||||
|
||||
@@ -134,7 +134,8 @@ Verbindliche URL-Schemata (Abweichungen erfordern Change-Request):
|
||||
- Public-Forms: app.hivegrid.de/f/{tenant}/{slug}
|
||||
- Public-Booking: app.hivegrid.de/b/{tenant}/{slug}
|
||||
- Public-Help: app.hivegrid.de/help/{lang}/{kategorie}/{slug}
|
||||
- Public-API: app.hivegrid.de/api/v1/{resource}
|
||||
- Public-API: api.hivegrid.de/v1/{resource}
|
||||
- MCP-Server: mcp.hivegrid.de
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ source: docs/ARCHITECTURE-DECISIONS.md
|
||||
|
||||
HiveGrid 2.0 ist eine SaaS-Plattform für SHK-Handwerksbetriebe (Sanitär, Heizung, Klima). Erstkunde: Geerkens GmbH. Das Produkt löst ab: HubSpot (CRM + Sales-Pipeline), Quiply (Team-Chat), Zapier/n8n (Automation), sowie mehrere Excel-Prozesse.
|
||||
|
||||
Die Codebase ist ein Git-Downstream der ToolCTRL-Boilerplate (upstream-Remote bleibt erhalten). Boilerplate deckt ~35–45 % der Anforderungen ab (Fundament high, Fachdomäne greenfield).
|
||||
Die Codebase wird als bespoke HiveGrid-Anwendung weitergeführt. Historisches Fundament wird genutzt, produktfremde Altlasten werden entfernt.
|
||||
|
||||
USP-Säulen (Day-1-Launch): Automation-Engine (visuell, KI-gestützt) + KI-Layer (EU-only) + KWP-Integration (Kern-ERP Geerkens).
|
||||
|
||||
@@ -22,8 +22,8 @@ USP-Säulen (Day-1-Launch): Automation-Engine (visuell, KI-gestützt) + KI-Layer
|
||||
source: docs/ARCHITECTURE-DECISIONS.md
|
||||
|
||||
Die folgenden Voraussetzungen sind vor dem ersten Fachmodul-Bau abzuarbeiten:
|
||||
1. Boilerplate einziehen (Repo ist aktuell Docs-only, keine echte Codebase)
|
||||
2. Naming-Drift in Docs beheben (AD-10)
|
||||
1. Codebase lauffähig halten
|
||||
2. Naming-Drift in Code und Docs beheben (AD-10)
|
||||
3. EU-Sterilisation: GitHub/Google-OAuth-Reste entfernen
|
||||
4. M00 aktivieren + 4 Querschnitts-Neubauten (logActivity, Search-Registry, Subframe-Adapter, Mass-Action-Schutz)
|
||||
5. Fehlende Dependencies eintragen (PLAN.md teils falsch)
|
||||
@@ -59,18 +59,18 @@ SHK-Seeds (beim Tenant-Anlegen): 6 Standard-Rollen, 2 Sales-Pipelines, Custom-Fe
|
||||
|
||||
---
|
||||
|
||||
## Topic: Boilerplate-Reuse-Struktur
|
||||
## Topic: HiveGrid-Workspace-Struktur
|
||||
|
||||
source: docs/ARCHITECTURE-DECISIONS.md (AD-10), docs/INTEGRATION-CONTRACTS.md
|
||||
|
||||
Boilerplate-Pakete (unveränderter Name, upstream-mergebar):
|
||||
Workspace-Pakete im `@hivegrid/*`-Scope:
|
||||
- packages/ui-app (UI-Shell)
|
||||
- packages/ui-subframe (Subframe-Adapter)
|
||||
- packages/ui-mock (Mock-UI für Hot-Swap-Tests)
|
||||
- @toolctrl/search (Meilisearch-Paket)
|
||||
- @toolctrl/foundation (Basis-Utilities)
|
||||
- @hivegrid/search (Meilisearch-Paket)
|
||||
- @hivegrid/foundation (Basis-Utilities)
|
||||
|
||||
Neue HiveGrid-Querschnittsdienste (zu bauen, @hivegrid/*-Namespace):
|
||||
HiveGrid-Querschnittsdienste:
|
||||
- @hivegrid/activity (logActivity-Service + zentrale Activity-Tabelle)
|
||||
- @hivegrid/audit (auditEvent-Service)
|
||||
- @hivegrid/notifications (notify-Service)
|
||||
|
||||
@@ -67,7 +67,7 @@ status: Accepted (locked)
|
||||
scope: Globale Suche, Search-Infrastruktur
|
||||
source: docs/ARCHITECTURE-DECISIONS.md
|
||||
|
||||
Entscheidung: Keyword-Suche über selbst gehostetes Meilisearch (BP-`@toolctrl/search` weitgehend 1:1), semantische Suche über pgvector + EU-Embeddings (Scaleway 1024-dim, BP 1:1), kombiniert via RRF-Fusion. Embedding-Pipeline + Outbox + Indexer-Worker aus der BP übernehmen.
|
||||
Entscheidung: Keyword-Suche über selbst gehostetes Meilisearch (BP-`@hivegrid/search` weitgehend 1:1), semantische Suche über pgvector + EU-Embeddings (Scaleway 1024-dim, BP 1:1), kombiniert via RRF-Fusion. Embedding-Pipeline + Outbox + Indexer-Worker aus der BP übernehmen.
|
||||
|
||||
Hinweis: Weicht bewusst von M056 "Postgres-FTS, kein Extra-Service" ab → Change-Request gegen M056 nötig.
|
||||
Betrifft: M24 / M056–M058
|
||||
@@ -120,16 +120,18 @@ Betrifft: M13 (bidirektional WebSocket); SSE-Pfad: M03, M22
|
||||
|
||||
---
|
||||
|
||||
## AD-10 — Naming-Konvention: `@toolctrl/*` behalten, Docs anpassen
|
||||
## AD-10 — Naming-Konvention: HiveGrid-Scope ohne Alt-Aliase
|
||||
|
||||
status: Accepted (locked)
|
||||
scope: Paket-Naming, Mono-Repo-Struktur, alle Module
|
||||
source: docs/ARCHITECTURE-DECISIONS.md
|
||||
|
||||
Entscheidung: Boilerplate-Pakete behalten ihre Namen (`@toolctrl/*`, `packages/ui-app`) — kein flächiger Rename zu `@hivegrid/*`/`ui-hivegrid`.
|
||||
- `@toolctrl/*` + `ui-app`/`ui-subframe`/`ui-mock` = geteilte Boilerplate-Pakete (mergebar mit upstream)
|
||||
- `@hivegrid/*` = nur für genuin neue HiveGrid-Pakete (z.B. Activity-Paket, Domänen-Pakete)
|
||||
- Docs: wo "ui-hivegrid"/"@hivegrid/foundation"/"sidebar.config.ts" steht, ist `ui-app`/`@toolctrl/foundation`/`apps/web/app/application/sidebar/` gemeint
|
||||
Entscheidung: Alle Workspace-Pakete und technischen Produkt-Contracts nutzen HiveGrid-Namen:
|
||||
`@hivegrid/*`, `hivegrid-cli`, `HIVEGRID_*`, `hivegrid_*`-Cookies, `x-hivegrid-*`-Header und
|
||||
`hivegrid.*`-Queue-Namen. Es gibt keine Alt-Aliase.
|
||||
- `packages/ui-app`, `packages/ui-subframe`, `packages/ui-mock` behalten ihre realen Pfade
|
||||
- Estate-Slugs bleiben externe Infrastruktur (`git.tool-ctrl.de`, `toolctrl-labs`, `toolctrl-ci`, Plane-Workspace `toolctrl`)
|
||||
- Docs: wo "ui-hivegrid", Alt-Scopes oder alte Sidebar-Pfade stehen, ist das Doku-Drift
|
||||
|
||||
Betrifft: Querschnitt (alle Module), Phase 0
|
||||
|
||||
@@ -178,10 +180,10 @@ Entscheidung: Geerkens startet weitgehend leer; KWP-Sync bringt Auftrags-/Stammd
|
||||
## AD-15 — Pricing/Billing: Engine bauen, Preise geparkt
|
||||
|
||||
status: Accepted (locked)
|
||||
scope: Stripe, Billing-Engine, Pricing
|
||||
scope: Legacy-Provider, Billing-Engine, Pricing
|
||||
source: docs/ARCHITECTURE-DECISIONS.md
|
||||
|
||||
Entscheidung: Stripe-Integration + Seat-/Plan-Engine + Verbrauchs-Metering werden technisch gebaut, die konkreten Preis-Stufen bleiben geparkt. Geerkens läuft auf manueller Sonderkondition/Trial. Self-Service-Checkout-Preise = eigene spätere Pricing-Runde.
|
||||
Entscheidung: Legacy-Provider-Integration + Seat-/Plan-Engine + Verbrauchs-Metering werden technisch gebaut, die konkreten Preis-Stufen bleiben geparkt. Geerkens läuft auf manueller Sonderkondition/Trial. Self-Service-Checkout-Preise = eigene spätere Pricing-Runde.
|
||||
|
||||
---
|
||||
|
||||
@@ -231,8 +233,8 @@ Bewusst akzeptiert: längste Zeit bis zum ersten echten Nutzer-Feedback, höchst
|
||||
|
||||
source: docs/ARCHITECTURE-DECISIONS.md
|
||||
|
||||
1. Boilerplate als Code-Basis einziehen (Repo ist heute Docs-only)
|
||||
2. Naming-Drift beheben: `@toolctrl/*` → Docs anpassen, Sidebar-Konvention korrigieren
|
||||
1. Codebase lauffähig halten
|
||||
2. Naming-Drift beheben: HiveGrid-Contracts in Code und Docs, Sidebar-Konvention korrigieren
|
||||
3. EU-Sterilisation: GitHub/Google-OAuth-Reste + `User.githubId/googleId` entfernen
|
||||
4. M00 aktivieren + 4 echte Neubauten: `logActivity()`/Activity-Stream, Search-Registry, Subframe-Adapter-Code, Mass-Action-Schutz
|
||||
5. Fehlende Dependencies ergänzen: `signature_pad`, `browser-image-compression`, `react-grid-layout`, `@dnd-kit`, `libphonenumber`, `ics`/`ical-generator`, `btree_gist`/`EXCLUDE`-Constraint u.a.
|
||||
|
||||
@@ -914,8 +914,8 @@ source: _bmad-output/specs/M28-subscriptions-billing.md
|
||||
TenantSubscription, SubscriptionProduct, SubscriptionPrice, SubscriptionFeature, TenantSubscriptionUsageRecord, Credit.
|
||||
|
||||
Acceptance criteria:
|
||||
- TenantSubscription: tenant_id, stripe_customer_id, stripe_subscription_id, product_id, status (active|trialing|frozen|cancelled), trial_ends_at, current_period_start/end, seat_count
|
||||
- Stripe-Webhook-Verarbeitung: checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.deleted
|
||||
- TenantSubscription: tenant_id, legacyProvider_customer_id, legacyProvider_subscription_id, product_id, status (active|trialing|frozen|cancelled), trial_ends_at, current_period_start/end, seat_count
|
||||
- Legacy-Provider-Webhook-Verarbeitung: checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.deleted
|
||||
- Frozen-Modus: Zugriff eingeschränkt (Read-Only-Middleware), Warnungen in App
|
||||
- AI-Credits: Credit-Konto pro Tenant, verknüpft mit Subscription-Plan
|
||||
|
||||
@@ -925,7 +925,7 @@ Trial-Phase, Dunning-Workflow, Setup-Fee.
|
||||
Acceptance criteria:
|
||||
- Trial: 14 Tage Default, keine Kreditkarte erforderlich
|
||||
- Dunning: bei fehlgeschlagener Zahlung → Email-Sequenz (Tag 1, 3, 7), dann Frozen-Modus
|
||||
- Setup-Fee: einmalige Gebühr bei Enterprise-Buchung (Stripe-Checkout-Session)
|
||||
- Setup-Fee: einmalige Gebühr bei Enterprise-Buchung (Payment-Checkout-Session)
|
||||
- Preise selbst geparkt (AD-15); Engine muss aber bereit sein
|
||||
|
||||
---
|
||||
@@ -1026,7 +1026,7 @@ source: _bmad-output/specs/M33a-apps-engine.md
|
||||
DataConnection, AppSyncRun, AppSyncRunLog, AppSyncItem, Adapter-Registry.
|
||||
|
||||
Acceptance criteria:
|
||||
- DataConnection: tenant_id, app_key (gmail|outlook|aircall|kwp|stripe|pandadoc|...), status (connected|disconnected|error), credentials_encrypted (AD-8), config (JSONB), last_synced_at
|
||||
- DataConnection: tenant_id, app_key (gmail|outlook|aircall|kwp|legacyProvider|pandadoc|...), status (connected|disconnected|error), credentials_encrypted (AD-8), config (JSONB), last_synced_at
|
||||
- AppSyncRun: connection_id, status (running|completed|failed), started_at, finished_at, items_processed
|
||||
- AppSyncItem: run_id, external_id, action (create|update|delete), payload_hash, status
|
||||
- Adapter-Registry: app_key → Adapter-Klasse (type-safe Registry)
|
||||
@@ -1036,7 +1036,7 @@ App-Connect-Flow, Sync-Mechanik, Connection-Health-Monitoring.
|
||||
|
||||
Acceptance criteria:
|
||||
- OAuth2-Connect-Flow für Gmail, Outlook, AirCall, PandaDoc
|
||||
- API-Key-Connect für KWP (type=windows_client), Stripe
|
||||
- API-Key-Connect für KWP (type=windows_client), Legacy-Provider
|
||||
- Sync-Intervalle: konfiguierbar per App (Push-Webhook bevorzugt, Pull als Fallback)
|
||||
- Connection-Health: Heartbeat-Check, Auto-Reconnect-Versuch, Tenant-Admin-Alert bei Fehler
|
||||
- Settings-UI: /settings/integrationen/{app_key}; Einstiegspunkt für alle Integrations
|
||||
@@ -1087,7 +1087,7 @@ Tenant-Account-Löschung mit Crypto-Shredding.
|
||||
Acceptance criteria:
|
||||
- TenantDeletionRequest: tenant_id, requested_by_user_id, reason, scheduled_for (14-Tage-Frist)
|
||||
- Crypto-Shredding: Per-Tenant-DEK löschen → alle verschlüsselten Daten unlesbar (AD-8)
|
||||
- Stripe-Subscription canceln vor Löschung
|
||||
- Legacy-Provider-Subscription canceln vor Löschung
|
||||
- pgvector-Embeddings, Redis-Cache, Scaleway-Storage-Objekte alle entfernen
|
||||
- AvvAcceptance + Subprocessor-Liste in Privacy-Hub-Route anzeigbar
|
||||
|
||||
@@ -1211,7 +1211,8 @@ Acceptance criteria:
|
||||
- Public-Forms: app.hivegrid.de/f/{tenant}/{slug}
|
||||
- Public-Booking: app.hivegrid.de/b/{tenant}/{slug}
|
||||
- Public-Help: app.hivegrid.de/help/{lang}/{kategorie}/{slug}
|
||||
- Public-API: app.hivegrid.de/api/v1/{resource}
|
||||
- Public-API: api.hivegrid.de/v1/{resource}
|
||||
- MCP-Server: mcp.hivegrid.de
|
||||
|
||||
### REQ-integration-activity-wiring
|
||||
Jede schreibende Aktion mit Entitäts-Bezug muss logActivity() aufrufen.
|
||||
|
||||
@@ -0,0 +1,749 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HiveGrid · Pricing-Mix & Unit Economics</title>
|
||||
<style>
|
||||
:root{
|
||||
--ink:#17202f; --muted:#667085; --bg:#f5f7fa; --card:#fff;
|
||||
--line:#d9e0e8; --line2:#b8c4d2; --accent:#1f6f78; --accent2:#174f56;
|
||||
--soft:#e5f4f4; --gold:#a66f13; --good:#168247; --bad:#bd3030;
|
||||
--blue:#315da8; --violet:#7557b8; --orange:#cf6a2c; --slate:#94a3b8;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Arial,sans-serif;font-size:14px;line-height:1.45}
|
||||
header{padding:26px 30px;color:#fff;background:linear-gradient(135deg,#132a36 0%,#1f6f78 62%,#7d5d1e 100%)}
|
||||
header h1{margin:0;font-size:24px;line-height:1.15;letter-spacing:0}
|
||||
header p{max-width:1120px;margin:7px 0 0;color:rgba(255,255,255,.82);font-size:13.5px}
|
||||
.pill{display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.28);background:rgba(255,255,255,.12);color:#fff;font-size:11px;font-weight:750;white-space:nowrap}
|
||||
.wrap{display:grid;grid-template-columns:460px minmax(0,1fr);gap:20px;align-items:start;padding:22px 30px 30px}
|
||||
.wrap>div{min-width:0}
|
||||
@media(max-width:1120px){.wrap{grid-template-columns:1fr;padding:16px}}
|
||||
.card{margin-bottom:18px;padding:18px 20px;background:var(--card);border:1px solid var(--line);border-radius:10px;box-shadow:0 1px 2px rgba(16,24,40,.04)}
|
||||
@media(max-width:680px){.card{overflow-x:auto} table{min-width:560px}.mix-table{min-width:0}}
|
||||
.card h2{margin:0 0 14px;padding-bottom:7px;border-bottom:2px solid var(--accent);font-size:15px;line-height:1.25}
|
||||
.card h3{margin:18px 0 8px;color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.06em}
|
||||
.lead{padding:12px 14px;margin:0 0 18px;border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:8px;background:#fbfcfe;color:#263241}
|
||||
table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
|
||||
th,td{padding:8px 9px;border-bottom:1px solid var(--line);text-align:left;vertical-align:middle}
|
||||
th{background:#edf2f7;color:#111827;font-size:12px}
|
||||
td.n,th.n{text-align:right;white-space:nowrap}
|
||||
tr.sum td{border-top:2px solid var(--ink);background:#fbfcfe;font-weight:850}
|
||||
.mix-table input[type=number]{width:76px;padding:5px 7px;border:1px solid var(--line2);border-radius:6px;background:#fff;color:var(--accent2);font-weight:750;text-align:right;font-variant-numeric:tabular-nums}
|
||||
.mix-table td:first-child{font-weight:800}
|
||||
.hint{color:var(--muted);font-size:12px}
|
||||
label.row{display:block;margin:14px 0}
|
||||
label.row .lab{display:flex;justify-content:space-between;gap:12px;margin-bottom:6px;font-weight:700}
|
||||
label.row .val{color:var(--accent);font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
input[type=range]{width:100%;accent-color:var(--accent)}
|
||||
select{width:100%;padding:9px 10px;border:1px solid var(--line2);border-radius:8px;background:#fff;color:var(--ink);font-weight:700}
|
||||
.note{margin-top:9px;color:var(--muted);font-size:12px}
|
||||
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:18px}
|
||||
@media(max-width:1240px){.kpis{grid-template-columns:repeat(2,1fr)}}
|
||||
@media(max-width:680px){.kpis{grid-template-columns:1fr}}
|
||||
.kpi{min-height:104px;padding:14px 16px;border:1px solid var(--line);border-radius:10px;background:var(--card)}
|
||||
.kpi .k{color:var(--muted);font-size:12px;font-weight:750}
|
||||
.kpi .v{margin-top:5px;font-size:25px;line-height:1.1;font-weight:850;font-variant-numeric:tabular-nums}
|
||||
.kpi .s{margin-top:4px;color:var(--muted);font-size:11.5px}
|
||||
.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
||||
@media(max-width:760px){.grid2{grid-template-columns:1fr}}
|
||||
.bar{display:flex;height:28px;margin:7px 0 5px;overflow:hidden;border:1px solid var(--line);border-radius:7px;background:#fff}
|
||||
.bar span{display:flex;align-items:center;justify-content:center;min-width:0;height:100%;overflow:hidden;color:#fff;font-size:11px;font-weight:800;white-space:nowrap}
|
||||
.legend{display:flex;flex-wrap:wrap;gap:10px 15px;margin-top:9px;font-size:12px}
|
||||
.legend i{display:inline-block;width:11px;height:11px;margin-right:5px;border-radius:3px;vertical-align:middle}
|
||||
.pos{color:var(--good)!important;font-weight:850}.neg{color:var(--bad)!important;font-weight:850}.warn{color:var(--gold)!important;font-weight:850}
|
||||
.src{margin-top:8px;color:var(--muted);font-size:11.5px;line-height:1.65}.src a{color:var(--accent)}
|
||||
details{margin-top:8px} summary{cursor:pointer;color:var(--accent);font-weight:800}
|
||||
.payment-row{display:grid;grid-template-columns:88px 1fr 58px;gap:10px;align-items:center;margin:12px 0}
|
||||
.payment-row strong{font-size:13px}
|
||||
.payment-row output{font-weight:850;color:var(--accent);text-align:right;font-variant-numeric:tabular-nums}
|
||||
.compact-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
.compact-grid label{display:block;color:var(--muted);font-size:12px;font-weight:750}
|
||||
.compact-grid input,.compact-grid select{margin-top:5px}
|
||||
.compact-grid input[type=number]{width:100%;padding:8px 10px;border:1px solid var(--line2);border-radius:8px;background:#fff;color:var(--ink);font-weight:750;font-variant-numeric:tabular-nums}
|
||||
@media(max-width:520px){.compact-grid{grid-template-columns:1fr}}
|
||||
.mode-panel{padding:12px;margin-top:10px;border:1px solid var(--line);border-radius:8px;background:#fbfcfe}
|
||||
.mode-panel h3{margin-top:0}
|
||||
.inline-result{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}
|
||||
.inline-result div{padding:9px 10px;border:1px solid var(--line);border-radius:8px;background:#fff}
|
||||
.inline-result span{display:block;color:var(--muted);font-size:11px;font-weight:750}
|
||||
.inline-result b{display:block;margin-top:2px;font-size:15px;font-variant-numeric:tabular-nums}
|
||||
@media(max-width:520px){.inline-result{grid-template-columns:1fr}}
|
||||
.toggle{display:flex;align-items:center;gap:10px;margin-top:6px;cursor:pointer;user-select:none}
|
||||
.toggle input{position:relative;width:38px;height:22px;appearance:none;border-radius:20px;background:var(--line2);cursor:pointer;transition:.15s}
|
||||
.toggle input:checked{background:var(--good)}.toggle input:before{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;transition:.15s}.toggle input:checked:before{left:18px}
|
||||
.seg{display:grid;grid-template-columns:repeat(3,1fr);overflow:hidden;border:1px solid var(--line2);border-radius:8px;background:#fff}
|
||||
.seg button{min-height:38px;padding:8px 5px;border:0;border-right:1px solid var(--line);background:#fff;color:var(--muted);cursor:pointer;font-size:12px;font-weight:800}
|
||||
.seg button:last-child{border-right:0}.seg button.on{background:var(--accent);color:#fff}.seg button:hover:not(.on){background:var(--soft);color:var(--accent)}
|
||||
.role-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px}
|
||||
.role-head h3{margin:0;color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.06em}
|
||||
.salary-mode{width:auto;min-width:170px;padding:6px 8px;font-size:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>HiveGrid · Pricing-Mix & Unit Economics <span class="pill">Stand 07.07.2026</span></h1>
|
||||
<p>Rechner für die betriebswirtschaftliche Planung: wie viele Starter-, Team- und Business-Betriebe, wie viele Extra-User je Paket, Add-on-Annahmen und Zahlungs-Mix über Mollie.</p>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div>
|
||||
<div class="card">
|
||||
<h2>Kundenmix</h2>
|
||||
<h3>Modus</h3>
|
||||
<select id="scaleMode">
|
||||
<option value="manual">Kundenmix manuell eingeben</option>
|
||||
<option value="target" selected>Aus Nutzerziel ableiten</option>
|
||||
</select>
|
||||
<div class="mode-panel" id="targetMixBox">
|
||||
<h3>Nutzerziel</h3>
|
||||
<div class="compact-grid">
|
||||
<label>Nutzer gesamt
|
||||
<input id="scaleUsers" type="number" min="100" max="500000" step="100" value="153000">
|
||||
</label>
|
||||
<label>Ø Nutzer je Betrieb
|
||||
<input id="scaleAvgUsers" type="number" min="1" max="250" step="1" value="20">
|
||||
</label>
|
||||
<label>Abgeleitetes Paket
|
||||
<select id="scalePlan">
|
||||
<option value="team" selected>Team · 20 Nutzer inkl.</option>
|
||||
<option value="starter">Starter · 7 Nutzer inkl.</option>
|
||||
<option value="business">Business · 40 Nutzer inkl.</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inline-result" id="targetPreview"></div>
|
||||
</div>
|
||||
<div class="note" id="scaleModeNote">Im Zielmodus errechnet der Rechner Betriebe/Teams und Extra-User automatisch aus Nutzerziel, Durchschnittsgröße und Paket.</div>
|
||||
|
||||
<div class="mode-panel" id="manualMixBox">
|
||||
<h3>Manueller Mix</h3>
|
||||
<table class="mix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Paket</th>
|
||||
<th class="n">Betriebe</th>
|
||||
<th class="n">Ø extra Büro</th>
|
||||
<th class="n">Ø extra Baustelle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Starter<br><span class="hint">2 Büro + 5 Baustelle inkl.</span></td>
|
||||
<td class="n"><input id="starterCount" type="number" min="0" step="1" value="25"></td>
|
||||
<td class="n"><input id="starterExtraOffice" type="number" min="0" step="0.1" value="0.5"></td>
|
||||
<td class="n"><input id="starterExtraSite" type="number" min="0" step="0.1" value="2"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Team<br><span class="hint">5 Büro + 15 Baustelle inkl.</span></td>
|
||||
<td class="n"><input id="teamCount" type="number" min="0" step="1" value="12"></td>
|
||||
<td class="n"><input id="teamExtraOffice" type="number" min="0" step="0.1" value="1"></td>
|
||||
<td class="n"><input id="teamExtraSite" type="number" min="0" step="0.1" value="4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Business<br><span class="hint">10 Büro + 30 Baustelle inkl.</span></td>
|
||||
<td class="n"><input id="businessCount" type="number" min="0" step="1" value="3"></td>
|
||||
<td class="n"><input id="businessExtraOffice" type="number" min="0" step="0.1" value="2"></td>
|
||||
<td class="n"><input id="businessExtraSite" type="number" min="0" step="0.1" value="8"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="note">Extra-User sind Durchschnittswerte pro Betrieb. 0 bedeutet: Paket wird ohne zusätzliche Nutzer verkauft.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Zahlungs-Mix · Mollie</h2>
|
||||
<div class="payment-row">
|
||||
<strong>SEPA</strong>
|
||||
<input id="paySepa" type="range" min="0" max="100" step="1" value="70">
|
||||
<output id="o_paySepa">70 %</output>
|
||||
</div>
|
||||
<div class="payment-row">
|
||||
<strong>Visa/MC</strong>
|
||||
<input id="payVisa" type="range" min="0" max="100" step="1" value="25">
|
||||
<output id="o_payVisa">25 %</output>
|
||||
</div>
|
||||
<div class="payment-row">
|
||||
<strong>Amex</strong>
|
||||
<input id="payAmex" type="range" min="0" max="100" step="1" value="5">
|
||||
<output id="o_payAmex">5 %</output>
|
||||
</div>
|
||||
<div class="note" id="paymentNote"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Add-ons</h2>
|
||||
<h3>Email Marketing</h3>
|
||||
<label class="row"><span class="lab">Anteil Betriebe mit Email Marketing <span class="val" id="o_emailAttach"></span></span>
|
||||
<input type="range" id="emailAttach" min="0" max="100" step="5" value="35"></label>
|
||||
<select id="emailTier">
|
||||
<option value="s">S · bis 10.000 Mails · 59 €/Monat</option>
|
||||
<option value="m" selected>M · 10.001-50.000 Mails · 149 €/Monat</option>
|
||||
<option value="l">L · 50.001-100.000 Mails · 249 €/Monat</option>
|
||||
<option value="xl">XL · 100.001-250.000 Mails · 499 €/Monat</option>
|
||||
<option value="xxl">XXL · 250.001-500.000 Mails · 899 €/Monat</option>
|
||||
</select>
|
||||
|
||||
<h3>Cloud-Speicher</h3>
|
||||
<label class="row"><span class="lab">Anteil Betriebe mit Speicher-Add-on <span class="val" id="o_storageAttach"></span></span>
|
||||
<input type="range" id="storageAttach" min="0" max="100" step="5" value="20"></label>
|
||||
<select id="storageAddon">
|
||||
<option value="100">+100 GB · 5 €/Monat</option>
|
||||
<option value="500" selected>+500 GB · 15 €/Monat</option>
|
||||
<option value="1000">+1 TB · 25 €/Monat</option>
|
||||
<option value="5000">+5 TB · 99 €/Monat</option>
|
||||
<option value="10000">+10 TB · 179 €/Monat</option>
|
||||
</select>
|
||||
<label class="row"><span class="lab">Ø Speichernutzung vom Pool <span class="val" id="o_storageUse"></span></span>
|
||||
<input type="range" id="storageUse" min="10" max="100" step="5" value="60"></label>
|
||||
<label class="row"><span class="lab">Egress je TB gespeichert <span class="val" id="o_egress"></span></span>
|
||||
<input type="range" id="egressFactor" min="0" max="2" step="0.1" value="0.3"></label>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Infrastruktur</h2>
|
||||
<h3>Dynamisches Kapazitätsmodell</h3>
|
||||
<div class="note">Rechnet aus den aktuell modellierten Nutzern automatisch die benötigte Kapazität. Die konkrete Serverliste steht rechts im Infra-Szenario.</div>
|
||||
<label class="row"><span class="lab">Kapazitäts-Puffer <span class="val" id="o_scaleBuffer"></span></span>
|
||||
<input id="scaleBuffer" type="range" min="10" max="80" step="5" value="30"></label>
|
||||
<label class="row"><span class="lab">Mindest-Estate-/Ops-Anteil <span class="val" id="o_estateShare"></span></span>
|
||||
<input type="range" id="estateShare" min="0" max="250" step="5" value="25"></label>
|
||||
<div class="note" id="infraNote"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Personal</h2>
|
||||
<label class="toggle"><input type="checkbox" id="persOn"> <span>Personal in GuV einrechnen</span></label>
|
||||
<div id="persBox" style="display:none">
|
||||
<label class="row"><span class="lab">Arbeitgeberaufschlag auf Brutto <span class="val" id="o_employerRate"></span></span>
|
||||
<input type="range" id="employerRate" min="19" max="25" step="0.5" value="22"></label>
|
||||
|
||||
<div class="role-head"><h3>Entwicklung</h3><select id="devMode" class="salary-mode"><option value="gross" selected>Brutto angeben</option><option value="net">Netto-Ziel angeben</option><option value="cost">AG-Kosten direkt</option></select></div>
|
||||
<label class="row"><span class="lab">Entwickler <span class="val" id="o_dev"></span></span>
|
||||
<input type="range" id="dev" min="0" max="8" step="1" value="2"></label>
|
||||
<label class="row"><span class="lab">Gehalt je Entwickler <span class="val" id="o_devpay"></span></span>
|
||||
<input type="range" id="devpay" min="3000" max="10000" step="250" value="7500"></label>
|
||||
<div class="note" id="devCostNote"></div>
|
||||
|
||||
<div class="role-head"><h3>Vertrieb</h3><select id="salesMode" class="salary-mode"><option value="gross" selected>Brutto angeben</option><option value="net">Netto-Ziel angeben</option><option value="cost">AG-Kosten direkt</option></select></div>
|
||||
<label class="row"><span class="lab">Vertrieb <span class="val" id="o_sales"></span></span>
|
||||
<input type="range" id="sales" min="0" max="6" step="1" value="1"></label>
|
||||
<label class="row"><span class="lab">Gehalt je Vertrieb <span class="val" id="o_salespay"></span></span>
|
||||
<input type="range" id="salespay" min="2500" max="8000" step="250" value="5000"></label>
|
||||
<div class="note" id="salesCostNote"></div>
|
||||
|
||||
<div class="role-head"><h3>Support</h3><select id="suppMode" class="salary-mode"><option value="gross" selected>Brutto angeben</option><option value="net">Netto-Ziel angeben</option><option value="cost">AG-Kosten direkt</option></select></div>
|
||||
<label class="row"><span class="lab">Support <span class="val" id="o_supp"></span></span>
|
||||
<input type="range" id="supp" min="0" max="6" step="1" value="1"></label>
|
||||
<label class="row"><span class="lab">Gehalt je Support <span class="val" id="o_supppay"></span></span>
|
||||
<input type="range" id="supppay" min="2500" max="6000" step="250" value="4000"></label>
|
||||
<div class="note" id="suppCostNote"></div>
|
||||
<div class="note">Netto-Ziel ist eine Planungs-Schätzung für Deutschland 2026, Steuerklasse I, gesetzlich versichert, ohne Kirchensteuer. Für echte Payroll bitte Lohnabrechnung/Steuerberater verwenden.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="lead" id="summaryLead"></div>
|
||||
|
||||
<div class="kpis">
|
||||
<div class="kpi"><div class="k">MRR</div><div class="v" id="kpi_mrr"></div><div class="s" id="kpi_arr"></div></div>
|
||||
<div class="kpi"><div class="k">Betriebe / Nutzer</div><div class="v" id="kpi_tenants"></div><div class="s" id="kpi_users"></div></div>
|
||||
<div class="kpi"><div class="k">Betriebskosten</div><div class="v" id="kpi_opex"></div><div class="s" id="kpi_opexpct"></div></div>
|
||||
<div class="kpi"><div class="k" id="kpi_marginLabel">Rohmarge</div><div class="v" id="kpi_margin"></div><div class="s" id="kpi_marginpct"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Kundenmix & Core-Umsatz</h2>
|
||||
<table>
|
||||
<thead><tr><th>Paket</th><th class="n">Betriebe</th><th class="n">Nutzer gesamt</th><th class="n">Extra-User</th><th class="n">Core-MRR</th><th class="n">Ø/Betrieb</th></tr></thead>
|
||||
<tbody id="planRows"></tbody>
|
||||
</table>
|
||||
<div class="note">Diese Tabelle zeigt den Paket- und Zusatznutzerumsatz. Email-Marketing und Speicher-Add-ons werden darunter separat ausgewiesen und sind im Gesamt-MRR enthalten.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Zahlungs-Mix & Mollie-Gebühren</h2>
|
||||
<table>
|
||||
<thead><tr><th>Zahlungsart</th><th class="n">Anteil</th><th class="n">Umsatzbasis</th><th class="n">Mollie-Gebühr</th><th class="n">Kosten / Monat</th></tr></thead>
|
||||
<tbody id="paymentRows"></tbody>
|
||||
</table>
|
||||
<div class="note">Modellannahme: SEPA-Anteil nutzt den Jahres-SEPA-Preis und wird als eine jährliche SEPA-Lastschrift modelliert. Visa/MC und Amex nutzen den monatlichen Listenpreis.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Wohin geht 1 € Umsatz?</h2>
|
||||
<div class="bar" id="splitbar"></div>
|
||||
<div class="legend" id="splitlegend"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Betriebskosten im Detail</h2>
|
||||
<table>
|
||||
<thead><tr><th>Posten</th><th class="n">€ / Monat</th><th class="n">% Umsatz</th><th class="n">Cent je €</th></tr></thead>
|
||||
<tbody id="opexRows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Infra-Szenario</h2>
|
||||
<table>
|
||||
<thead><tr><th>Komponente</th><th>Rolle</th><th class="n">€ / Monat</th></tr></thead>
|
||||
<tbody id="infraRows"></tbody>
|
||||
</table>
|
||||
<div class="note" id="infraScaleNote"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<h2>Add-on-Ergebnis</h2>
|
||||
<table><tbody id="addonRows"></tbody></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Speicher-Modell</h2>
|
||||
<table><tbody id="storageRows"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="persCard" style="display:none">
|
||||
<h2>Mit Personal · GuV & Break-even</h2>
|
||||
<table>
|
||||
<thead><tr><th>Block</th><th class="n">€ / Monat</th><th class="n">% Umsatz</th></tr></thead>
|
||||
<tbody id="persRows"></tbody>
|
||||
</table>
|
||||
<div class="note" id="breakeven"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Preislogik & Quellen</h2>
|
||||
<details open>
|
||||
<summary>Pricing-Basis</summary>
|
||||
<div class="src">
|
||||
Starter 199 €/Monat bzw. 179 € Jahres-SEPA, inkl. 2 Büro + 5 Baustelle, Zusatznutzer 59 €/25 €.
|
||||
Team 499 €/Monat bzw. 449 € Jahres-SEPA, inkl. 5 Büro + 15 Baustelle, Zusatznutzer 39 €/19 €.
|
||||
Business 999 €/Monat bzw. 899 € Jahres-SEPA, inkl. 10 Büro + 30 Baustelle, Zusatznutzer 35 €/15 €.
|
||||
Bei Jahres-SEPA werden Zusatznutzer mit 10 % Vorteil modelliert.
|
||||
</div>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>Mollie-Gebühren</summary>
|
||||
<div class="src">
|
||||
Mollie Pricing, geprüft am 07.07.2026: Visa/Mastercard EEA Consumer Cards 1,80 % + 0,25 €, American Express 2,90 % + 0,25 €, SEPA Direct Debit 0,35 € je erfolgreicher Transaktion. Commercial Cards können höher liegen. Quelle:
|
||||
<a href="https://www.mollie.com/pricing">mollie.com/pricing</a>.
|
||||
</div>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>Infra- und Personalkosten</summary>
|
||||
<div class="src">
|
||||
HiveGrid Prod läuft aktuell in einem separaten Hetzner-Account mit 5 VMs: control/web/api/db jeweils CX33 und worker+services CX43, plus 50 GB DB-Volume. Der Rechner nutzt jetzt ein dynamisches Kapazitätsmodell statt fixer Stufen: Web-Knoten ca. 10.000 Kapazitätsnutzer je CX43, API ca. 12.000 je CX43, Dedicated Worker ca. 25.000 je AX42, Service-Knoten ca. 40.000 je CX53, Redis ca. 50.000 je CPX42 und DB-Shards ca. 50.000 je Shard mit Primary+Standby. Der eingestellte Kapazitäts-Puffer wird auf die Zielnutzer aufgeschlagen. Hetzner-API-Preise für fsn1, geprüft am 07.07.2026: CX33 8,49 €, CX43 15,99 €, CX53 29,49 €, CPX42 69,49 €, CPX52 100,49 €, CPX62 129,99 €, LB11 7,49 €, LB21 21,49 €, LB31 42,99 €, DB-Volume 0,0572 €/GB/Monat. AX42 wird konservativ mit 97,30 €, AX102 mit 189 € als Planwert angesetzt; Dedicated-Angebote schwanken und müssen vor Kauf gegengeprüft werden.
|
||||
Hetzner Object Storage wird mit 6,49 € Basispreis, ca. 1 TB Storage und 1 TB Egress inklusive, zusätzlichem Storage ca. 6,49 €/TB/Monat und zusätzlichem Egress 1 €/TB modelliert. Object Storage bleibt separat vom Compute-Szenario, weil der Verbrauch durch verkaufte Speicherpakete entsteht.
|
||||
Personal: Arbeitgeberanteil ist als Zuschlag auf Brutto parametrisierbar, Default 22 %. Das orientiert sich an deutschen Arbeitgeber-Sozialabgaben von grob 21-23 % auf das Bruttogehalt. Netto-Umrechnung ist eine grobe Planungsrechnung, keine Payroll.
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PLANS = {
|
||||
starter: { label:'Starter', monthly:199, sepa:179, incOffice:2, incSite:5, extraOffice:59, extraSite:25, storage:100 },
|
||||
team: { label:'Team', monthly:499, sepa:449, incOffice:5, incSite:15, extraOffice:39, extraSite:19, storage:250 },
|
||||
business: { label:'Business', monthly:999, sepa:899, incOffice:10, incSite:30, extraOffice:35, extraSite:15, storage:500 }
|
||||
};
|
||||
const EMAIL = {
|
||||
s:{label:'Email Marketing S', volume:'bis 10.000', price:59, cost:25},
|
||||
m:{label:'Email Marketing M', volume:'10.001-50.000', price:149, cost:50},
|
||||
l:{label:'Email Marketing L', volume:'50.001-100.000', price:249, cost:95},
|
||||
xl:{label:'Email Marketing XL', volume:'100.001-250.000', price:499, cost:230},
|
||||
xxl:{label:'Email Marketing XXL', volume:'250.001-500.000', price:899, cost:430}
|
||||
};
|
||||
const STORAGE = {
|
||||
100:{label:'+100 GB', gb:100, price:5},
|
||||
500:{label:'+500 GB', gb:500, price:15},
|
||||
1000:{label:'+1 TB', gb:1000, price:25},
|
||||
5000:{label:'+5 TB', gb:5000, price:99},
|
||||
10000:{label:'+10 TB', gb:10000, price:179}
|
||||
};
|
||||
const PRICE = {
|
||||
cx33:8.49, cx43:15.99, cx53:29.49,
|
||||
cpx22:19.49, cpx42:69.49, cpx52:100.49, cpx62:129.99,
|
||||
lb11:7.49, lb21:21.49, lb31:42.99,
|
||||
volumeGB:0.0572,
|
||||
ax42:97.30, ax102:189
|
||||
};
|
||||
const CAPACITY = {
|
||||
webCx43Users:10000,
|
||||
apiCx43Users:12000,
|
||||
workerAx42Users:25000,
|
||||
workerBurstCx43Users:50000,
|
||||
serviceCx53Users:40000,
|
||||
redisCpx42Users:50000,
|
||||
dbShardUsers:50000,
|
||||
dbGbPerUser:0.03
|
||||
};
|
||||
const COL = {infra:'#168247', storage:'#a66f13', mailjet:'#7557b8', payment:'#cf6a2c', misc:'#94a3b8', dev:'#bd3030', sales:'#0f7b93', support:'#315da8', margin:'#168247'};
|
||||
const $ = id => document.getElementById(id);
|
||||
const eur = n => (n).toLocaleString('de-DE',{style:'currency',currency:'EUR',minimumFractionDigits:n<100&&n!==Math.round(n)?2:0,maximumFractionDigits:2});
|
||||
const eur0 = n => Math.round(n).toLocaleString('de-DE',{style:'currency',currency:'EUR',maximumFractionDigits:0});
|
||||
const pct = n => Number.isFinite(n) ? (n*100).toLocaleString('de-DE',{maximumFractionDigits:1})+' %' : '0 %';
|
||||
const num = n => Math.round(n).toLocaleString('de-DE');
|
||||
const gb = n => n>=1000 ? (n/1000).toLocaleString('de-DE',{maximumFractionDigits:1})+' TB' : Math.round(n).toLocaleString('de-DE')+' GB';
|
||||
let lastPay = 'paySepa';
|
||||
|
||||
function val(id){ return +$(id).value || 0; }
|
||||
function sepaExtra(v){ return v * 0.9; }
|
||||
function planRevenueMonthly(p, eo, es){ return p.monthly + eo*p.extraOffice + es*p.extraSite; }
|
||||
function planRevenueSepa(p, eo, es){ return p.sepa + eo*sepaExtra(p.extraOffice) + es*sepaExtra(p.extraSite); }
|
||||
function ceilDiv(a,b){ return Math.ceil(a/b); }
|
||||
function infraBreakdown(users){
|
||||
const buffer = val('scaleBuffer') / 100;
|
||||
const capacityUsers = Math.ceil(users * (1 + buffer));
|
||||
const activeSessions = Math.ceil(capacityUsers * 0.08);
|
||||
const components = [];
|
||||
|
||||
components.push(['Control · CX33', 'Coolify, Bastion, Traefik', PRICE.cx33]);
|
||||
|
||||
const webNodes = users <= 1000 ? 1 : Math.max(2, ceilDiv(capacityUsers, CAPACITY.webCx43Users));
|
||||
const apiNodes = users <= 1000 ? 1 : Math.max(2, ceilDiv(capacityUsers, CAPACITY.apiCx43Users));
|
||||
components.push([`Web · ${webNodes}x CX43`, `${num(CAPACITY.webCx43Users)} Nutzer Kapazität je Knoten`, webNodes * PRICE.cx43]);
|
||||
components.push([`API · ${apiNodes}x CX43`, `${num(CAPACITY.apiCx43Users)} Nutzer Kapazität je Knoten`, apiNodes * PRICE.cx43]);
|
||||
|
||||
const serviceNodes = Math.max(1, ceilDiv(capacityUsers, CAPACITY.serviceCx53Users));
|
||||
components.push([`Search/AV/Captcha · ${serviceNodes}x CX53`, 'Meilisearch, ClamAV, mCaptcha getrennt von Worker-Last', serviceNodes * PRICE.cx53]);
|
||||
|
||||
let redisNodes = 0;
|
||||
if(capacityUsers > 1000) redisNodes = capacityUsers <= 20000 ? 1 : Math.max(3, ceilDiv(capacityUsers, CAPACITY.redisCpx42Users));
|
||||
if(redisNodes > 0) components.push([`Redis · ${redisNodes}x CPX42`, 'Queues, Sessions, Cache; ab Scale als Cluster/Sentinel gedacht', redisNodes * PRICE.cpx42]);
|
||||
|
||||
if(capacityUsers <= 1000){
|
||||
components.push(['DB · CPX42', 'Postgres, PgBouncer und Redis gemeinsam', PRICE.cpx42]);
|
||||
}else if(capacityUsers <= 3000){
|
||||
components.push(['DB · CPX62', 'Postgres + PgBouncer, Redis getrennt bei Bedarf', PRICE.cpx62]);
|
||||
}else{
|
||||
const dbShards = ceilDiv(capacityUsers, CAPACITY.dbShardUsers);
|
||||
const dbNodes = dbShards * 2;
|
||||
components.push([`DB · ${dbShards} Shards / ${dbNodes}x AX102`, 'je Shard Primary + Standby, Tenant-Sharding ab Scale', dbNodes * PRICE.ax102]);
|
||||
}
|
||||
|
||||
const workerDedicated = capacityUsers <= 1000 ? 0 : Math.max(1, ceilDiv(capacityUsers, CAPACITY.workerAx42Users));
|
||||
if(workerDedicated === 0) components.push(['Worker Light · CX43', 'Queues und Hintergrundjobs auf kleiner Stufe', PRICE.cx43]);
|
||||
if(workerDedicated > 0) components.push([`Worker · ${workerDedicated}x AX42`, `${num(CAPACITY.workerAx42Users)} Nutzer Kapazität je Worker-Server`, workerDedicated * PRICE.ax42]);
|
||||
const workerBurst = users > 3000 ? ceilDiv(capacityUsers, CAPACITY.workerBurstCx43Users) : 0;
|
||||
if(workerBurst > 0) components.push([`Worker Burst · ${workerBurst}x CX43`, 'Import/Webhook/Plugin-Spitzen getrennt von Haupt-Workern', workerBurst * PRICE.cx43]);
|
||||
|
||||
const lbTargets = webNodes + apiNodes;
|
||||
const lb = activeSessions > 20000 || lbTargets > 75 ? ['Load Balancer · LB31', `${num(activeSessions)} geplante aktive Sessions, ${lbTargets} Targets`, PRICE.lb31]
|
||||
: activeSessions > 10000 || lbTargets > 25 ? ['Load Balancer · LB21', `${num(activeSessions)} geplante aktive Sessions, ${lbTargets} Targets`, PRICE.lb21]
|
||||
: users > 500 ? ['Load Balancer · LB11', `${num(activeSessions)} geplante aktive Sessions, ${lbTargets} Targets`, PRICE.lb11]
|
||||
: null;
|
||||
if(lb) components.push(lb);
|
||||
|
||||
const dbVolumeGb = Math.max(100, Math.ceil(capacityUsers * CAPACITY.dbGbPerUser / 100) * 100);
|
||||
components.push([`DB-Backup/Volume-Reserve · ${gb(dbVolumeGb)}`, `${Math.round(CAPACITY.dbGbPerUser * 1000)} MB DB-/Index-Reserve je Kapazitätsnutzer`, dbVolumeGb * PRICE.volumeGB]);
|
||||
|
||||
const minEstate = Math.ceil(capacityUsers / 10000) * 10;
|
||||
const estate = Math.max(val('estateShare'), minEstate);
|
||||
if(estate > 0) components.push(['Estate-/Ops-Anteil', 'anteilige Sentry/Grafana/Gitea/Monitoring-/Admin-Overheads', estate]);
|
||||
|
||||
const total = components.reduce((sum, component) => sum + component[2], 0);
|
||||
return {
|
||||
label: `${num(users)} Nutzerziel`,
|
||||
cap: `${num(capacityUsers)} Nutzer Kapazität inkl. ${Math.round(buffer*100)} % Puffer`,
|
||||
note: `Kapazitätsmodell: ${num(users)} Zielnutzer werden mit ${num(capacityUsers)} Kapazitätsnutzern geplant. Daraus folgen ${webNodes} Web-, ${apiNodes} API-, ${workerDedicated} Dedicated-Worker-Knoten, ${serviceNodes} Service-Knoten und ${activeSessions} geplante aktive Sessions.`,
|
||||
components,
|
||||
total,
|
||||
capacityUsers,
|
||||
activeSessions
|
||||
};
|
||||
}
|
||||
function objectStorageCost(actualGB, egressFactor){
|
||||
if(actualGB<=0) return 0;
|
||||
const storageTB = actualGB/1000;
|
||||
const egressTB = storageTB*egressFactor;
|
||||
return 6.49 + Math.max(0, storageTB-1)*6.49 + Math.max(0, egressTB-1)*1;
|
||||
}
|
||||
|
||||
function annualIncomeTaxApprox(taxable){
|
||||
if(taxable <= 12348) return 0;
|
||||
if(taxable <= 17799){
|
||||
const x = taxable - 12348;
|
||||
const avgRate = 0.14 + (0.24 - 0.14) * (x / (17799 - 12348)) / 2;
|
||||
return x * avgRate;
|
||||
}
|
||||
if(taxable <= 69879){
|
||||
const base = annualIncomeTaxApprox(17799);
|
||||
const x = taxable - 17799;
|
||||
const avgRate = 0.24 + (0.42 - 0.24) * (x / (69879 - 17799)) / 2;
|
||||
return base + x * avgRate;
|
||||
}
|
||||
if(taxable <= 277826) return annualIncomeTaxApprox(69879) + (taxable - 69879) * 0.42;
|
||||
return annualIncomeTaxApprox(277826) + (taxable - 277826) * 0.45;
|
||||
}
|
||||
function employeeSocialAnnual(grossMonthly){
|
||||
const annual = grossMonthly * 12;
|
||||
const healthBase = Math.min(annual, 69750);
|
||||
const pensionBase = Math.min(annual, 96600);
|
||||
return healthBase * (0.073 + 0.0145 + 0.018) + pensionBase * (0.093 + 0.013);
|
||||
}
|
||||
function netFromGross(grossMonthly){
|
||||
const annual = grossMonthly * 12;
|
||||
const social = employeeSocialAnnual(grossMonthly);
|
||||
const taxable = Math.max(0, annual - social - 1230);
|
||||
const tax = annualIncomeTaxApprox(taxable);
|
||||
return (annual - social - tax) / 12;
|
||||
}
|
||||
function grossFromNet(targetNet){
|
||||
let lo = targetNet, hi = targetNet * 3 + 2000;
|
||||
for(let i=0;i<40;i++){
|
||||
const mid = (lo + hi) / 2;
|
||||
if(netFromGross(mid) < targetNet) lo = mid; else hi = mid;
|
||||
}
|
||||
return hi;
|
||||
}
|
||||
function salaryUnitCost(mode, amount, employerRate){
|
||||
if(mode === 'cost') return { gross: amount / (1 + employerRate), net: netFromGross(amount / (1 + employerRate)), cost: amount };
|
||||
const gross = mode === 'net' ? grossFromNet(amount) : amount;
|
||||
return { gross, net: netFromGross(gross), cost: gross * (1 + employerRate) };
|
||||
}
|
||||
function roleCost(role, countId, payId, modeId, noteId, employerRate){
|
||||
const count = val(countId);
|
||||
const amount = val(payId);
|
||||
const mode = $(modeId).value;
|
||||
const unit = salaryUnitCost(mode, amount, employerRate);
|
||||
$(noteId).textContent = `${mode === 'net' ? 'Netto-Ziel' : mode === 'gross' ? 'Brutto' : 'AG-Kosten'} ${eur(amount)} → ca. ${eur(unit.gross)} brutto, ${eur(unit.net)} netto, ${eur(unit.cost)} Arbeitgeberkosten je ${role}.`;
|
||||
return { count, unit, total: count * unit.cost };
|
||||
}
|
||||
|
||||
function syncPayment(changed){
|
||||
const ids = ['paySepa','payVisa','payAmex'];
|
||||
let changedVal = Math.max(0, Math.min(100, val(changed)));
|
||||
$(changed).value = changedVal;
|
||||
const others = ids.filter(id => id !== changed);
|
||||
const remaining = 100 - changedVal;
|
||||
const otherSum = others.reduce((s,id)=>s+val(id),0);
|
||||
if(otherSum <= 0){
|
||||
others.forEach(id => $(id).value = remaining / 2);
|
||||
}else{
|
||||
others.forEach(id => $(id).value = val(id) / otherSum * remaining);
|
||||
}
|
||||
}
|
||||
|
||||
function paymentMix(){
|
||||
const total = val('paySepa') + val('payVisa') + val('payAmex') || 1;
|
||||
return { sepa: val('paySepa')/total, visa: val('payVisa')/total, amex: val('payAmex')/total };
|
||||
}
|
||||
|
||||
function planInputsFromMode(){
|
||||
if($('scaleMode').value !== 'target'){
|
||||
return [
|
||||
['starter', val('starterCount'), val('starterExtraOffice'), val('starterExtraSite')],
|
||||
['team', val('teamCount'), val('teamExtraOffice'), val('teamExtraSite')],
|
||||
['business', val('businessCount'), val('businessExtraOffice'), val('businessExtraSite')]
|
||||
];
|
||||
}
|
||||
|
||||
const targetUsers = Math.max(1, val('scaleUsers'));
|
||||
const avgUsersInput = Math.max(1, val('scaleAvgUsers'));
|
||||
const planKey = $('scalePlan').value;
|
||||
const p = PLANS[planKey];
|
||||
const includedUsers = p.incOffice + p.incSite;
|
||||
const avgUsers = Math.max(avgUsersInput, includedUsers);
|
||||
const tenants = Math.ceil(targetUsers / avgUsers);
|
||||
const extraUsers = Math.max(0, avgUsers - includedUsers);
|
||||
const extraOffice = +(extraUsers * 0.25).toFixed(2);
|
||||
const extraSite = +(extraUsers - extraOffice).toFixed(2);
|
||||
const inputs = [['starter',0,0,0], ['team',0,0,0], ['business',0,0,0]];
|
||||
const idx = planKey === 'starter' ? 0 : planKey === 'team' ? 1 : 2;
|
||||
inputs[idx] = [planKey, tenants, extraOffice, extraSite];
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function compute(){
|
||||
const mix = paymentMix();
|
||||
$('o_paySepa').textContent = pct(mix.sepa);
|
||||
$('o_payVisa').textContent = pct(mix.visa);
|
||||
$('o_payAmex').textContent = pct(mix.amex);
|
||||
$('o_emailAttach').textContent = $('emailAttach').value+' %';
|
||||
$('o_storageAttach').textContent = $('storageAttach').value+' %';
|
||||
$('o_storageUse').textContent = $('storageUse').value+' %';
|
||||
$('o_egress').textContent = (+$('egressFactor').value).toLocaleString('de-DE',{maximumFractionDigits:1})+' TB/TB';
|
||||
$('o_scaleBuffer').textContent = $('scaleBuffer').value+' %';
|
||||
$('o_estateShare').textContent = eur0(val('estateShare'));
|
||||
$('o_employerRate').textContent = $('employerRate').value+' %';
|
||||
|
||||
const targetMode = $('scaleMode').value === 'target';
|
||||
$('targetMixBox').style.display = targetMode ? 'block' : 'none';
|
||||
$('manualMixBox').style.display = targetMode ? 'none' : 'block';
|
||||
const targetUsers = Math.max(1, val('scaleUsers'));
|
||||
const avgUsersInput = Math.max(1, val('scaleAvgUsers'));
|
||||
const selectedPlan = PLANS[$('scalePlan').value];
|
||||
const avgUsers = targetMode ? Math.max(avgUsersInput, selectedPlan.incOffice + selectedPlan.incSite) : avgUsersInput;
|
||||
const targetTenants = Math.ceil(targetUsers / avgUsers);
|
||||
const planInputs = planInputsFromMode();
|
||||
const email = EMAIL[$('emailTier').value];
|
||||
const storageAddon = STORAGE[$('storageAddon').value];
|
||||
const emailAttach = val('emailAttach')/100;
|
||||
const storageAttach = val('storageAttach')/100;
|
||||
const storageUse = val('storageUse')/100;
|
||||
const egressFactor = val('egressFactor');
|
||||
|
||||
let totalTenants = 0, totalUsers = 0, totalExtraUsers = 0, coreMRR = 0, monthlyListBasis = 0, sepaBasis = 0, storagePoolGB = 0;
|
||||
const rows = [];
|
||||
for(const [key,count,eo,es] of planInputs){
|
||||
const p = PLANS[key];
|
||||
const usersPerTenant = p.incOffice + p.incSite + eo + es;
|
||||
const extraUsers = (eo + es) * count;
|
||||
const monthlyTenant = planRevenueMonthly(p, eo, es);
|
||||
const sepaTenant = planRevenueSepa(p, eo, es);
|
||||
const weightedTenant = mix.sepa*sepaTenant + (mix.visa+mix.amex)*monthlyTenant;
|
||||
const mrr = weightedTenant * count;
|
||||
totalTenants += count;
|
||||
totalUsers += usersPerTenant * count;
|
||||
totalExtraUsers += extraUsers;
|
||||
coreMRR += mrr;
|
||||
monthlyListBasis += monthlyTenant * count;
|
||||
sepaBasis += sepaTenant * count;
|
||||
storagePoolGB += count * (p.storage + eo*20 + es*10);
|
||||
rows.push({ key, p, count, users:usersPerTenant*count, extraUsers, mrr, avg: count ? mrr/count : 0 });
|
||||
}
|
||||
|
||||
const emailTenants = totalTenants * emailAttach;
|
||||
const storageTenants = totalTenants * storageAttach;
|
||||
const emailRevenue = emailTenants * email.price;
|
||||
const emailCost = emailTenants * email.cost;
|
||||
const storageRevenue = storageTenants * storageAddon.price;
|
||||
storagePoolGB += storageTenants * storageAddon.gb;
|
||||
const MRR = coreMRR + emailRevenue + storageRevenue;
|
||||
|
||||
const avgAddonRevenue = totalTenants ? (emailRevenue + storageRevenue) / totalTenants : 0;
|
||||
const sepaFee = totalTenants * mix.sepa * (0.35/12);
|
||||
const visaFee = (monthlyListBasis / Math.max(totalTenants,1) + avgAddonRevenue) * totalTenants * mix.visa * 0.018 + totalTenants * mix.visa * 0.25;
|
||||
const amexFee = (monthlyListBasis / Math.max(totalTenants,1) + avgAddonRevenue) * totalTenants * mix.amex * 0.029 + totalTenants * mix.amex * 0.25;
|
||||
const paymentCost = sepaFee + visaFee + amexFee;
|
||||
|
||||
if(targetMode) totalUsers = targetUsers;
|
||||
|
||||
const actualStorageGB = storagePoolGB * storageUse;
|
||||
const storageCost = objectStorageCost(actualStorageGB, egressFactor);
|
||||
const infra = infraBreakdown(totalUsers);
|
||||
const hosting = infra.total;
|
||||
const misc = totalTenants * 0.75;
|
||||
const opex = hosting + storageCost + emailCost + paymentCost + misc;
|
||||
const gross = MRR - opex;
|
||||
const derivedExtraUsers = targetMode ? Math.max(0, avgUsers - (selectedPlan.incOffice + selectedPlan.incSite)) : 0;
|
||||
$('targetPreview').innerHTML = targetMode ? [
|
||||
['Betriebe', num(targetTenants)],
|
||||
['Paket', PLANS[$('scalePlan').value].label],
|
||||
['Extra-User/Betrieb', derivedExtraUsers.toLocaleString('de-DE',{maximumFractionDigits:1})]
|
||||
].map(([label,value]) => `<div><span>${label}</span><b>${value}</b></div>`).join('') : '';
|
||||
$('scaleModeNote').innerHTML = targetMode
|
||||
? `${num(targetUsers)} Nutzer bei Ø ${avgUsers.toLocaleString('de-DE',{maximumFractionDigits:1})} je Betrieb ergeben <b>${num(targetTenants)} ${selectedPlan.label}-Betriebe</b>.`
|
||||
: `Manueller Modus aktiv: Betriebe und Extra-User kommen ausschließlich aus der Tabelle.`;
|
||||
$('infraNote').innerHTML = `Für diese Rechnung werden <b>${infra.cap}</b> geplant. Compute/Infra: <b>${eur0(infra.total)}</b> vor Object Storage.`;
|
||||
|
||||
const persOn = $('persOn').checked;
|
||||
$('persBox').style.display = persOn ? 'block' : 'none';
|
||||
let personnel = 0, persParts = [];
|
||||
if(persOn){
|
||||
const employerRate = val('employerRate') / 100;
|
||||
const dev = roleCost('Entwickler', 'dev', 'devpay', 'devMode', 'devCostNote', employerRate);
|
||||
const sales = roleCost('Vertriebler', 'sales', 'salespay', 'salesMode', 'salesCostNote', employerRate);
|
||||
const support = roleCost('Support', 'supp', 'supppay', 'suppMode', 'suppCostNote', employerRate);
|
||||
personnel = dev.total + sales.total + support.total;
|
||||
persParts = [['Entwicklung',dev.total,COL.dev],['Vertrieb',sales.total,COL.sales],['Support',support.total,COL.support]];
|
||||
$('o_dev').textContent = val('dev'); $('o_devpay').textContent = eur0(val('devpay'));
|
||||
$('o_sales').textContent = val('sales'); $('o_salespay').textContent = eur0(val('salespay'));
|
||||
$('o_supp').textContent = val('supp'); $('o_supppay').textContent = eur0(val('supppay'));
|
||||
}
|
||||
const net = gross - personnel;
|
||||
|
||||
const visibleRows = rows.filter(r => r.count > 0);
|
||||
const mixLabel = visibleRows.length ? visibleRows.map(r=>`${num(r.count)} ${r.p.label}`).join(' · ') : 'kein aktiver Paketmix';
|
||||
$('summaryLead').innerHTML = `<strong>${num(totalTenants)} Betriebe</strong> (${mixLabel}) ergeben ${eur0(MRR)} MRR. Es sind ${num(totalUsers)} Nutzer modelliert, davon ${num(totalExtraUsers)} Extra-User.`;
|
||||
$('kpi_mrr').textContent = eur0(MRR); $('kpi_arr').textContent = eur0(MRR*12)+' ARR';
|
||||
$('kpi_tenants').textContent = num(totalTenants); $('kpi_users').textContent = `${num(totalUsers)} Nutzer · Ø ${totalTenants ? (totalUsers/totalTenants).toLocaleString('de-DE',{maximumFractionDigits:1}) : 0} je Betrieb`;
|
||||
$('kpi_opex').textContent = eur0(opex); $('kpi_opexpct').textContent = pct(opex/MRR)+' vom Umsatz';
|
||||
$('kpi_marginLabel').textContent = persOn ? 'Marge nach Personal' : 'Rohmarge';
|
||||
$('kpi_margin').textContent = eur0(persOn ? net : gross); $('kpi_margin').className = 'v '+((persOn ? net : gross)>=0?'pos':'neg');
|
||||
$('kpi_marginpct').textContent = pct((persOn ? net : gross)/MRR);
|
||||
|
||||
$('planRows').innerHTML = (visibleRows.length ? visibleRows : rows).map(r => `<tr><td><b>${r.p.label}</b></td><td class="n">${num(r.count)}</td><td class="n">${num(r.users)}</td><td class="n">${num(r.extraUsers)}</td><td class="n">${eur0(r.mrr)}</td><td class="n">${eur(r.avg)}</td></tr>`).join('')
|
||||
+ `<tr class="sum"><td>Core gesamt</td><td class="n">${num(totalTenants)}</td><td class="n">${num(totalUsers)}</td><td class="n">${num(totalExtraUsers)}</td><td class="n">${eur0(coreMRR)}</td><td class="n">${eur(totalTenants?coreMRR/totalTenants:0)}</td></tr>`;
|
||||
|
||||
$('paymentRows').innerHTML = [
|
||||
['SEPA Jahreszahlung', mix.sepa, sepaBasis*mix.sepa, '0,35 € / Jahr', sepaFee],
|
||||
['Visa/Mastercard', mix.visa, monthlyListBasis*mix.visa, '1,80 % + 0,25 €', visaFee],
|
||||
['American Express', mix.amex, monthlyListBasis*mix.amex, '2,90 % + 0,25 €', amexFee]
|
||||
].map(r => `<tr><td>${r[0]}</td><td class="n">${pct(r[1])}</td><td class="n">${eur0(r[2])}</td><td class="n">${r[3]}</td><td class="n">${eur(r[4])}</td></tr>`).join('')
|
||||
+ `<tr class="sum"><td>Payment gesamt</td><td class="n">100 %</td><td class="n">${eur0(MRR)}</td><td></td><td class="n">${eur(paymentCost)}</td></tr>`;
|
||||
|
||||
$('infraRows').innerHTML = infra.components.map(r => `<tr><td><b>${r[0]}</b></td><td>${r[1]}</td><td class="n">${eur(r[2])}</td></tr>`).join('')
|
||||
+ `<tr class="sum"><td>Compute/Infra gesamt</td><td>${infra.cap}</td><td class="n">${eur0(hosting)}</td></tr>`;
|
||||
$('infraScaleNote').innerHTML = `${infra.note} Object Storage kommt darunter separat dazu: aktuell ${eur(storageCost)} bei ${gb(actualStorageGB)} angenommener Nutzung.`;
|
||||
|
||||
const opexRows = [['Compute/Infra',hosting,COL.infra],['Object Storage',storageCost,COL.storage],['Mailjet',emailCost,COL.mailjet],['Mollie',paymentCost,COL.payment],['Monitoring / Kleinkram',misc,COL.misc]];
|
||||
$('opexRows').innerHTML = opexRows.map(r => `<tr><td>${r[0]}</td><td class="n">${eur0(r[1])}</td><td class="n">${pct(r[1]/MRR)}</td><td class="n">${(r[1]/MRR*100).toLocaleString('de-DE',{maximumFractionDigits:2})}</td></tr>`).join('')
|
||||
+ `<tr class="sum"><td>Betrieb gesamt</td><td class="n">${eur0(opex)}</td><td class="n">${pct(opex/MRR)}</td><td class="n">${(opex/MRR*100).toLocaleString('de-DE',{maximumFractionDigits:2})}</td></tr>`;
|
||||
|
||||
$('addonRows').innerHTML = [
|
||||
['Email-Marketing Betriebe', num(emailTenants)],
|
||||
['Email-Marketing Umsatz', eur0(emailRevenue)],
|
||||
['Mailjet-Kosten', eur0(emailCost)],
|
||||
['Email-Marketing Rohertrag', eur0(emailRevenue-emailCost)],
|
||||
['Speicher-Add-on Betriebe', num(storageTenants)],
|
||||
['Speicher-Add-on Umsatz', eur0(storageRevenue)]
|
||||
].map(r=>`<tr><td>${r[0]}</td><td class="n">${r[1]}</td></tr>`).join('');
|
||||
|
||||
$('storageRows').innerHTML = [
|
||||
['Verkaufter Speicherpool', gb(storagePoolGB)],
|
||||
['Angenommene Nutzung', gb(actualStorageGB)],
|
||||
['Object-Storage-Kosten', eur(storageCost)],
|
||||
['Speicher-Add-on Umsatz', eur0(storageRevenue)],
|
||||
['Speicher Rohertrag', eur0(storageRevenue-storageCost)]
|
||||
].map(r=>`<tr><td>${r[0]}</td><td class="n">${r[1]}</td></tr>`).join('');
|
||||
|
||||
let split = opexRows.slice();
|
||||
if(persOn) split = split.concat(persParts);
|
||||
split.push([persOn?'Marge':'Rohmarge', Math.max(0, persOn?net:gross), COL.margin]);
|
||||
const denom = split.reduce((s,p)=>s+p[1],0) || 1;
|
||||
$('splitbar').innerHTML = split.filter(p=>p[1]>0).map(([label,value,color]) => {
|
||||
const width = value/denom*100, cents = value/MRR*100;
|
||||
return `<span style="width:${width}%;background:${color}" title="${label}: ${pct(value/MRR)}">${width>7?Math.round(cents)+'¢':''}</span>`;
|
||||
}).join('');
|
||||
$('splitlegend').innerHTML = split.filter(p=>p[1]>0).map(([label,value,color])=>`<span><i style="background:${color}"></i>${label} ${Math.round(value/MRR*100)}¢ (${pct(value/MRR)})</span>`).join('')
|
||||
+ (persOn && net<0 ? ` <span class="neg">Verlust ${eur0(net)}</span>` : '');
|
||||
|
||||
$('persCard').style.display = persOn ? 'block' : 'none';
|
||||
if(persOn){
|
||||
$('persRows').innerHTML = [['Umsatz',MRR],['Betrieb',-opex],...persParts.map(p=>[p[0],-p[1]]),['Ergebnis',net]].map((r,i,arr)=>`<tr class="${i===arr.length-1?'sum':''}"><td>${i>0&&i<arr.length-1?'– ':''}${r[0]}</td><td class="n ${i===arr.length-1?(r[1]>=0?'pos':'neg'):''}">${eur0(r[1])}</td><td class="n">${pct(r[1]/MRR)}</td></tr>`).join('');
|
||||
const variablePerTenant = totalTenants ? (opex-hosting)/totalTenants : 0;
|
||||
const revenuePerTenant = totalTenants ? MRR/totalTenants : 0;
|
||||
const contribution = revenuePerTenant - variablePerTenant;
|
||||
const be = contribution>0 ? Math.ceil((personnel+hosting)/contribution) : Infinity;
|
||||
$('breakeven').innerHTML = Number.isFinite(be) ? `<b>Break-even:</b> ca. <b>${be} Betriebe</b> bei ${eur(contribution)} Deckungsbeitrag je Betrieb. Aktuell ${num(totalTenants)} Betriebe: ${totalTenants>=be?`<span class="pos">${num(totalTenants-be)} über Break-even</span>`:`<span class="neg">${num(be-totalTenants)} fehlen</span>`}.` : 'Deckungsbeitrag ist negativ.';
|
||||
}
|
||||
|
||||
$('paymentNote').innerHTML = `Mix wird automatisch auf 100 % gehalten. SEPA nutzt Jahres-SEPA-Preise, Karten nutzen Listenpreise. Aktueller Payment-Kostensatz: <b>${pct(paymentCost/MRR)}</b>.`;
|
||||
}
|
||||
|
||||
function wire(){
|
||||
document.querySelectorAll('input,select').forEach(el => {
|
||||
el.addEventListener('input', e => {
|
||||
if(['paySepa','payVisa','payAmex'].includes(e.target.id)){
|
||||
lastPay = e.target.id;
|
||||
syncPayment(lastPay);
|
||||
}
|
||||
compute();
|
||||
});
|
||||
});
|
||||
$('persOn').addEventListener('change', compute);
|
||||
}
|
||||
|
||||
syncPayment('paySepa');
|
||||
wire();
|
||||
compute();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -60,7 +60,7 @@
|
||||
<body>
|
||||
<header>
|
||||
<h1>HiveGrid · Kostenmodell & Unit Economics <span class="pill">Stand 22.06.2026</span></h1>
|
||||
<p>Reiner Betrieb (Hosting · Mailjet · Stripe), kein eigenes KI-Token-Risiko (MCP-Server = Kunde bringt eigenes Modell). Personal optional zuschaltbar. Alle Hebel live drehbar.</p>
|
||||
<p>Reiner Betrieb (Hosting · Mailjet · Zahlung), kein eigenes KI-Token-Risiko (MCP-Server = Kunde bringt eigenes Modell). Personal optional zuschaltbar. Alle Hebel live drehbar.</p>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
@@ -89,11 +89,16 @@
|
||||
<label class="row"><span class="lab">Mailjet-Preis je Mail <span class="val" id="o_mailrate"></span></span>
|
||||
<input type="range" id="mailrate" min="0.0005" max="0.0015" step="0.0001" value="0.0008"></label>
|
||||
|
||||
<h3>Zahlungsart (Stripe)</h3>
|
||||
<div class="seg" id="seg_pay">
|
||||
<button data-pay="card" class="on">Karte · 1,5 % + 0,25 €</button>
|
||||
<button data-pay="sepa">SEPA-Lastschrift · 0,8 % (max 5 €)</button>
|
||||
<h3>Zahlung</h3>
|
||||
<div class="seg" id="seg_prov">
|
||||
<button data-prov="legacyProvider" class="on">Legacy-Provider</button>
|
||||
<button data-prov="adyen">Adyen</button>
|
||||
</div>
|
||||
<div class="seg" id="seg_meth" style="margin-top:8px">
|
||||
<button data-meth="card" class="on">Visa / Mastercard</button>
|
||||
<button data-meth="sepa">SEPA-Lastschrift</button>
|
||||
</div>
|
||||
<div class="note" id="payNote"></div>
|
||||
|
||||
<h3>Infra-Stufe</h3>
|
||||
<div class="seg" id="seg_infra">
|
||||
@@ -176,9 +181,11 @@
|
||||
Stufen (Engineering-Schätzung, vor Lasttest): Start 56 € (≤~1.000 Seats) · Wachstum 120 € (≤~2.500) · Skalierung 450 € (≤~6.000).
|
||||
</div>
|
||||
</details>
|
||||
<details><summary>Stripe & Mailjet</summary>
|
||||
<details><summary>Zahlungsgebühren (Legacy-Provider vs. Adyen) & Mailjet</summary>
|
||||
<div class="src" style="margin-top:8px">
|
||||
Stripe EU: Karte <b>1,5 % + 0,25 €</b> je Buchung · SEPA-Lastschrift <b>0,8 % gedeckelt 5 €</b> (für DE-Handwerk das natürliche Zahlungsmittel → halbiert die größte Betriebszeile).
|
||||
<b>Legacy-Provider</b> (inkl. Billing 0,7 %): Karte 2,2 % + 0,25 € · SEPA 1,5 % + 0,30 €.
|
||||
<b>Adyen</b> (Interchange++): Karte ~0,4 % + 0,11 € · SEPA ~0,12 € flat. Minimum ~120 € dokumentiert, praktisch <b>~1.000 €+/Monat</b> bei Direktvertrag; <b>keine native Subscription-Billing/Customer-Portal-Schicht</b> (selbst bauen oder Chargebee/Recurly dazu — oft wieder US). Lohnt erst ab ~300 k €/Monat Processing.
|
||||
EEA-Standardkarten unterstellt; Firmen-Kreditkarten haben höhere, unregulierte Interchange (verteuert beide Anbieter).
|
||||
Mailjet ~€0,0006–0,001/Mail je Aggregat-Volumen.
|
||||
</div>
|
||||
</details>
|
||||
@@ -189,20 +196,25 @@
|
||||
<a href="https://mailchimp.com/resources/email-marketing-benchmarks/">Mailchimp E-Mail-Benchmarks</a> ·
|
||||
<a href="https://www.hetzner.com/cloud/">Hetzner Cloud Preise</a> ·
|
||||
<a href="https://www.mailjet.com/pricing/">Mailjet Preise</a> ·
|
||||
Stripe DE Standard-Gebühren.
|
||||
Legacy-Provider DE Standard-Gebühren.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const COL = {hosting:'#3b82f6',mailjet:'#f5a623',stripe:'#8b5cf6',misc:'#94a3b8',dev:'#d23b3b',sales:'#0ea5e9',support:'#1f9d57',marge:'#16a34a'};
|
||||
const COL = {hosting:'#3b82f6',mailjet:'#f5a623',legacyProvider:'#8b5cf6',misc:'#94a3b8',dev:'#d23b3b',sales:'#0ea5e9',support:'#1f9d57',marge:'#16a34a'};
|
||||
const $ = id => document.getElementById(id);
|
||||
const eur = n => '€'+(n).toLocaleString('de-DE',{minimumFractionDigits:n<100&&n!==Math.round(n)?2:0,maximumFractionDigits:2});
|
||||
const eur0 = n => '€'+Math.round(n).toLocaleString('de-DE');
|
||||
const pct = n => (n*100).toLocaleString('de-DE',{maximumFractionDigits:1})+' %';
|
||||
|
||||
let pay='card', infra='auto';
|
||||
let prov='legacyProvider', meth='card', infra='auto';
|
||||
const ADYEN_FLOOR=1000;
|
||||
function feePerTenant(charge){
|
||||
if(prov==='legacyProvider') return meth==='card' ? charge*0.022+0.25 : charge*0.015+0.30; // inkl. Billing 0,7 %
|
||||
return meth==='card' ? charge*0.004+0.11 : 0.12; // Adyen IC++
|
||||
}
|
||||
|
||||
function autoInfra(seatsTotal){
|
||||
if(seatsTotal<=1000) return 56;
|
||||
@@ -231,15 +243,21 @@ function compute(){
|
||||
// mailjet
|
||||
const mailjet = mails*tenants*mailrate;
|
||||
|
||||
// stripe (per tenant monthly charge)
|
||||
// payment fees (per tenant charge → provider floor)
|
||||
const charge = price*seats;
|
||||
const stripePerT = pay==='card' ? charge*0.015+0.25 : Math.min(charge*0.008,5);
|
||||
const stripe = stripePerT*tenants;
|
||||
const feePerT = feePerTenant(charge);
|
||||
let payfee = feePerT*tenants, floorOn=false;
|
||||
if(prov==='adyen' && payfee<ADYEN_FLOOR){ payfee=ADYEN_FLOOR; floorOn=true; }
|
||||
const rateTxt = prov==='legacyProvider'
|
||||
? (meth==='card'?'2,2 % + 0,25 € (inkl. Billing 0,7 %)':'1,5 % + 0,30 € (inkl. Billing 0,7 %)')
|
||||
: (meth==='card'?'~0,4 % + 0,11 € (IC++)':'~0,12 € flat (IC++)');
|
||||
$('payNote').innerHTML = `${prov==='legacyProvider'?'Legacy-Provider':'Adyen'} · ${rateTxt} → ${eur(feePerT)}/Mandant`
|
||||
+ (prov==='adyen' ? ` · Minimum ${eur0(ADYEN_FLOOR)}/Mo${floorOn?` <b>(greift — Nutzung wäre nur ${eur(feePerT*tenants)})</b>`:''}` : '');
|
||||
|
||||
// misc
|
||||
const misc = tenants*0.5;
|
||||
|
||||
const opex = hosting+mailjet+stripe+misc;
|
||||
const opex = hosting+mailjet+payfee+misc;
|
||||
const gross = MRR-opex;
|
||||
|
||||
// KPIs
|
||||
@@ -249,11 +267,13 @@ function compute(){
|
||||
$('kpi_opexpct').textContent=pct(opex/MRR)+' vom Umsatz';
|
||||
|
||||
// opex table
|
||||
const rows=[['Hosting',hosting,COL.hosting],['Mailjet',mailjet,COL.mailjet],['Stripe-Gebühren',stripe,COL.stripe],['Geo-IP / Kleinkram',misc,COL.misc]];
|
||||
const rows=[['Hosting',hosting,COL.hosting],['Mailjet',mailjet,COL.mailjet],['Zahlungsgebühren',payfee,COL.legacyProvider],['Geo-IP / Kleinkram',misc,COL.misc]];
|
||||
$('opexrows').innerHTML=rows.map(([l,v])=>`<tr><td>${l}</td><td class="n">${eur0(v)}</td><td class="n">${eur(v/tenants)}</td><td class="n">${pct(v/MRR)}</td><td class="n">${(v/MRR*100).toLocaleString('de-DE',{maximumFractionDigits:2})}</td></tr>`).join('')
|
||||
+`<tr class="sum"><td>Betrieb gesamt</td><td class="n">${eur0(opex)}</td><td class="n">${eur(opex/tenants)}</td><td class="n">${pct(opex/MRR)}</td><td class="n">${(opex/MRR*100).toLocaleString('de-DE',{maximumFractionDigits:2})}</td></tr>`;
|
||||
const biggest = rows.slice().sort((a,b)=>b[1]-a[1])[0][0];
|
||||
$('opexnote').innerHTML=`Größter Betriebsposten: <b>${biggest}</b>. ${pay==='card'?'Tipp: SEPA-Lastschrift statt Karte halbiert die Stripe-Zeile.':'SEPA-Lastschrift aktiv → Stripe gedeckelt.'}`;
|
||||
$('opexnote').innerHTML=`Größter Betriebsposten: <b>${biggest}</b>. `
|
||||
+(meth==='card'?'Hebel 1: auf <b>SEPA-Lastschrift</b> wechseln. ':'')
|
||||
+(prov==='legacyProvider'?'Hebel 2: <b>Legacy-Provider → Adyen</b> drückt die Zahlungszeile drastisch.':'Adyen aktiv → Zahlungszeile minimal.');
|
||||
|
||||
// personnel
|
||||
const persOn=$('persOn').checked;
|
||||
@@ -282,7 +302,7 @@ function compute(){
|
||||
}
|
||||
|
||||
// split bar (cents per euro)
|
||||
let parts=[['Hosting',hosting,COL.hosting],['Mailjet',mailjet,COL.mailjet],['Stripe',stripe,COL.stripe],['Kleinkram',misc,COL.misc]];
|
||||
let parts=[['Hosting',hosting,COL.hosting],['Mailjet',mailjet,COL.mailjet],['Zahlung',payfee,COL.legacyProvider],['Kleinkram',misc,COL.misc]];
|
||||
if(persOn) parts=parts.concat(persParts);
|
||||
const margeVal=Math.max(net,0);
|
||||
parts.push([persOn?'Marge':'Rohmarge',margeVal,COL.marge]);
|
||||
@@ -301,7 +321,7 @@ function compute(){
|
||||
$('persrows').innerHTML=blocks.map(([l,v])=>`<tr><td>${l}</td><td class="n">${eur0(v)}</td><td class="n">${pct(v/MRR)}</td></tr>`).join('')
|
||||
+`<tr class="sum"><td>= Ergebnis</td><td class="n ${net>=0?'pos':'neg'}">${eur0(net)}</td><td class="n">${pct(net/MRR)}</td></tr>`;
|
||||
// break-even: tenants where MRR - variableOpex*t - fixedHosting - personnel = 0
|
||||
const contrib = charge - stripePerT - mails*mailrate - 0.5; // per tenant
|
||||
const contrib = charge - feePerT - mails*mailrate - 0.5; // per tenant
|
||||
const fixed = personnel + hosting;
|
||||
const be = contrib>0 ? Math.ceil(fixed/contrib) : Infinity;
|
||||
$('breakeven').innerHTML = isFinite(be)
|
||||
@@ -314,7 +334,8 @@ function compute(){
|
||||
['price','seats','tenants','mails','mailrate','persOn','dev','devpay','sales','salespay','supp','supppay']
|
||||
.forEach(id=>$(id).addEventListener('input',compute));
|
||||
$('persOn').addEventListener('change',()=>{$('persBox').style.display=$('persOn').checked?'block':'none';compute();});
|
||||
document.querySelectorAll('#seg_pay button').forEach(b=>b.onclick=()=>{pay=b.dataset.pay;document.querySelectorAll('#seg_pay button').forEach(x=>x.classList.remove('on'));b.classList.add('on');compute();});
|
||||
document.querySelectorAll('#seg_prov button').forEach(b=>b.onclick=()=>{prov=b.dataset.prov;document.querySelectorAll('#seg_prov button').forEach(x=>x.classList.remove('on'));b.classList.add('on');compute();});
|
||||
document.querySelectorAll('#seg_meth button').forEach(b=>b.onclick=()=>{meth=b.dataset.meth;document.querySelectorAll('#seg_meth button').forEach(x=>x.classList.remove('on'));b.classList.add('on');compute();});
|
||||
document.querySelectorAll('#seg_infra button').forEach(b=>b.onclick=()=>{infra=b.dataset.infra;document.querySelectorAll('#seg_infra button').forEach(x=>x.classList.remove('on'));b.classList.add('on');compute();});
|
||||
document.querySelectorAll('.presets button').forEach(b=>b.onclick=()=>{
|
||||
const m={klein:[20,5000],mittel:[40,10000],gross:[60,17500]}[b.dataset.size];
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
create_and_fix.py — Phase 2+3 der Review-Plane-Operation. IDEMPOTENT, default DRY-RUN, scharf: --apply
|
||||
P2: neue David-Screen-Tickets anlegen (+ Cycle + Module David + Doc-Link) — nur was fehlt.
|
||||
P3: Bündel auflösen (VD-2/3/4 rename→VD-P26/27/29; D-REST/VD-1/VD-5 cancel),
|
||||
AK-P1c→R-P1c (rename+Owner+Module), Fixes (R-P8a Name, EXT-04 KWP-Nr, R-REFIT→Done).
|
||||
"""
|
||||
import json, os, re, sys, time, urllib.request, urllib.error
|
||||
APPLY = "--apply" in sys.argv
|
||||
TOK = {}
|
||||
for line in open(os.path.expanduser("~/.cli-tokens/plane")):
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1); TOK[k.strip()] = v.strip()
|
||||
BASE = TOK["PLANE_BASE_URL"].rstrip("/"); WS = TOK["PLANE_WORKSPACE"]; KEY = TOK["PLANE_API_KEY"]
|
||||
PID = "35d0d968-8ab3-40cc-97b8-b81d917dc5df"
|
||||
API = f"{BASE}/api/v1/workspaces/{WS}/projects/{PID}"
|
||||
DOC = "https://git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud/src/branch/main/.planning/ticket-docs/"
|
||||
BRF = "https://git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud/src/branch/main/.planning/designer-briefs/"
|
||||
DAVID = "6c1fc63f-133d-4f40-86e4-7f658a6edcc7"; RAFFAEL = "8e34a88c-19c9-4565-b4f5-50415d623ce8"
|
||||
bk = json.load(open(".planning/plane-sync/plane-backup-review.json"))
|
||||
M = bk["meta"]
|
||||
CYC = M["cycles"]; MOD = M["modules"]; ST = M["states"]; LB = M["labels"]
|
||||
def A(cls, href, txt): return f'<a target="_blank" class="text-accent-secondary underline underline-offset-[3px] hover:text-accent-primary transition-colors cursor-pointer" href="{href}" rel="noopener noreferrer">{txt}</a>'
|
||||
|
||||
def http(method, path, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
r = urllib.request.Request(API + path, data=data, method=method)
|
||||
r.add_header("X-API-Key", KEY)
|
||||
if data: r.add_header("Content-Type", "application/json")
|
||||
for _ in range(4):
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
raw = resp.read().decode(); return resp.status, (json.loads(raw) if raw else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 429: time.sleep(5); continue
|
||||
return e.code, {}
|
||||
return 999, {}
|
||||
|
||||
def get_all(path):
|
||||
out, c = [], None
|
||||
while True:
|
||||
st, d = http("GET", path + (("?per_page=100&cursor=" + c) if c else "?per_page=100"))
|
||||
if st >= 400: break
|
||||
if isinstance(d, list): out += d; break
|
||||
out += d.get("results", [])
|
||||
if d.get("next_page_results") and d.get("next_cursor"): c = d["next_cursor"]
|
||||
else: break
|
||||
return out
|
||||
|
||||
# aktueller Stand → Working-ID → issue
|
||||
cur = get_all("/issues/")
|
||||
wid2 = {re.split(r'\s*·\s*', i["name"])[0].strip(): i for i in cur}
|
||||
|
||||
def desc(wid, brief, blockedby, blocks, owner, fenster):
|
||||
parts = ["<div><p><b>📚 Doku</b><br>",
|
||||
A("", DOC + wid + ".md", f"📄 Ticket-Doku ({wid})"), "<br>"]
|
||||
if brief: parts += [A("", BRF + brief + ".md", f"🎨 Designer-Brief {brief[:3]}"), "<br>"]
|
||||
parts += [A("", "https://git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud/src/branch/main/.planning/ENTITY-MODEL.md", "ENTITY-MODEL.md"), "</p>"]
|
||||
parts += [f"<p><b>⛔ blocked-by:</b> {blockedby}<br><b>➡️ blocks:</b> {blocks}<br>",
|
||||
f"<b>👤 Owner:</b> {owner} · <b>🗓 Fenster:</b> {fenster}</p>",
|
||||
"<p><b>✅ Screens / Features / DoD:</b> siehe Ticket-Doku (oben).</p></div>"]
|
||||
return "".join(parts)
|
||||
|
||||
# NEW: (wid, titel, brief, cycle, target, v2, blocks)
|
||||
NEW = [
|
||||
("D-P2","Screens P2 — Identität & Mandantentrennung","P02-identitaet-mandantentrennung","W1","2026-06-21",0,"R-P2a,R-P2b"),
|
||||
("D-P3","Screens P3 — Querschnitts-Oberflächen","P03-querschnitts-oberflaechen","W2","2026-06-21",0,"AK-P3"),
|
||||
("D-P4","Screens P4 — Files & Tasks","P04-files-tasks","W1","2026-06-19",0,"AK-P4a,AK-P4b"),
|
||||
("D-P5","Screens P5 — Apps-Engine","P05-apps-engine-integrations-framework","W2","2026-06-21",0,"R-P5"),
|
||||
("D-P7","Screens P7 — Lead-Inbox","P07-lead-inbox","W2","2026-06-26",0,"R-P7"),
|
||||
("D-P8","Screens P8 — Projekt & Pipelines (Kern, ohne Kanban)","P08-projekt-pipelines-kern","W3","2026-06-29",0,"R-P8a,R-P8b"),
|
||||
("D-P9","Screens P9 — Auftrags-Sub-Areas (ohne Plantafel/Abnahme)","P09-projekt-pipelines-auftrag","W3","2026-07-01",0,"R-P9a,R-P9b"),
|
||||
("D-P12","Screens P12 — Automation (ohne Flow-Editor)","P12-automation-engine","W4","2026-07-07",0,"R-P12a,R-P12b"),
|
||||
("D-P13","Screens P13 — KI-Layer (ex-D-REST)","P13-ki-layer","W4","2026-07-09",0,"R-P13"),
|
||||
("D-P14","Screens P14 — Email & Marketing (ohne Kampagnen-Editor)","P14-email-marketing","W3","2026-06-29",0,"AK-P14a,AK-P14b"),
|
||||
("D-P15","Screens P15 — Telefonie (ohne Chat)","P15-telefonie-chat","W3","2026-07-01",0,"AK-P15a"),
|
||||
("D-P16","Screens P16 — Kalender/Signatur (ohne Form/Booking)","P16-kalender-booking-forms-signatur","W4","2026-07-07",0,"AK-P16a,AK-P16d"),
|
||||
("D-P20","Screens P20 — Billing/Onboarding/Branding (ex-D-REST)","P20-billing-onboarding-branding","W4","2026-07-08",0,"AK-P20"),
|
||||
("D-P21","Screens P21 — DSGVO/Launch (ex-D-REST)","P21-dsgvo-launch","W4","2026-07-09",0,"AK-P21"),
|
||||
("VD-P22","Screens P22 — Public-API-Keys (ex-VD-5)","P22-public-api-webhooks","v2W1","2026-07-17",1,"VR-P22a,VR-P22b"),
|
||||
("VD-P23","Screens P23 — Postfach-Inbox (ex-VD-1)","P23-postfach-sync","v2W1","2026-07-17",1,"VA-P23"),
|
||||
("VD-P24","Screens P24 — Abo-Baukasten + Wizard (ex-VD-1)","P24-self-service-billing-wizard","v2W1","2026-07-15",1,"VA-P24a,VA-P24b"),
|
||||
("VD-P25","Screens P25 — KI-Tiefe/HiveMind","P25-ki-tiefe-hivemind-rag","v2W1","2026-07-20",1,"VR-P25"),
|
||||
("VD-P28","Screens P28 — Plattform-Admin-Tiefe (ex-VD-5)","P28-plattform-admin-tiefe","v2W3","2026-07-29",1,"VR-P28a,VA-P28b"),
|
||||
("VD-P30","Screens P30 — Dashboard-Deluxe","P30-dashboard-deluxe-querschnitt","v2W3","2026-07-30",1,"VA-P30a,VA-P30b"),
|
||||
("VD-P31","Screens P31 — DSGVO-Vollausbau","P31-dsgvo-vollausbau","v2W3","2026-07-31",1,"VR-P31"),
|
||||
("VD-P32","Screens P32 — KWP-Setup (ex-VD-5)","P32-kwp-integration","v2W2","2026-07-22",1,"VR-P32a,VR-P32b,VR-P32c"),
|
||||
]
|
||||
CYK = {"W1":"W1 — Fundament & Identität","W2":"W2 — Plattform & CRM (Demo 1)","W3":"W3 — Kern-Kette & Kommunikation (Demo 2)","W4":"W4 — USP, Härtung & Launch","v2W1":"v2-W1 — Public-API/Postfach/Abo/KI-Start","v2W2":"v2-W2 — KWP/Portal/Mobile-Start","v2W3":"v2-W3 — Admin/Help/Dashboard/v2-Launch"}
|
||||
|
||||
created = skipped = 0
|
||||
print(f"=== Phase 2 NEUE Tickets {'(SCHARF)' if APPLY else '(DRY-RUN)'} ===")
|
||||
for wid, titel, brief, cyc, tgt, v2, blocks in NEW:
|
||||
if wid in wid2:
|
||||
skipped += 1; continue
|
||||
name = f"{wid} · {titel}"
|
||||
labels = [LB["typ:screen"]] + ([LB["v2"]] if v2 else [])
|
||||
body = {"name": name, "description_html": desc(wid, brief, "D-D0a, D-D0b", blocks, "david", CYK[cyc].split(" — ")[0]),
|
||||
"state": ST.get("Backlog"), "assignees": [DAVID], "labels": labels, "target_date": tgt}
|
||||
if not APPLY:
|
||||
print(f" + {wid:8} [{cyc}] → {titel[:40]}"); created += 1; continue
|
||||
st, d = http("POST", "/issues/", body); time.sleep(1.1)
|
||||
if st < 300 and d.get("id"):
|
||||
iid = d["id"]
|
||||
http("POST", f"/cycles/{CYC[CYK[cyc]]}/cycle-issues/", {"issues": [iid]}); time.sleep(1.0)
|
||||
http("POST", f"/modules/{MOD['David (Screens)']}/module-issues/", {"issues": [iid]}); time.sleep(1.0)
|
||||
print(f" ✓ angelegt {wid}"); created += 1
|
||||
else:
|
||||
print(f" ✗ {wid}: HTTP {st}")
|
||||
print(f"angelegt: {created} | schon vorhanden (skip): {skipped}")
|
||||
|
||||
# ---- Phase 3 ----
|
||||
def add_doclink(html, wid):
|
||||
if f"ticket-docs/{wid}.md" in html: return html
|
||||
link = A("", DOC + wid + ".md", f"📄 Ticket-Doku ({wid})") + "<br>"
|
||||
return re.sub(r'(📚 Doku</(?:b|strong)><br\s*/?>)', lambda m: m.group(1) + link, html, count=1)
|
||||
|
||||
RENAME = [("VD-2","VD-P26","Screens P26 — Customer-Portal"),
|
||||
("VD-3","VD-P27","Screens P27 — Mobile-PWA-Shell (PWA-only)"),
|
||||
("VD-4","VD-P29","Screens P29 — Help-Center")]
|
||||
CANCEL = [("D-REST","aufgelöst → D-P13/D-P20/D-P21"),
|
||||
("VD-1","aufgelöst → VD-P23/VD-P24"),
|
||||
("VD-5","aufgelöst → VD-P22/VD-P28/VD-P32")]
|
||||
print(f"\n=== Phase 3 Rename/Cancel/Fix {'(SCHARF)' if APPLY else '(DRY-RUN)'} ===")
|
||||
def pat(wid, body, note):
|
||||
i = wid2.get(wid)
|
||||
if not i: print(f" ⚠ {wid} nicht gefunden"); return
|
||||
print(f" ~ {wid}: {note}")
|
||||
if APPLY:
|
||||
st, _ = http("PATCH", f"/issues/{i['id']}/", body); time.sleep(1.1)
|
||||
if st >= 300: print(f" ✗ HTTP {st}")
|
||||
|
||||
for old, new, titel in RENAME:
|
||||
i = wid2.get(old)
|
||||
if i: pat(old, {"name": f"{new} · {titel}", "description_html": add_doclink(i["description_html"], new)}, f"rename → {new}")
|
||||
for wid, note in CANCEL:
|
||||
i = wid2.get(wid)
|
||||
if i:
|
||||
nm = i["name"] if "aufgelöst" in i["name"] else i["name"] + f" ({note})"
|
||||
pat(wid, {"name": nm, "state": ST["Cancelled"]}, f"CANCEL ({note})")
|
||||
# AK-P1c → R-P1c (rename + Owner Raffael + Module-Wechsel)
|
||||
i = wid2.get("AK-P1c")
|
||||
if i:
|
||||
nm = "R-P1c · " + re.split(r'\s*·\s*', i["name"], maxsplit=1)[1] if "·" in i["name"] else "R-P1c · P1 Hot-Swap-Maschine + Walking-Skeleton + PWA"
|
||||
pat("AK-P1c", {"name": nm, "assignees": [RAFFAEL], "description_html": add_doclink(i["description_html"].replace("AK-P1c","R-P1c"), "R-P1c")}, "→ R-P1c (Owner Raffael)")
|
||||
if APPLY:
|
||||
http("POST", f"/modules/{MOD['Raffael (Dev)']}/module-issues/", {"issues": [i["id"]]}); time.sleep(1.0)
|
||||
http("DELETE", f"/modules/{MOD['Akeshan (Dev)']}/module-issues/{i['id']}/"); time.sleep(1.0)
|
||||
# R-P8a Name-Fix (Deal→Projekt)
|
||||
i = wid2.get("R-P8a")
|
||||
if i and "Deal-CRUD" in i["name"]:
|
||||
pat("R-P8a", {"name": i["name"].replace("Deal-CRUD", "Projekt-CRUD")}, "Name Deal-CRUD→Projekt-CRUD")
|
||||
# EXT-04 KWP-Nummer
|
||||
i = wid2.get("EXT-04")
|
||||
if i and "v2-P10" in i["description_html"]:
|
||||
pat("EXT-04", {"description_html": i["description_html"].replace("v2-P10", "P32 (v2)")}, "KWP-Nummer v2-P10→P32")
|
||||
# R-REFIT → Done
|
||||
i = wid2.get("R-REFIT")
|
||||
if i and i["state"] != ST["Done"]:
|
||||
pat("R-REFIT", {"state": ST["Done"]}, "→ Done (erledigt 16.06.)")
|
||||
print("\n" + ("✅ Phase 2+3 scharf ausgeführt." if APPLY else "→ scharf: python3 .planning/plane-sync/create_and_fix.py --apply"))
|
||||
@@ -13,6 +13,9 @@ Operationen:
|
||||
- VA-P27a/b + VD-3 → Mobile-PWA-Ausbau umgewidmet (Expo/native/Store raus)
|
||||
- VEXT-4, VJ-1 → v3-Backlog (aus Cycle entfernt, Label v2→v3)
|
||||
- VD-6, EXT-13, VJ-3 → App-Store-/native-Teil entfernt, Rest bleibt
|
||||
C) Board-States/Labels-Hygiene (2026-06-28): "In Review" gehört in Gruppe started
|
||||
(nicht completed, sonst zählen wartende Tickets als erledigt); neuer State
|
||||
"Blocked / On Hold" + Labels wartet-auf-screen / wartet-extern (idempotent angelegt).
|
||||
|
||||
Default = DRY-RUN (zeigt nur Diffs, schreibt nichts). Mit --apply scharf schalten.
|
||||
"""
|
||||
@@ -178,6 +181,42 @@ if V3 is None:
|
||||
print("[würde Label 'v3' anlegen]")
|
||||
V2 = labels.get("v2")
|
||||
|
||||
# ---------- C) Board-States/Labels sicherstellen (idempotent) ----------
|
||||
# Workflow-Hygiene: "In Review" muss in Gruppe started liegen (nicht completed, sonst
|
||||
# zaehlen wartende Tickets als erledigt). Neuer State "Blocked / On Hold" fuer "Code
|
||||
# fertig, liegt bewusst" + Labels fuer den Blocker-Grund (typuebergreifend, damit Davids
|
||||
# Screen-Board sauber bleibt). Siehe Operation C im Docstring.
|
||||
print("\n--- Board-States/Labels ---")
|
||||
WANT_LABELS = {"wartet-auf-screen": "#8B5CF6", "wartet-extern": "#0EA5E9"}
|
||||
for lname, lcolor in WANT_LABELS.items():
|
||||
if lname in labels:
|
||||
print(f" ok (bereits): Label '{lname}'")
|
||||
elif APPLY:
|
||||
st, d = req("POST", "/labels/", {"name": lname, "color": lcolor})
|
||||
if st < 400: labels[lname] = d["id"]; print(f" Label '{lname}' angelegt.")
|
||||
else: print(f" Label '{lname}' FAIL:", st, d)
|
||||
else:
|
||||
print(f" [wuerde Label '{lname}' anlegen]")
|
||||
|
||||
states_by_name = {s["name"]: s for s in get_all("/states/")}
|
||||
ir = states_by_name.get("In Review")
|
||||
if ir and ir.get("group") != "started":
|
||||
print(f" FIX: 'In Review' group {ir.get('group')} -> started")
|
||||
if APPLY:
|
||||
st, d = req("PATCH", f"/states/{ir['id']}/", {"group": "started", "sequence": 75000})
|
||||
if st >= 400: print(" FAIL:", st, d)
|
||||
elif ir:
|
||||
print(" ok (bereits): 'In Review' in started")
|
||||
if "Blocked / On Hold" in states_by_name:
|
||||
print(" ok (bereits): State 'Blocked / On Hold'")
|
||||
elif APPLY:
|
||||
st, d = req("POST", "/states/", {"name": "Blocked / On Hold", "color": "#F59E0B",
|
||||
"group": "started", "sequence": 80000})
|
||||
if st < 400: print(" State 'Blocked / On Hold' angelegt.")
|
||||
else: print(" State 'Blocked / On Hold' FAIL:", st, d)
|
||||
else:
|
||||
print(" [wuerde State 'Blocked / On Hold' anlegen]")
|
||||
|
||||
# Cycle-Namen fixen
|
||||
print("\n--- Cycle-Namen ---")
|
||||
cyc_by_name = {c["name"]: c for c in cycles}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""set_startdates.py — Nachzug: start_date für die 21 neuen Screen-Tickets (gen_dayplan ordnet nach start_date).
|
||||
Idempotent. Default DRY-RUN, scharf: --apply"""
|
||||
import json, os, re, sys, time, urllib.request, urllib.error
|
||||
APPLY = "--apply" in sys.argv
|
||||
TOK = {}
|
||||
for line in open(os.path.expanduser("~/.cli-tokens/plane")):
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1); TOK[k.strip()] = v.strip()
|
||||
BASE = TOK["PLANE_BASE_URL"].rstrip("/"); WS = TOK["PLANE_WORKSPACE"]; KEY = TOK["PLANE_API_KEY"]
|
||||
PID = "35d0d968-8ab3-40cc-97b8-b81d917dc5df"
|
||||
API = f"{BASE}/api/v1/workspaces/{WS}/projects/{PID}"
|
||||
STARTS = {
|
||||
"D-P2":"2026-06-20","D-P4":"2026-06-20","D-P3":"2026-06-20","D-P5":"2026-06-20","D-P7":"2026-06-25",
|
||||
"D-P8":"2026-06-27","D-P14":"2026-06-27","D-P9":"2026-06-30","D-P15":"2026-06-30",
|
||||
"D-P16":"2026-07-04","D-P12":"2026-07-06","D-P20":"2026-07-08","D-P13":"2026-07-09","D-P21":"2026-07-09",
|
||||
"VD-P22":"2026-07-13","VD-P23":"2026-07-13","VD-P24":"2026-07-13","VD-P25":"2026-07-16",
|
||||
"VD-P32":"2026-07-20","VD-P28":"2026-07-27","VD-P30":"2026-07-29","VD-P31":"2026-07-29",
|
||||
}
|
||||
def http(method, path, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
r = urllib.request.Request(API + path, data=data, method=method); r.add_header("X-API-Key", KEY)
|
||||
if data: r.add_header("Content-Type", "application/json")
|
||||
for _ in range(4):
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
raw = resp.read().decode(); return resp.status, (json.loads(raw) if raw else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 429: time.sleep(5); continue
|
||||
return e.code, {}
|
||||
return 999, {}
|
||||
def get_all(path):
|
||||
out, c = [], None
|
||||
while True:
|
||||
st, d = http("GET", path + (("?per_page=100&cursor=" + c) if c else "?per_page=100"))
|
||||
if st >= 400 or isinstance(d, list): out += d if isinstance(d, list) else []; break
|
||||
out += d.get("results", [])
|
||||
if d.get("next_page_results") and d.get("next_cursor"): c = d["next_cursor"]
|
||||
else: break
|
||||
return out
|
||||
wid2 = {re.split(r'\s*·\s*', i["name"])[0].strip(): i for i in get_all("/issues/")}
|
||||
print(f"=== start_date {'(SCHARF)' if APPLY else '(DRY-RUN)'} ===")
|
||||
done = 0
|
||||
for wid, sd in STARTS.items():
|
||||
i = wid2.get(wid)
|
||||
if not i: print(f" ⚠ {wid} fehlt"); continue
|
||||
if (i.get("start_date") or "")[:10] == sd: continue
|
||||
print(f" ~ {wid}: start_date = {sd}")
|
||||
if APPLY:
|
||||
st, _ = http("PATCH", f"/issues/{i['id']}/", {"start_date": sd}); time.sleep(1.1)
|
||||
if st < 300: done += 1
|
||||
else: print(f" ✗ HTTP {st}")
|
||||
print(f"{'gesetzt: '+str(done) if APPLY else '→ scharf: --apply'}")
|
||||
@@ -1,227 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HiveGrid -> Plane Ticket-Sync (idempotent).
|
||||
Liest Zugang aus ~/.cli-tokens/plane (env-Format).
|
||||
ZIEL-PROJEKT ist HARDCODED auf HiveGrid (NICHT die PLANE_PROJECT-Variable,
|
||||
die faelschlich auf PowerLinked zeigt).
|
||||
|
||||
Phase A: Cycles (W1-W4) + 7 Module + Labels + Modul "Externals" (12 Issues).
|
||||
Phase B (spaeter): restliche Module-Issues + modueluebergreifende Relations.
|
||||
|
||||
Aufruf: python3 sync_plane.py
|
||||
Re-Run-sicher: existierende Objekte werden per Name erkannt, nicht dupliziert.
|
||||
"""
|
||||
import json, os, sys, urllib.request, urllib.error
|
||||
|
||||
# ⚠ BOOTSTRAP-GUARD (seit 2026-06-16): Plane ist LIVE + manuell gescrubbt (Umlaute, PWA-Umbau,
|
||||
# v3-Defers). Dieses Skript matcht Issues per VOLLNAMEN — ein Re-Run nach dem Scrub fände
|
||||
# umbenannte Tickets NICHT wieder und legte DUPLIKATE an. Laufende Pflege -> scrub_plane.py
|
||||
# (Working-ID-Match, idempotent). Bewusster Neu-Bootstrap nur mit --force-bootstrap.
|
||||
if "--force-bootstrap" not in sys.argv:
|
||||
raise SystemExit(
|
||||
"✋ Bootstrap gesperrt: Plane ist live + gescrubbt — Re-Run würde Duplikate erzeugen.\n"
|
||||
" Laufende Änderungen -> .planning/plane-sync/scrub_plane.py (idempotent).\n"
|
||||
" Echter Neu-Bootstrap nur bewusst mit --force-bootstrap."
|
||||
)
|
||||
|
||||
# ---- Zugang ----
|
||||
TOK = {}
|
||||
with open(os.path.expanduser("~/.cli-tokens/plane")) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1)
|
||||
TOK[k.strip()] = v.strip()
|
||||
|
||||
BASE = TOK["PLANE_BASE_URL"].rstrip("/")
|
||||
WS = TOK["PLANE_WORKSPACE"]
|
||||
KEY = TOK["PLANE_API_KEY"]
|
||||
# HARDCODED: HiveGrid-Projekt (NICHT PowerLinked!)
|
||||
PID = "35d0d968-8ab3-40cc-97b8-b81d917dc5df"
|
||||
API = f"{BASE}/api/v1/workspaces/{WS}/projects/{PID}"
|
||||
|
||||
GITEA = "https://git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud/src/branch/main"
|
||||
|
||||
MEMBERS = {
|
||||
"raffael": "8e34a88c-19c9-4565-b4f5-50415d623ce8",
|
||||
"akeshan": "8bf43e86-dc8a-4963-b57a-aeb5f2cc450a",
|
||||
"david": "6c1fc63f-133d-4f40-86e4-7f658a6edcc7",
|
||||
"dejan": "a5883f5e-3038-453c-9401-39ab1f40be9e",
|
||||
"haris": "48367c1f-20d2-44f1-8ae2-faf35aa2052a",
|
||||
}
|
||||
|
||||
def req(method, path, body=None):
|
||||
url = path if path.startswith("http") else API + path
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
r = urllib.request.Request(url, data=data, method=method)
|
||||
r.add_header("X-API-Key", KEY)
|
||||
r.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
raw = resp.read().decode()
|
||||
return resp.status, (json.loads(raw) if raw else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {"_error": e.read().decode()[:400]}
|
||||
|
||||
def get_all(path):
|
||||
"""Cursor-paginierte Liste -> alle results."""
|
||||
out, cursor = [], None
|
||||
while True:
|
||||
p = path + (("?cursor=" + cursor) if cursor else "")
|
||||
st, d = req("GET", p)
|
||||
if st >= 400:
|
||||
print(" GET FAIL", path, st, d); break
|
||||
if isinstance(d, list):
|
||||
out += d; break
|
||||
out += d.get("results", [])
|
||||
if d.get("next_page_results") and d.get("next_cursor"):
|
||||
cursor = d["next_cursor"]
|
||||
else:
|
||||
break
|
||||
return out
|
||||
|
||||
# ---- States ----
|
||||
states = {s["name"]: s["id"] for s in get_all("/states/")}
|
||||
BACKLOG = states.get("Backlog")
|
||||
print("States:", list(states.keys()), "| Backlog-id ok:", bool(BACKLOG))
|
||||
|
||||
# ---- Cycles (idempotent by name) ----
|
||||
CYCLES = [
|
||||
("W1 — Fundament & Identitaet", "2026-06-16", "2026-06-19"),
|
||||
("W2 — Plattform & CRM (Demo 1)", "2026-06-20", "2026-06-26"),
|
||||
("W3 — Kern-Kette & Kommunikation (Demo 2)", "2026-06-27", "2026-07-03"),
|
||||
("W4 — USP, Haertung & Launch", "2026-07-04", "2026-07-10"),
|
||||
]
|
||||
existing_cycles = {c["name"]: c["id"] for c in get_all("/cycles/")}
|
||||
cycle_id = {}
|
||||
for name, start, end in CYCLES:
|
||||
if name in existing_cycles:
|
||||
cycle_id[name] = existing_cycles[name]; print("Cycle vorhanden:", name)
|
||||
else:
|
||||
st, d = req("POST", "/cycles/", {"name": name, "start_date": start, "end_date": end, "project_id": PID})
|
||||
if st < 400: cycle_id[name] = d["id"]; print("Cycle angelegt:", name)
|
||||
else: print("Cycle FAIL:", name, st, d)
|
||||
W = {"W1": CYCLES[0][0], "W2": CYCLES[1][0], "W3": CYCLES[2][0], "W4": CYCLES[3][0]}
|
||||
|
||||
# ---- Module (idempotent by name) ----
|
||||
MODULES = ["Externals", "Raffael (Dev)", "Akeshan (Dev)", "David (Screens)",
|
||||
"Dejan (Website)", "Haris (Vertrieb)", "Noah (Validierung)"]
|
||||
existing_mods = {m["name"]: m["id"] for m in get_all("/modules/")}
|
||||
module_id = {}
|
||||
for name in MODULES:
|
||||
if name in existing_mods:
|
||||
module_id[name] = existing_mods[name]; print("Modul vorhanden:", name)
|
||||
else:
|
||||
st, d = req("POST", "/modules/", {"name": name})
|
||||
if st < 400: module_id[name] = d["id"]; print("Modul angelegt:", name)
|
||||
else: print("Modul FAIL:", name, st, d)
|
||||
|
||||
# ---- Labels (idempotent by name) ----
|
||||
LABELS = {
|
||||
"typ:phase": "#2563eb", "typ:screen": "#db2777", "typ:external": "#d97706",
|
||||
"typ:website": "#0891b2", "typ:vertrieb": "#16a34a", "typ:validierung": "#7c3aed",
|
||||
"gate:W1": "#9ca3af", "gate:W2": "#6b7280", "gate:W3": "#4b5563", "gate:W4": "#374151",
|
||||
}
|
||||
existing_labels = {l["name"]: l["id"] for l in get_all("/labels/")}
|
||||
label_id = {}
|
||||
for name, color in LABELS.items():
|
||||
if name in existing_labels:
|
||||
label_id[name] = existing_labels[name]
|
||||
else:
|
||||
st, d = req("POST", "/labels/", {"name": name, "color": color})
|
||||
if st < 400: label_id[name] = d["id"]; print("Label angelegt:", name)
|
||||
else: print("Label FAIL:", name, st, d)
|
||||
|
||||
# ---- Helper: Doku-HTML + Issue-Description ----
|
||||
def doku_link(path, section):
|
||||
return f'<a href="{GITEA}/{path}">{path.split("/")[-1]} — {section}</a>'
|
||||
|
||||
def desc(doku, blocked_by, blocks, owner, window, dod):
|
||||
lis = "".join(f"<li>{d}</li>" for d in dod)
|
||||
return (f"<p><b>📚 Doku</b><br/>{doku}</p>"
|
||||
f"<p><b>⛔ blocked-by:</b> {blocked_by or '—'}<br/>"
|
||||
f"<b>➡️ blocks:</b> {blocks or '—'}<br/>"
|
||||
f"<b>👤 Owner:</b> {owner} · <b>🗓 Fenster:</b> {window}</p>"
|
||||
f"<p><b>✅ Definition of Done</b></p><ul>{lis}</ul>")
|
||||
|
||||
# ---- Issue anlegen (idempotent by exact name) ----
|
||||
existing_issues = {i["name"]: i["id"] for i in get_all("/issues/")}
|
||||
def ensure_issue(title, assignees, labels, start, target, description_html):
|
||||
if title in existing_issues:
|
||||
return existing_issues[title], False
|
||||
body = {"name": title, "description_html": description_html,
|
||||
"state": BACKLOG, "assignees": [MEMBERS[a] for a in assignees],
|
||||
"labels": [label_id[l] for l in labels if l in label_id],
|
||||
"start_date": start, "target_date": target}
|
||||
st, d = req("POST", "/issues/", body)
|
||||
if st >= 400:
|
||||
print("Issue FAIL:", title, st, d); return None, False
|
||||
existing_issues[title] = d["id"]
|
||||
return d["id"], True
|
||||
|
||||
def link_issue(iid, cycle_key, module):
|
||||
"""Cycle- + Modul-Zuordnung (idempotent; Plane M2M ignoriert Duplikate)."""
|
||||
cn = W[cycle_key]
|
||||
if cn in cycle_id:
|
||||
st, d = req("POST", f"/cycles/{cycle_id[cn]}/cycle-issues/",
|
||||
{"issues": [iid], "project": PID})
|
||||
if st >= 400 and "already" not in str(d).lower():
|
||||
print(" cycle-link FAIL", st, d)
|
||||
if module in module_id:
|
||||
st, d = req("POST", f"/modules/{module_id[module]}/module-issues/",
|
||||
{"issues": [iid], "project": PID})
|
||||
if st >= 400 and "already" not in str(d).lower():
|
||||
print(" module-link FAIL", st, d)
|
||||
|
||||
EXT_DOC = ".planning/EXTERNALS-CHECKLISTE.md"
|
||||
EXTERNALS = [
|
||||
("EXT-02 · Mailjet Sende-Domain (SPF/DKIM/DMARC) + Warm-up", ["raffael"], "W1", "2026-06-16", "2026-06-16",
|
||||
"A6/B1/D1", "—", "P2 (Einladungs-Mails), P14 (Marketing-Zustellbarkeit)",
|
||||
["SPF/DKIM/DMARC gesetzt & verifiziert (Auto-Probe gruen)", "Domain-Warm-up gestartet", "AD-6 im ADR auf Accepted"]),
|
||||
("EXT-03 · Anwalt: AVV + DSGVO-Auskunft + Website-Rechtstexte", ["dejan"], "W1", "2026-06-16", "2026-06-19",
|
||||
"E6", "—", "Website-LIVE (Rechtstexte), P21 / Launch",
|
||||
["AVV-Vorlage (Art. 28) beauftragt", "DSGVO-Auskunfts-Vorlage", "Impressum/Datenschutz fuer Website"]),
|
||||
("EXT-04 · KWP: Callback-URLs melden + bnWin.net 8.1 N3", ["raffael"], "W1", "2026-06-16", "2026-06-17",
|
||||
"A1", "N-KWP (Noah/Geerkens-IT)", "v2-P10 (KWP-Integration)",
|
||||
["Beide Callback-URLs an KWP gemeldet (app + staging, PKCE)", "bnWin.net-Update 8.1 N3 bei Geerkens-IT angestossen"]),
|
||||
("EXT-05 · Noah: 4 Gate-Freitags-Slots blocken", ["raffael"], "W1", "2026-06-16", "2026-06-16",
|
||||
"E1", "—", "alle Noah-Demos (19./26.06., 03./10.07.)",
|
||||
["4 Slots terminiert", "von Noah bestaetigt"]),
|
||||
("EXT-06 · Hetzner-Projekt HiveGrid: S3+Token + DEK-Master-Key", ["raffael"], "W1", "2026-06-16", "2026-06-17",
|
||||
"B5", "—", "P4 (Storage), P5/P17 (DEK), AK-Infra-Live",
|
||||
["Hetzner-Projekt HiveGrid + S3-Bucket + Token", "DEK-Master-Key in Coolify-Secrets", "SCALEWAY_*-Env -> Hetzner-Endpoint"]),
|
||||
("EXT-07 · Stripe-Konto + Live-Verifizierung", ["raffael"], "W1", "2026-06-16", "2026-06-26",
|
||||
"A5", "—", "P20 (Billing-Go-Live)",
|
||||
["Konto + Geschaeftsdaten + Bankkonto", "Test-Mode fuer Dev aktiv", "Live-Verifizierung eingereicht"]),
|
||||
("EXT-08 · AirCall-Konto + Sandbox + Webhooks", ["raffael"], "W1", "2026-06-16", "2026-06-30",
|
||||
"B2", "—", "P15 (Telefonie)",
|
||||
["Konto + Sandbox", "API-Keys", "Webhooks konfiguriert"]),
|
||||
("EXT-09 · PandaDoc-Sandbox (Provider-Wahl)", ["raffael"], "W1", "2026-06-16", "2026-07-03",
|
||||
"B4/D4", "—", "P16 (Angebot/Signatur)",
|
||||
["Provider gewaehlt (PandaDoc vs. DocuSign)", "Sandbox + API-Key"]),
|
||||
("EXT-10 · Google-Cloud OAuth + Calendar-Scope-Antrag", ["raffael"], "W1", "2026-06-16", "2026-06-18",
|
||||
"A2", "—", "P16 (Kalender, nur Google)",
|
||||
["OAuth-App + nur Calendar-Scope", "Verifizierung beantragt", "Testmodus-Bruecke mit Workspace-Konten"]),
|
||||
("EXT-11 · Mistral API-Key", ["raffael"], "W1", "2026-06-16", "2026-06-18",
|
||||
"B6", "—", "P13 (KI-Layer)",
|
||||
["La-Plateforme-Key in Coolify-Secrets"]),
|
||||
("EXT-12 · Geo-IP + HIBP pruefen/anbinden", ["raffael"], "W1", "2026-06-16", "2026-06-19",
|
||||
"B8/B9", "—", "P2 (Session-Standort + Breach-Check)",
|
||||
["Geo-IP EU-Konformitaet geprueft (oder stub)", "HIBP k-Anonymity angebunden"]),
|
||||
("EXT-13 · v2-Vorlauf-Externals (Play/FCM/Gmail-Scope/Entra)", ["raffael"], "W3", "2026-06-29", "2026-06-29",
|
||||
"v2-Vorlauf", "—", "v2 (Mobile-App, Postfach-Sync)",
|
||||
["Google Play Console", "FCM/APNs-Zertifikate", "Gmail-Restricted-Scope-Antrag (Wochen Vorlauf!)", "MS-Entra-App"]),
|
||||
]
|
||||
|
||||
print("\n--- Externals-Issues ---")
|
||||
for title, asg, cyc, start, target, sec, bby, blk, dod in EXTERNALS:
|
||||
html = desc(doku_link(EXT_DOC, sec), bby, blk,
|
||||
", ".join(asg), f"{cyc} (faellig {target})", dod)
|
||||
labels = ["typ:external", f"gate:{cyc}"]
|
||||
iid, created = ensure_issue(title, asg, labels, start, target, html)
|
||||
if iid:
|
||||
link_issue(iid, cyc, "Externals")
|
||||
print(("angelegt:" if created else "verknuepft:"), title)
|
||||
|
||||
print("\nFERTIG (Phase A). Projekt-URL:",
|
||||
f"{BASE}/{WS}/projects/{PID}/issues/")
|
||||
@@ -1,407 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HiveGrid -> Plane Ticket-Sync PHASE B (idempotent).
|
||||
Legt die restlichen Issues (Raffael/Akeshan/David/Dejan/Haris/Noah + R-REFIT + EXT-14)
|
||||
an und verdrahtet das JIT-Netz als klickbare Issue-Links (blocked-by).
|
||||
Voraussetzung: Phase A (sync_plane.py) lief -> Module/Cycles/Labels/Externals existieren.
|
||||
Re-Run-sicher: Issues per Name, Links per Titel-Prefix erkannt.
|
||||
"""
|
||||
import json, os, sys, time, urllib.request, urllib.error
|
||||
|
||||
# ⚠ BOOTSTRAP-GUARD (seit 2026-06-16): Plane ist LIVE + manuell gescrubbt (Umlaute, PWA-Umbau,
|
||||
# v3-Defers). Dieses Skript matcht Issues per VOLLNAMEN — ein Re-Run nach dem Scrub fände
|
||||
# umbenannte Tickets NICHT wieder und legte DUPLIKATE an. Laufende Pflege -> scrub_plane.py
|
||||
# (Working-ID-Match, idempotent). Bewusster Neu-Bootstrap nur mit --force-bootstrap.
|
||||
if "--force-bootstrap" not in sys.argv:
|
||||
raise SystemExit(
|
||||
"✋ Bootstrap gesperrt: Plane ist live + gescrubbt — Re-Run würde Duplikate erzeugen.\n"
|
||||
" Laufende Änderungen -> .planning/plane-sync/scrub_plane.py (idempotent).\n"
|
||||
" Echter Neu-Bootstrap nur bewusst mit --force-bootstrap."
|
||||
)
|
||||
|
||||
TOK = {}
|
||||
for line in open(os.path.expanduser("~/.cli-tokens/plane")):
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1); TOK[k.strip()] = v.strip()
|
||||
BASE = TOK["PLANE_BASE_URL"].rstrip("/"); WS = TOK["PLANE_WORKSPACE"]; KEY = TOK["PLANE_API_KEY"]
|
||||
PID = "35d0d968-8ab3-40cc-97b8-b81d917dc5df"
|
||||
API = f"{BASE}/api/v1/workspaces/{WS}/projects/{PID}"
|
||||
GITEA = "https://git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud/src/branch/main"
|
||||
M = {"raffael":"8e34a88c-19c9-4565-b4f5-50415d623ce8","akeshan":"8bf43e86-dc8a-4963-b57a-aeb5f2cc450a",
|
||||
"david":"6c1fc63f-133d-4f40-86e4-7f658a6edcc7","dejan":"a5883f5e-3038-453c-9401-39ab1f40be9e",
|
||||
"haris":"48367c1f-20d2-44f1-8ae2-faf35aa2052a"}
|
||||
|
||||
def _once(method, url, body):
|
||||
r = urllib.request.Request(url, data=json.dumps(body).encode() if body is not None else None, method=method)
|
||||
r.add_header("X-API-Key", KEY); r.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
raw = resp.read().decode(); return resp.status, (json.loads(raw) if raw else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {"_error": e.read().decode()[:300]}
|
||||
|
||||
def req(method, path, body=None):
|
||||
"""Mit 429-Retry (Backoff) + Throttle, um Plane-Rate-Limit (60/min) zu respektieren."""
|
||||
url = path if path.startswith("http") else API + path
|
||||
st, d = None, None
|
||||
for _ in range(8):
|
||||
st, d = _once(method, url, body)
|
||||
if st == 429:
|
||||
time.sleep(25); continue
|
||||
time.sleep(1.1) # Throttle: < 60 Requests/min
|
||||
return st, d
|
||||
return st, d
|
||||
|
||||
def get_all(path):
|
||||
out, cursor = [], None
|
||||
while True:
|
||||
st, d = req("GET", path + (("?cursor=" + cursor) if cursor else ""))
|
||||
if st >= 400: print("GET FAIL", path, st, d); break
|
||||
if isinstance(d, list): out += d; break
|
||||
out += d.get("results", [])
|
||||
if d.get("next_page_results") and d.get("next_cursor"): cursor = d["next_cursor"]
|
||||
else: break
|
||||
return out
|
||||
|
||||
states = {s["name"]: s["id"] for s in get_all("/states/")}
|
||||
BACKLOG = states["Backlog"]
|
||||
cyc = {c["name"]: c["id"] for c in get_all("/cycles/")}
|
||||
mod = {m["name"]: m["id"] for m in get_all("/modules/")}
|
||||
lab = {l["name"]: l["id"] for l in get_all("/labels/")}
|
||||
CY = {"W1":"W1 — Fundament & Identitaet","W2":"W2 — Plattform & CRM (Demo 1)",
|
||||
"W3":"W3 — Kern-Kette & Kommunikation (Demo 2)","W4":"W4 — USP, Haertung & Launch"}
|
||||
|
||||
# Doku-Pfade
|
||||
RM,PS,SM,EM,PB,TL,EX = (".planning/ROADMAP.md",".planning/PHASE-SOURCES.md",".planning/SCREEN-MAP.md",
|
||||
".planning/ENTITY-MODEL.md",".planning/DESIGNER-FIRST-PLAYBOOK.md",".planning/TIMELINE.md",".planning/EXTERNALS-CHECKLISTE.md")
|
||||
AD,IC = "docs/ARCHITECTURE-DECISIONS.md","docs/INTEGRATION-CONTRACTS.md"
|
||||
def sp(x): return f"_bmad-output/specs/{x}"
|
||||
def ho(p): return f".planning/handouts/{p}"
|
||||
|
||||
def link(path, sec): return f'<a href="{GITEA}/{path}">{path.split("/")[-1]} — {sec}</a>'
|
||||
def desc(doks, bby, blk, owner, win, dod):
|
||||
dl = "<br/>".join(link(p, s) for p, s in doks)
|
||||
li = "".join(f"<li>{d}</li>" for d in dod)
|
||||
return (f"<p><b>📚 Doku</b><br/>{dl}</p>"
|
||||
f"<p><b>⛔ blocked-by:</b> {bby or '—'}<br/><b>➡️ blocks:</b> {blk or '—'}<br/>"
|
||||
f"<b>👤 Owner:</b> {owner} · <b>🗓 Fenster:</b> {win}</p>"
|
||||
f"<p><b>✅ Definition of Done</b></p><ul>{li}</ul>")
|
||||
|
||||
# Issue-Definition: (wid, title, [assignees], cycle, modul, typ, start, target, [doku], [blocked_by], blocks_text, [dod])
|
||||
def I(wid,t,a,c,mo,ty,s,tg,dk,bb,bl,dod): return (wid,t,a,c,mo,ty,s,tg,dk,bb,bl,dod)
|
||||
RA,AK,DA,DJ,HA,NO = "Raffael (Dev)","Akeshan (Dev)","David (Screens)","Dejan (Website)","Haris (Vertrieb)","Noah (Validierung)"
|
||||
|
||||
ISSUES = [
|
||||
# ---- RAFFAEL ----
|
||||
I("R-REFIT","R-REFIT · ER-Modell-Re-Fit P6–P9 (Requirements/Screen-Map/Phase-Sources)",["raffael"],"W1",RA,"typ:phase","2026-06-16","2026-06-21",
|
||||
[(EM,"gelocktes Modell"),(PS,"Phase 6–9 Pivot-Note"),(SM,"§2–§4")],[],"R-P6, R-P7, R-P9a, D-CRM, D-KANBAN, D-ABN",
|
||||
["REQUIREMENTS P6–9 gegen ENTITY-MODEL nachgezogen","SCREEN-MAP §2–§4 finalisiert (Lead→Projekt, ein Kanban, Rechnung-Sub-Entity)","PHASE-SOURCES P6–9 re-mapped"]),
|
||||
I("R-P1a","R-P1a · P1 EU-Sterilisation + Contracts + URL-Schemata",["raffael"],"W1",RA,"typ:phase","2026-06-16","2026-06-17",
|
||||
[(RM,"Phase 1"),(IC,"URL-Schemata"),(EX,"D3 GA/ConvertKit raus")],[],"R-P1b + gesamtes Fundament",
|
||||
["GitHub/Google-OAuth-Reste + githubId/googleId raus","GA + ConvertKit entfernt (D3)","URL-Schemata (App/Admin/Portal/Forms/Booking/API) dokumentiert"]),
|
||||
I("R-P1b","R-P1b · P1 Service-Kerne (Audit/notify/i18n/Flags) + Konnektor-Muster",["raffael"],"W1",RA,"typ:phase","2026-06-17","2026-06-18",
|
||||
[(RM,"Phase 1"),(sp("M21-audit-log.md"),"Audit"),(sp("M32-feature-flags.md"),"Flags")],["R-P1a"],"R-P2a, AK-P3, alle Module (Konnektor)",
|
||||
["AuditEvent SHA-256-Hash-Kette (default-on)","notify() Kanal-Abstraktion (In-App/Email, Push-Stub)","packages/i18n + verify:i18n","tenantHasFlag() + tenantHasModule()-Muster am Skeleton"]),
|
||||
I("R-P2a","R-P2a · P2 Mandant + Auth (Email/PW/HIBP) + Sessions + TOTP",["raffael"],"W1",RA,"typ:phase","2026-06-18","2026-06-19",
|
||||
[(RM,"Phase 2"),(sp("M27-auth-multi-tenancy.md"),"Auth")],["R-P1b","EXT-12"],"R-P2b",
|
||||
["Self-Service-Signup + Auto-Slug (Reserved-Words)","Login Email/PW + HIBP + Brute-Force","TOTP-2FA","Sessions Postgres-SoT + Redis-Cache (AD-4)"]),
|
||||
I("R-P2b","R-P2b · P2 RLS-Härtung (Gate-Beweis) + RBAC + User-Lifecycle",["raffael"],"W1",RA,"typ:phase","2026-06-19","2026-06-19",
|
||||
[(RM,"Phase 2"),(AD,"AD-1 RLS")],["R-P2a"],"Gate W1 + alle Tenant-Daten-Module",
|
||||
["RLS USING/WITH CHECK, SET LOCAL app.tenant_id, App-Rolle ohne BYPASSRLS","Cross-Tenant-Zugriff nachweisbar verhindert","6 SHK-Rollen geseedet","Einladung 48h / Deaktivierung / PII-Löschung"]),
|
||||
I("R-P5","R-P5 · P5 Apps-Engine & Integrations-Framework",["raffael"],"W2",RA,"typ:phase","2026-06-20","2026-06-21",
|
||||
[(RM,"Phase 5"),(sp("M33a-apps-engine.md"),"Apps-Engine")],["R-P2b"],"R-P6, AK-P14a, AK-P15a, AK-P16a, EXT-08/09/10",
|
||||
["DataConnection OAuth2 + API-Key-Connect","type-safe Adapter-Registry, AppSyncRun/Item","Connection-Health, OAuth-Tokens per-Tenant-DEK (AD-8)"]),
|
||||
I("R-P6","R-P6 · P6 CRM-Kern",["raffael"],"W2",RA,"typ:phase","2026-06-22","2026-06-24",
|
||||
[(RM,"Phase 6"),(sp("M01-crm-kern.md"),"CRM"),(EM,"Kontaktmodell")],["R-P5","AK-P4b","R-REFIT"],"R-P7, AK-P14a, AK-P15a",
|
||||
["Entitäten typisiert + RLS, Entity-Builder raus (AD-2)","Listen filter/sort/paginate/bulk/CSV serverseitig","Detail Inline-Edit + Activity-Stream + Custom-Felder","Live-Duplikat-Check + Lead→Projekt (AD-20)","Konnektor tenantHasModule('crm')"]),
|
||||
I("R-P7","R-P7 · P7 Lead-Inbox",["raffael"],"W2",RA,"typ:phase","2026-06-25","2026-06-26",
|
||||
[(RM,"Phase 7"),(sp("M02-lead-inbox.md"),"Lead-Inbox")],["R-P6","R-REFIT"],"R-P8a, AK-P16c (Form→Lead), Demo 1",
|
||||
["Triage-Liste + Quick-Actions + Bulk + Realtime","Multi-Quellen-Auto-Anlage (Form/Email/AirCall)","Status-Workflow + DSGVO-Auto-Lösch-Cron","Anti-Spam/Quarantäne","lead.qualified-Event"]),
|
||||
I("R-P8a","R-P8a · P8 Pipeline-Engine + Deal-CRUD + Auto-Verkaufsfall",["raffael"],"W3",RA,"typ:phase","2026-06-27","2026-06-28",
|
||||
[(RM,"Phase 8"),(sp("M03-sales-pipeline.md"),"Sales"),(EM,"Projekt-Kern")],["R-P7"],"R-P8b",
|
||||
["Pipeline/Stage/Deal + HiveNumberSeq, SHK-Pipelines geseedet","M049: lead.qualified → Auto-Verkaufsfall","Lost-Reason-CRUD"]),
|
||||
I("R-P8b","R-P8b · P8 Konfigurierbarer Kanban + Forecast/Reporting",["raffael"],"W3",RA,"typ:phase","2026-06-28","2026-06-29",
|
||||
[(RM,"Phase 8"),(sp("M03-sales-pipeline.md"),"Kanban/KPIs")],["R-P8a"],"R-P9a (deal.won), R-P11b (Aggregate)",
|
||||
["Kanban D&D + Stage-Validation, SSE-Realtime","Forecast/KPIs/Stage-Conversion mit Aggregat-Cache","Field-Level-Permissions (Volumen)","deal.won-Event"]),
|
||||
I("R-P9a","R-P9a · P9 Projekt-Pipeline + Sub-Areas + Rechnung (Sub-Entity v1)",["raffael"],"W3",RA,"typ:phase","2026-06-30","2026-07-01",
|
||||
[(RM,"Phase 9"),(sp("M04-auftragspipeline.md"),"Auftrag"),(EM,"Projekt + Sub-Areas")],["R-P8b","R-REFIT"],"R-P9b",
|
||||
["Auftrags-Welt = Pipeline-View auf Projekt-Kern (kein Won-Objekt)","Sub-Areas: Termine/Rechnungen/Checklisten/Tasks/Doku","Rechnung = neues Sub-Entity v1"]),
|
||||
I("R-P9b","R-P9b · P9 Plantafel (offline) + Abnahme-Protokoll (offline)",["raffael"],"W3",RA,"typ:phase","2026-07-01","2026-07-02",
|
||||
[(RM,"Phase 9"),(sp("M04-auftragspipeline.md"),"Plantafel/Abnahme")],["R-P9a","AK-P17b"],"Demo 2, R-P11a",
|
||||
["Plantafel pro Monteur/Tag, offline-fähig, HR-Abwesenheiten sichtbar","Abnahme-Protokoll Touch-Signatur, offline + Sync","PDF + Auto-Email"]),
|
||||
I("R-P11a","R-P11a · P11 Globale Suche (Meilisearch, Cmd+K)",["raffael"],"W4",RA,"typ:phase","2026-07-03","2026-07-04",
|
||||
[(RM,"Phase 11"),(sp("M24-globale-suche.md"),"Suche")],["R-P9b","AK-INFRA2"],"R-P11b",
|
||||
["Meilisearch permission-aware via SearchAdapter","Outbox-Index-Pattern, SearchIndexEntry/RecentSearch","Cmd+K debounced + Vollbild-View"]),
|
||||
I("R-P11b","R-P11b · P11 Dashboard (3 Rollen-Layouts)",["raffael"],"W4",RA,"typ:phase","2026-07-04","2026-07-05",
|
||||
[(RM,"Phase 11"),(sp("M20-dashboard.md"),"Dashboard")],["R-P11a","R-P8b"],"R-P12a",
|
||||
["3 feste Rollen-Layouts (GF/Büro/Monteur) geseedet","Widgets aus Sales/Projekte/Tasks/HR/KI","Live-Update WebSocket/SSE + Aggregat-Cache"]),
|
||||
I("R-P12a","R-P12a · P12 Automation-Engine + Trigger/Action-Registry + Guards",["raffael"],"W4",RA,"typ:phase","2026-07-06","2026-07-07",
|
||||
[(RM,"Phase 12"),(sp("M17-automation-engine.md"),"Automation")],["R-P11b"],"R-P12b, R-P13 (KI-Actions)",
|
||||
["Trigger-Library (CRM/Sales/Projekte/Email/Forms/Kalender/Zeit/Webhook)","Action-Library erweiterbar (KWP/KI docken nach)","Loop-Guard/Rate-Limit/Idempotenz/Metering","AutomationRule/Execution/StepExecution"]),
|
||||
I("R-P12b","R-P12b · P12 Visueller Flow-Editor + Dry-Run + 24h-Undo + Sprach-Andock",["raffael"],"W4",RA,"typ:phase","2026-07-07","2026-07-08",
|
||||
[(RM,"Phase 12"),(sp("M17-automation-engine.md"),"Flow-Editor")],["R-P12a","D-FLOW"],"R-P13",
|
||||
["Graph-Editor aus Trigger/Action-Library","Dry-Run-Vorschau + Bestätigungs-Pflicht","24h-Undo-Fenster","Sprach-zu-Automation-Andock (nie Auto-Aktivierung)"]),
|
||||
I("R-P13","R-P13 · P13 KI-Layer (Mistral-Gateway)",["raffael"],"W4",RA,"typ:phase","2026-07-09","2026-07-09",
|
||||
[(RM,"Phase 13"),(sp("M18-ki-layer.md"),"KI")],["R-P12b","EXT-11"],"FREEZE Do 09.07.",
|
||||
["EU-LLM-Gateway Mistral + AiCredits-Konto + harte Limits","Multi-Provider-Fallback-Interface","Promptfoo-Suite (Injection)","Sprach-zu-Automation exklusiv im Automation-Kontext"]),
|
||||
# ---- AKESHAN ----
|
||||
I("AK-P1c","AK-P1c · P1 Hot-Swap-Maschine + Walking-Skeleton + PWA",["akeshan"],"W1",AK,"typ:phase","2026-06-16","2026-06-17",
|
||||
[(RM,"Phase 1"),(PB,"§1 Fundamente")],[],"alle Screen-Andock, Gate W1",
|
||||
["VM-Konvention + Action-Callback + Subframe-Adapter","ui-mock + Visual-Regression-Gate + Hot-Swap-Drill","Walking-Skeleton-Route + PWA-Manifest"]),
|
||||
I("AK-INFRA1","AK-INFRA1 · Infra-Live: Staging + Coolify-Apps + PG/Redis/PgBouncer",["akeshan"],"W1",AK,"typ:external","2026-06-16","2026-06-17",
|
||||
[(EX,"C3/C4")],["EXT-06"],"CI/Gates, alle Deploys",
|
||||
["staging-app/api auf staging-1 (öffentlich + gehärtet, noindex, HMAC)","Coolify-managed Postgres/Redis + PgBouncer"]),
|
||||
I("AK-INFRA2","AK-INFRA2 · DNS + Wildcard-TLS + zentrale Dienste anbinden",["akeshan"],"W1",AK,"typ:external","2026-06-17","2026-06-18",
|
||||
[(EX,"C1/C2/C5")],["AK-INFRA1"],"R-P2a (mCaptcha), AK-P4a (ClamAV), R-P11a (Meili)",
|
||||
["Cloudflare *.hivegrid.de Wildcard-TLS + admin/staging-Subdomains","Meili-scoped-Key + mCaptcha + ClamAV per ENV anbinden"]),
|
||||
I("AK-P4a","AK-P4a · P4 Files (chunked Upload, HEIC→JPG, ClamAV)",["akeshan"],"W1",AK,"typ:phase","2026-06-18","2026-06-19",
|
||||
[(RM,"Phase 4"),(sp("M23-files.md"),"Files")],["AK-INFRA2","EXT-06"],"AK-P4b, R-P6 (File-Links)",
|
||||
["chunked Upload (TUS) + RLS auf Storage-Pfaden (Hetzner-S3)","Preview PDF→PNG / Bild→WebP / HEIC→JPG (Pflicht!)","ClamAV-Scan, Permissions/Sharing, FileEntityLink"]),
|
||||
I("AK-P4b","AK-P4b · P4 Tasks + polymorphe EntityLink-Tabelle",["akeshan"],"W1",AK,"typ:phase","2026-06-19","2026-06-19",
|
||||
[(RM,"Phase 4"),(sp("M26-tasks.md"),"Tasks")],["AK-P4a"],"R-P6, Gate W1 (EntityLink)",
|
||||
["Task-CRUD: Status/Priorität/Fälligkeit/Entity-Link/Watcher/Reminder","30-Tage-Auto-Archiv-Cron","polymorphe EntityLink-Tabelle (entity_type+entity_id)"]),
|
||||
I("AK-P3","AK-P3 · P3 Querschnitts-Oberflächen (Audit/Notif/Sprache DE)",["akeshan"],"W2",AK,"typ:phase","2026-06-20","2026-06-21",
|
||||
[(RM,"Phase 3"),(sp("M21-audit-log.md"),"Audit-UI"),(sp("M22-notifications.md"),"Notif-UI")],["R-P1b","R-P2b"],"—",
|
||||
["Audit-Viewer permission-gegated + NDJSON/CSV-Export","Notification-Center + Kategorie-Toggles pro Kanal","Sprach-Settings DE (i18n-ready)"]),
|
||||
I("AK-P19","AK-P19 · P19 Plattform-Admin (admin.hivegrid.de)",["akeshan"],"W2",AK,"typ:phase","2026-06-22","2026-06-24",
|
||||
[(RM,"Phase 19"),(sp("M37-plattform-admin.md"),"Admin")],["R-P2b","AK-P3"],"AK-P20, AK-DEMO1",
|
||||
["Host-Routing admin.hivegrid.de (NICHT /admin-Pfad!) + Cookie-Domain-Trennung","TOTP-Pflicht, Admin ohne Tenant-Workspace","Tenant-Verwaltung + Abo-Zusammenstellung (Konnektoren+Stripe)","Queue-Monitor + Support-Inbox + AdminAuditLog"]),
|
||||
I("AK-P17a","AK-P17a · P17 HR-Kern (Stammdaten, Doku, Passwort-Safe)",["akeshan"],"W2",AK,"typ:phase","2026-06-25","2026-06-25",
|
||||
[(RM,"Phase 17"),(sp("M14-hr-hub.md"),"HR")],["AK-P4b"],"AK-P17b",
|
||||
["HR-Profile/Dokumente AES-256-GCM (KMS-Envelope, AD-8)","verschlüsselter Passwort-Safe mit Audit-Trail + Reveal"]),
|
||||
I("AK-P17b","AK-P17b · P17 HR Urlaub/Krank + Surveys + Self-Service",["akeshan"],"W2",AK,"typ:phase","2026-06-26","2026-06-26",
|
||||
[(RM,"Phase 17"),(sp("M14-hr-hub.md"),"Urlaub/Krank")],["AK-P17a"],"R-P9b (Abwesenheiten→Plantafel)",
|
||||
["mehrstufiger Urlaubs-Approval-Workflow + Abwesenheits-Kalender","Krankmeldung + AU-Upload (verschlüsselt)","Surveys, Mitarbeiter-Self-Service (Web/PWA)"]),
|
||||
I("AK-DEMO1","AK-DEMO1 · Demo-Tenant + SHK-Demo-Daten für Noah-Demo 1",["akeshan"],"W2",AK,"typ:external","2026-06-26","2026-06-26",
|
||||
[(ho("akeshan/HANDOUT-AKESHAN.md"),"Demo-Setup")],["AK-P19","N-DATEN"],"N-DEMO1, H-DEMO",
|
||||
["Demo-Tenant via Admin angelegt","SHK-Seeds eingespielt (echte Geerkens-Beispieldaten)","klickbarer CRM-Stand für Noah"]),
|
||||
I("AK-P14a","AK-P14a · P14 Sende-Domain-Verifikation + BCC-Brücke + Tracking",["akeshan"],"W3",AK,"typ:phase","2026-06-27","2026-06-28",
|
||||
[(RM,"Phase 14"),(sp("M09-email-inbox.md"),"Sende-Domain/Inbound")],["EXT-02","R-P6"],"AK-P14b",
|
||||
["Sende-Domain DKIM/SPF/DMARC + Auto-Probe","BCC-Logging-Brücke (Mail am Kontakt, kein OAuth)","Email-Tracking + Delivery-Status + Bounces"]),
|
||||
I("AK-P14b","AK-P14b · P14 Email-Marketing (Kampagnen + A/B)",["akeshan"],"W3",AK,"typ:phase","2026-06-28","2026-06-29",
|
||||
[(RM,"Phase 14"),(sp("M10-email-marketing.md"),"Marketing")],["AK-P14a"],"—",
|
||||
["Block-Editor + Segmente + Versand-Engine (BullMQ, Rate-Limit)","Double-Opt-In + Suppression + One-Click-Unsubscribe","Reporting + A/B-Varianten"]),
|
||||
I("AK-P15a","AK-P15a · P15 Telefonie (AirCall)",["akeshan"],"W3",AK,"typ:phase","2026-06-30","2026-07-01",
|
||||
[(RM,"Phase 15"),(sp("M12-telefonie.md"),"Telefonie")],["EXT-08","R-P5","R-P6"],"AK-P15b",
|
||||
["AirCall-Adapter (Apps-Engine, OAuth verschlüsselt)","Click-to-Call + Inbound, unbekannte Nr → Lead","Anruf-Events als Automation-Trigger, Voicemail/Notizen"]),
|
||||
I("AK-P15b","AK-P15b · P15 Team-Chat (Realtime)",["akeshan"],"W3",AK,"typ:phase","2026-07-01","2026-07-02",
|
||||
[(RM,"Phase 15"),(sp("M13-team-chat.md"),"Chat")],["AK-P15a","AK-P4a"],"Demo 2",
|
||||
["WebSocket + Presence/Typing/Read-Receipts (Redis-Pub/Sub, AD-9)","3 Channel-Typen (public/private/entity-linked)","@Mentions + File-Uploads + Threads/Reactions/Pins + Suche"]),
|
||||
I("AK-P16a","AK-P16a · P16 Kalender-Sync (Google)",["akeshan"],"W4",AK,"typ:phase","2026-07-03","2026-07-04",
|
||||
[(RM,"Phase 16"),(sp("M25-calendar-sync.md"),"Kalender")],["EXT-10","R-P5"],"AK-P16b",
|
||||
["bidirektionaler Google-Sync (Auto-Token-Refresh, Webhooks)","HiveGrid-Termine + Konflikt-Log (Outlook → v2)"]),
|
||||
I("AK-P16b","AK-P16b · P16 Booking (Slots + Verfügbarkeit + Storno)",["akeshan"],"W4",AK,"typ:phase","2026-07-04","2026-07-05",
|
||||
[(RM,"Phase 16"),(sp("M06-booking.md"),"Booking")],["AK-P16a"],"—",
|
||||
["öffentliche Booking-Seiten, Slots timezone-aware","Buchung → Lead + Activity + Bestätigungs-Email (iFrame)","Storno/Verschiebung/Re-Assign/No-Show (M129 voll)"]),
|
||||
I("AK-P16c","AK-P16c · P16 Form-Builder (Editor + Submission + Actions)",["akeshan"],"W4",AK,"typ:phase","2026-07-05","2026-07-06",
|
||||
[(RM,"Phase 16"),(sp("M05-form-builder.md"),"Forms")],["AK-INFRA2","R-P7"],"—",
|
||||
["12 Feldtypen + Conditional-Logic + SHK-Templates","Embed URL/QR/iFrame","Post-Submit-Actions (Lead/Deal/Task/Webhook/Email/Automation) + ActionLog","Rate-Limit + mCaptcha + DSGVO-Pflicht"]),
|
||||
I("AK-P16d","AK-P16d · P16 Angebot/Signatur (PandaDoc)",["akeshan"],"W4",AK,"typ:phase","2026-07-06","2026-07-07",
|
||||
[(RM,"Phase 16"),(sp("M07-angebot-signatur.md"),"Signatur")],["EXT-09"],"—",
|
||||
["PDF-Upload → Multi-Signer via PandaDoc (Apps-Engine)","Webhook-Verarbeitung + Status-Tracking pro Unterzeichner"]),
|
||||
I("AK-P20","AK-P20 · P20 Billing (Stripe Abo-Baukasten) + Onboarding + Branding",["akeshan"],"W4",AK,"typ:phase","2026-07-08","2026-07-08",
|
||||
[(RM,"Phase 20"),(sp("M28-subscriptions-billing.md"),"Billing")],["EXT-07","AK-P19"],"AK-P21",
|
||||
["Stripe Abo-Baukasten: Basis/Nutzer + Module als Positionen (KEINE Seat-Typen!)","Trial + Dunning + Frozen-Modus (Read-Only-Middleware)","Auto-SHK-Provisionierung + Demo-Daten-Knopf","Hell/Dunkel-Toggle (SSR-Cookie)"]),
|
||||
I("AK-P21","AK-P21 · P21 DSGVO (Export + Lösch-Workflow) + Feedback-Widget",["akeshan"],"W4",AK,"typ:phase","2026-07-09","2026-07-09",
|
||||
[(RM,"Phase 21"),(sp("M35-dsgvo-export.md"),"DSGVO")],["AK-P20"],"FREEZE",
|
||||
["DSGVO-ZIP-Export (72h-signed-URL)","Tenant-Löschung: Crypto-Shredding + Stripe-Cancel + Storage-Cleanup","Feedback-Widget → Admin-Support-Inbox + Minimal-FAQ"]),
|
||||
# ---- DAVID ----
|
||||
I("D-WEB","D-WEB · Website-Screens + Brand-Tokens",["david"],"W1",DA,"typ:screen","2026-06-16","2026-06-16",
|
||||
[(SM,"Website"),(ho("david/HANDOUT-DAVID.md"),"Website")],[],"J-STRUKTUR, H-PITCH (Tokens)",
|
||||
["Landing/Features/Pricing-Teaser/Demo-Formular","Brand-/Design-Tokens (Web + App)"]),
|
||||
I("D-D0a","D-D0a · D0 Design-System + App-Shell (A1)",["david"],"W1",DA,"typ:screen","2026-06-16","2026-06-18",
|
||||
[(SM,"§0.1 App-Shell"),(PB,"§1")],[],"ALLE Screens, H-PITCH",
|
||||
["Tokens (Farben/Typo/Spacing)","Sidebar (Konnektor-Gating) + Topbar (Cmd+K, Bell, Tenant-Switch) + Breadcrumb","Status-Banner-System + Toast"]),
|
||||
I("D-D0b","D-D0b · D0 Archetypen A2–A7 + Zustands-Kit + Primitives",["david"],"W1",DA,"typ:screen","2026-06-18","2026-06-20",
|
||||
[(SM,"§0.2/0.3"),(PB,"§2 Archetypen")],["D-D0a"],"alle Compose-Screens",
|
||||
["A2 List · A3 Detail · A4 Form (12 Typen) · A5 Modal/Wizard · A6 Settings/Connection · A7 Zustände","Primitives (Buttons/Badges/KPI/Charts/Signatur-Feld)"]),
|
||||
I("D-FLOW","D-FLOW · Flow-/Node-Editor (Bespoke, Langläufer ab 18.06.)",["david"],"W3",DA,"typ:screen","2026-06-18","2026-07-03",
|
||||
[(SM,"§5 Flow-Editor"),(PB,"§3 Rang 1")],["D-D0b"],"R-P12b",
|
||||
["Graph-Canvas + Knoten-Katalog nach Modul","Dry-Run-Vorschau + Bestätigung","Sprach-Eingabe-Feld + Loop-Guard-Warnung"]),
|
||||
I("D-CRM","D-CRM · CRM-Cluster-Screens",["david"],"W2",DA,"typ:screen","2026-06-22","2026-06-24",
|
||||
[(SM,"§2"),(EM,"Kontaktmodell")],["D-D0b","R-REFIT"],"N-DEMO1 (Politur)",
|
||||
["Kontakt/Firma Liste + Detail (A3-Muster)","Lead-Inbox + Lead→Projekt","Custom-Felder + Tags + Duplikat-Tool","§2 gegen ENTITY-MODEL finalisiert"]),
|
||||
I("D-KANBAN","D-KANBAN · Pipeline-Kanban (Bespoke, konfigurierbar)",["david"],"W2",DA,"typ:screen","2026-06-24","2026-06-26",
|
||||
[(SM,"§3 Kanban"),(EM,"Pipeline=View")],["D-D0b","R-REFIT"],"—",
|
||||
["EIN konfigurierbares Board (Deal/Auftrag = Pipeline-Typen)","Won/Lost-Drop-Dialoge","Volumen permission-gegated"]),
|
||||
I("D-FORM","D-FORM · Form-Builder-Canvas + öffentliches Formular (Bespoke)",["david"],"W2",DA,"typ:screen","2026-06-24","2026-06-26",
|
||||
[(SM,"§3 Form-Builder"),(PB,"§3 Rang 2")],["D-D0b"],"—",
|
||||
["Feld-Palette + Canvas D&D + Conditional-Logic-Editor","öffentliches Formular (mCaptcha, Tenant-Logo)"]),
|
||||
I("D-KAMP","D-KAMP · Kampagnen-Editor + Segmente (Bespoke)",["david"],"W2",DA,"typ:screen","2026-06-25","2026-06-26",
|
||||
[(SM,"§3 Kampagnen")],["D-D0b"],"—",
|
||||
["Multi-Step (Basis→Inhalt→Empfänger→Test→Zeit→Bestätigung)","Ausschluss-Aufschlüsselung + Pflicht-Footer","Klick-Map + A/B-Vergleich + Segment-Builder"]),
|
||||
I("D-HR","D-HR · HR-Compose-Screens",["david"],"W2",DA,"typ:screen","2026-06-25","2026-06-26",
|
||||
[(SM,"§3 HR")],["D-D0b"],"—",
|
||||
["Hub-Start + Announcements","Urlaub/Krank + Payslips + Passwort-Safe (Reveal) + Surveys"]),
|
||||
I("D-ADMIN","D-ADMIN · Plattform-Admin-Compose",["david"],"W2",DA,"typ:screen","2026-06-26","2026-06-26",
|
||||
[(SM,"§3 Plattform-Admin")],["D-D0b"],"—",
|
||||
["Admin-Dashboard (Tenants/User/MRR) + Tenant-Liste/Detail","Abo-Zusammenstellung + Feature-Flags + Health/Queues + Support-Inbox"]),
|
||||
I("D-PLAN","D-PLAN · Plantafel (Bespoke-Mobile, offline)",["david"],"W3",DA,"typ:screen","2026-06-29","2026-07-01",
|
||||
[(SM,"§4 Plantafel"),(PB,"§3 Rang 3")],["D-D0b"],"—",
|
||||
["Desktop-Grid (Mitarbeiter×Zeit) D&D + Konflikt-Indikatoren","Monteur-Mobil-Modus 'Meine Termine heute' offline"]),
|
||||
I("D-ABN","D-ABN · Abnahme-Workflow (Bespoke-Mobile)",["david"],"W3",DA,"typ:screen","2026-06-29","2026-07-01",
|
||||
[(SM,"§4 Abnahme")],["D-D0b","R-REFIT"],"—",
|
||||
["SHK-Abnahme-Formular + Mängel-Repeater mit Foto","Touch-Unterschriften Kunde+Monteur, offline + Sync"]),
|
||||
I("D-CHAT","D-CHAT · Chat-UI (Bespoke)",["david"],"W3",DA,"typ:screen","2026-06-30","2026-07-01",
|
||||
[(SM,"§4 Chat")],["D-D0b"],"—",
|
||||
["Channel-Index + Channel-Detail (Reactions/Threads/Read)","Composer (Markdown/Emoji/@Mention/Anhang) + Tipp-Indikator"]),
|
||||
I("D-DASH","D-DASH · Dashboard (3 Rollen-Layouts) + Cmd+K",["david"],"W3",DA,"typ:screen","2026-07-01","2026-07-03",
|
||||
[(SM,"§5 Dashboard/Cmd+K")],["D-D0b"],"—",
|
||||
["3 feste Rollen-Layouts (GF/Büro/Monteur)","Cmd+K-Modal (Match-Highlight) + Vollbild-Such-View"]),
|
||||
I("D-BOOK","D-BOOK · Booking-Page (Public, Polish) + Kalender-View",["david"],"W3",DA,"typ:screen","2026-07-02","2026-07-03",
|
||||
[(SM,"§5 Booking/Kalender")],["D-D0b"],"—",
|
||||
["öffentliche Booking-Page (Slot-Picker, .ics, höchster Polish)","Kalender Monats-/Wochen-Grid + Connect-Card"]),
|
||||
I("D-REST","D-REST · KI/Telefonie/Signatur/Billing/DSGVO-Compose",["david"],"W4",DA,"typ:screen","2026-07-04","2026-07-06",
|
||||
[(SM,"§6")],["D-D0b"],"—",
|
||||
["KI-Settings/Credits, AirCall-Setup, Signatur-Wizard","Abo-Übersicht, Privacy-Hub, Feedback-Widget"]),
|
||||
I("D-POLISH","D-POLISH · Politur extern sichtbarer Flächen + Visual-Reg-Baselines",["david"],"W4",DA,"typ:screen","2026-07-06","2026-07-09",
|
||||
[(PB,"§6c CI-Gates")],["D-BOOK","D-DASH"],"FREEZE",
|
||||
["Booking/Forms/Marketing-Mails poliert","Visual-Regression-Baselines final (vor Freeze)"]),
|
||||
# ---- DEJAN ----
|
||||
I("J-SETUP","J-SETUP · Domain/Hosting/Stack + Setup",["dejan"],"W1",DJ,"typ:website","2026-06-16","2026-06-16",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-1")],[],"J-STRUKTUR",
|
||||
["Stack/Hosting/Domain geklärt","Repo-Setup + Seitengerüst"]),
|
||||
I("J-STRUKTUR","J-STRUKTUR · Seitenstruktur + Davids Screens einbauen",["dejan"],"W1",DJ,"typ:website","2026-06-16","2026-06-17",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-1")],["J-SETUP","D-WEB"],"J-CONTENT",
|
||||
["Seitenstruktur (Landing/Features/Pricing-Teaser/Demo-Formular/Webinar-Warteliste)","Davids Designs umgesetzt"]),
|
||||
I("J-CONTENT","J-CONTENT · Content/Texte einbauen",["dejan"],"W1",DJ,"typ:website","2026-06-17","2026-06-17",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-1")],["J-STRUKTUR","H-TXT","N-TXTABN"],"J-LIVE",
|
||||
["Texte (Haris, Noah-abgenommen) eingepflegt","Webinar-Plattform gewählt (EU-konform)"]),
|
||||
I("J-LAW","J-LAW · Rechtstexte einbauen (Anwalt)",["dejan"],"W1",DJ,"typ:website","2026-06-17","2026-06-18",
|
||||
[(EX,"E6")],["EXT-03"],"J-LIVE",
|
||||
["Impressum/Datenschutz/AVV-Link eingebaut"]),
|
||||
I("J-QA","J-QA · QA + Lead-Routing-Test",["dejan"],"W1",DJ,"typ:website","2026-06-17","2026-06-18",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-1")],["J-CONTENT"],"J-LIVE",
|
||||
["responsive QA","Formular → Lead-Routing an Haris getestet"]),
|
||||
I("J-LIVE","J-LIVE · WEBSITE LIVE",["dejan"],"W1",DJ,"typ:website","2026-06-18","2026-06-18",
|
||||
[(TL,"Dejan / LIVE 17./18."),(ho("dejan/HANDOUT-DEJAN.md"),"AP-1")],["J-QA","J-LAW"],"H-INBOUND",
|
||||
["live auf hivegrid.de","EU-konformes Analytics + Search Console"]),
|
||||
I("J-SEO","J-SEO · SEO + Conversion + Pricing-Seite vorbereiten",["dejan"],"W2",DJ,"typ:website","2026-06-20","2026-06-26",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-2")],["J-LIVE"],"—",
|
||||
["SEO-Basics + Conversion-Feinschliff","Pricing-Seite vorbereitet (Preise 25.06.) + Founding-Sektion + Payment-Link"]),
|
||||
I("J-WEBINAR","J-WEBINAR · Webinar-Landing + Anmelde-Flow",["dejan"],"W3",DJ,"typ:website","2026-06-29","2026-07-03",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-3")],["J-LIVE"],"H-WEBINAR",
|
||||
["Landing + Anmelde-Flow","Bestätigungs-/Erinnerungs-Mails"]),
|
||||
I("J-WEBTECH","J-WEBTECH · Webinar-Technik (Plattform/Aufzeichnung)",["dejan"],"W4",DJ,"typ:website","2026-07-04","2026-07-10",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-5")],["J-WEBINAR"],"—",
|
||||
["Plattform-Setup (Livestorm/Zoom) + Generalprobe","Aufzeichnung + Follow-up-Seite (Webinar 14./15.07.)"]),
|
||||
I("J-LAUNCHUPD","J-LAUNCHUPD · Launch-Update Website",["dejan"],"W4",DJ,"typ:website","2026-07-06","2026-07-10",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"AP-4")],["J-LIVE","J-SEO"],"—",
|
||||
["finale Feature-Seiten + Pricing live","Launch-Ankündigung"]),
|
||||
# ---- HARIS ----
|
||||
I("H-LISTE","H-LISTE · Zielkundenliste SHK (KWP-Nutzer-Fokus)",["haris"],"W1",HA,"typ:vertrieb","2026-06-16","2026-06-17",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-1")],[],"H-CALLS",
|
||||
["SHK-Zielkundenliste, Fokus KWP-Nutzer (A-Leads)"]),
|
||||
I("H-PAIN","H-PAIN · Painpoint-Gespräch mit Noah",["haris"],"W1",HA,"typ:vertrieb","2026-06-16","2026-06-17",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-1")],["EXT-05"],"H-TXT, H-PITCH",
|
||||
["Exklusiv-Gespräch SHK-Painpoints (HubSpot/Zettelwirtschaft)"]),
|
||||
I("H-TXT","H-TXT · Website-Texte schreiben",["haris"],"W1",HA,"typ:vertrieb","2026-06-17","2026-06-17",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-1")],["H-PAIN","N-PAIN"],"N-TXTABN, J-CONTENT",
|
||||
["Headline/Nutzenversprechen/Feature-Texte → an Dejan"]),
|
||||
I("H-PITCH","H-PITCH · Pitch-Deck + Gesprächsskript",["haris"],"W1",HA,"typ:vertrieb","2026-06-18","2026-06-19",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-2")],["D-D0a"],"H-CALLS",
|
||||
["Pitch-Deck mit D0/Auth-Screens","Gesprächsskript (Noah-Abnahme)"]),
|
||||
I("H-INBOUND","H-INBOUND · Lead-Sheet + Inbound-Prozess (<24h)",["haris"],"W1",HA,"typ:vertrieb","2026-06-18","2026-06-19",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-2")],["J-LIVE"],"—",
|
||||
["Lead-Sheet aufgesetzt","jede Anfrage < 24h beantwortet"]),
|
||||
I("H-CALLS","H-CALLS · Erste Discovery-Calls",["haris"],"W2",HA,"typ:vertrieb","2026-06-23","2026-06-26",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-3")],["H-LISTE","H-PITCH"],"H-FOUND",
|
||||
["Discovery/Qualifizierung (Pain, KWP-Nutzung, Team-Größe)"]),
|
||||
I("H-FOUND","H-FOUND · Founding-Member-Angebot strukturieren",["haris"],"W2",HA,"typ:vertrieb","2026-06-25","2026-06-26",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-4")],["H-CALLS","EXT-14"],"H-PAYLINK",
|
||||
["Early-Bird-Jahresdeal (Preise 25.06.!)","Noah-Abnahme"]),
|
||||
I("H-PAYLINK","H-PAYLINK · Stripe Payment Link (Founding-Deal)",["haris"],"W2",HA,"typ:vertrieb","2026-06-26","2026-06-26",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-4")],["EXT-07","EXT-14"],"H-DEMO",
|
||||
["Founding-Deal-Payment-Link live (Geld VOR Launch möglich)"]),
|
||||
I("H-DEMO","H-DEMO · Klick-Demos + echte Produkt-Demos",["haris"],"W3",HA,"typ:vertrieb","2026-06-30","2026-07-03",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-5")],["AK-DEMO1"],"H-FOLLOW",
|
||||
["Klick-Demos (Demo-1-Stand) ab 30.06.","echte Demos (Kette E2E) ab 03.07."]),
|
||||
I("H-WEBINAR","H-WEBINAR · Webinar-Einladungen + Moderation",["haris"],"W4",HA,"typ:vertrieb","2026-07-06","2026-07-10",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-5")],["J-WEBINAR"],"—",
|
||||
["Einladungen raus (kleine Liste, Spam-Risiko)","Webinar Nr.1 moderieren (14./15.07.)"]),
|
||||
I("H-FOLLOW","H-FOLLOW · Trial-Follow-ups + Bilanz",["haris"],"W4",HA,"typ:vertrieb","2026-07-09","2026-07-10",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-6")],["H-DEMO"],"—",
|
||||
["Trial-Follow-ups (14 Tage nach Onboarding)","Bilanz: erste Gelder ✓"]),
|
||||
# ---- NOAH (Assignees Haris + Dejan) ----
|
||||
I("N-KWP","N-KWP · KWP-Mail freigeben + bnWin.net 8.1 N3 anstoßen",["haris","dejan"],"W1",NO,"typ:validierung","2026-06-16","2026-06-17",
|
||||
[(EX,"A1"),(ho("noah/HANDOUT-NOAH.md"),"KWP")],[],"EXT-04",
|
||||
["KWP-Mail-Antwort freigegeben/weitergeleitet","bnWin.net-Update 8.1 N3 bei Geerkens-IT angestoßen"]),
|
||||
I("N-PAIN","N-PAIN · Painpoint-Input liefern (mit Haris)",["haris","dejan"],"W1",NO,"typ:validierung","2026-06-16","2026-06-17",
|
||||
[(ho("noah/HANDOUT-NOAH.md"),"Painpoint")],[],"H-TXT",
|
||||
["SHK-Alltag-Painpoints im Gespräch geliefert"]),
|
||||
I("N-TXTABN","N-TXTABN · Website-Texte abnehmen",["haris","dejan"],"W1",NO,"typ:validierung","2026-06-17","2026-06-17",
|
||||
[(ho("noah/HANDOUT-NOAH.md"),"Abnahme")],["H-TXT"],"J-CONTENT",
|
||||
["Website-Texte abgenommen (alle Außen-Texte = Noah)"]),
|
||||
I("N-SLOTS","N-SLOTS · 4 Gate-Freitags-Slots bestätigen",["haris","dejan"],"W1",NO,"typ:validierung","2026-06-16","2026-06-16",
|
||||
[(ho("noah/HANDOUT-NOAH.md"),"Slots")],["EXT-05"],"N-DEMO1, N-DEMO2, N-GONOGO",
|
||||
["Slots 19./26.06. + 03./10.07. bestätigt"]),
|
||||
I("N-DATEN","N-DATEN · SHK-Beispieldaten liefern",["haris","dejan"],"W2",NO,"typ:validierung","2026-06-20","2026-06-26",
|
||||
[(EX,"E3"),(ho("noah/HANDOUT-NOAH.md"),"Daten")],[],"AK-DEMO1",
|
||||
["echte Pipelines + Custom-Felder + Rollen geliefert (für Seeds)"]),
|
||||
I("N-LAGER","N-LAGER · Lagerist-Frage beantworten (M070-Scope)",["haris","dejan"],"W2",NO,"typ:validierung","2026-06-20","2026-06-26",
|
||||
[(EX,"E2")],[],"—",
|
||||
["Antwort: gehört Lagerist-Arbeitsplatz zum Validierungs-Versprechen? (entscheidet M070-Dünnung)"]),
|
||||
I("N-DEMO1","N-DEMO1 · Demo 1 abnehmen (CRM klickbar)",["haris","dejan"],"W2",NO,"typ:validierung","2026-06-26","2026-06-26",
|
||||
[(TL,"Gate W2 / Demo 1")],["AK-DEMO1","R-P7","D-CRM","N-SLOTS"],"—",
|
||||
["CRM klickbar validiert (Geerkens-Praxisbrille)"]),
|
||||
I("N-DEMO2","N-DEMO2 · Demo 2 abnehmen (Kette E2E)",["haris","dejan"],"W3",NO,"typ:validierung","2026-07-03","2026-07-03",
|
||||
[(TL,"Gate W3 / Demo 2")],["R-P9b","N-SLOTS"],"—",
|
||||
["Kette Lead→Sales→Auftrag E2E validiert"]),
|
||||
I("N-GONOGO","N-GONOGO · Voll-Walkthrough + Go/No-Go",["haris","dejan"],"W4",NO,"typ:validierung","2026-07-10","2026-07-10",
|
||||
[(TL,"Gate W4 / Launch")],["R-P13","AK-P21","N-SLOTS"],"[LAUNCH]",
|
||||
["Voll-Walkthrough + Go/No-Go-Entscheidung Big-Bang-Launch"]),
|
||||
# ---- ergänztes External ----
|
||||
I("EXT-14","EXT-14 · Kostenmatrix/Preise mit Partner fixieren",["raffael"],"W2","Externals","typ:external","2026-06-24","2026-06-25",
|
||||
[(EX,"AD-15 / Preise"),(TL,"Risiko 1 — härtester Business-Blocker")],[],"H-FOUND, H-PAYLINK, J-SEO (Pricing-Seite)",
|
||||
["Kostenmatrix mit Partner durchgerechnet","Preise/Tier-Zuschnitt fixiert (Abo-Baukasten)","an Haris übergeben"]),
|
||||
]
|
||||
|
||||
# ---- Anlegen ----
|
||||
existing = {i["name"]: i["id"] for i in get_all("/issues/")}
|
||||
wid2id = {}
|
||||
for i in get_all("/issues/"):
|
||||
nm = i["name"]
|
||||
if " · " in nm: wid2id[nm.split(" · ", 1)[0]] = i["id"]
|
||||
|
||||
created = 0
|
||||
for (wid, title, asg, cy, mo, ty, start, target, doks, bby, blk, dod) in ISSUES:
|
||||
if title in existing:
|
||||
iid = existing[title]
|
||||
else:
|
||||
win = f"{cy} (faellig {target})"
|
||||
html = desc(doks, ", ".join(bby) if bby else "—", blk, ", ".join(asg), win, dod)
|
||||
body = {"name": title, "description_html": html, "state": BACKLOG,
|
||||
"assignees": [M[a] for a in asg], "labels": [lab[ty], lab[f"gate:{cy}"]],
|
||||
"start_date": start, "target_date": target}
|
||||
st, d = req("POST", "/issues/", body)
|
||||
if st >= 400:
|
||||
print("ISSUE FAIL:", wid, st, d); continue
|
||||
iid = d["id"]; existing[title] = iid; created += 1
|
||||
wid2id[wid] = iid
|
||||
cn = CY[cy]
|
||||
if cn in cyc: req("POST", f"/cycles/{cyc[cn]}/cycle-issues/", {"issues": [iid], "project": PID})
|
||||
if mo in mod: req("POST", f"/modules/{mod[mo]}/module-issues/", {"issues": [iid], "project": PID})
|
||||
print(f"Issues: {created} neu angelegt, {len(ISSUES)-created} bereits vorhanden.")
|
||||
|
||||
# ---- Blocker-Links (JIT-Netz) ----
|
||||
print("\n--- Blocker-Links ---")
|
||||
title_by_wid = {wid: t for (wid, t, *_ ) in ISSUES}
|
||||
linkn = 0
|
||||
for (wid, title, asg, cy, mo, ty, start, target, doks, bby, blk, dod) in ISSUES:
|
||||
if not bby: continue
|
||||
iid = wid2id.get(wid)
|
||||
if not iid: continue
|
||||
have = {l.get("title","") for l in get_all(f"/issues/{iid}/links/")}
|
||||
for b in bby:
|
||||
bid = wid2id.get(b)
|
||||
if not bid:
|
||||
print(f" ! Blocker {b} (von {wid}) nicht gefunden"); continue
|
||||
lt = f"⛔ blocked-by: {b}"
|
||||
if any(x.startswith(lt) for x in have): continue
|
||||
url = f"{BASE}/{WS}/projects/{PID}/issues/{bid}/"
|
||||
st, d = req("POST", f"/issues/{iid}/links/", {"title": lt, "url": url})
|
||||
if st < 400: linkn += 1
|
||||
else: print(" link FAIL", wid, "->", b, st, d)
|
||||
print(f"Blocker-Links: {linkn} neu gesetzt.")
|
||||
print("\nFERTIG (Phase B).", f"{BASE}/{WS}/projects/{PID}/")
|
||||
@@ -1,250 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HiveGrid -> Plane Ticket-Sync v2 (idempotent).
|
||||
Legt die 38 v2-Tickets an: Cycles v2-W1..W3 + Label v2/block-2 + Issues über 7 Module
|
||||
+ JIT-Blocker-Links (inkl. v1->v2-Andockpunkte, die v1-Tickets existieren bereits).
|
||||
Doku-Permalinks zeigen ins HiveGrid-Gitea-Repo (wie in v1).
|
||||
Re-Run-sicher: Issues per Name, Links per Titel-Prefix, Cycles/Labels per Name erkannt.
|
||||
"""
|
||||
import json, os, sys, time, urllib.request, urllib.error
|
||||
|
||||
# ⚠ BOOTSTRAP-GUARD (seit 2026-06-16): Plane ist LIVE + manuell gescrubbt (Umlaute, PWA-Umbau,
|
||||
# v3-Defers). Dieses Skript matcht Issues per VOLLNAMEN — ein Re-Run nach dem Scrub fände
|
||||
# umbenannte Tickets NICHT wieder und legte DUPLIKATE an. Laufende Pflege -> scrub_plane.py
|
||||
# (Working-ID-Match, idempotent). Bewusster Neu-Bootstrap nur mit --force-bootstrap.
|
||||
if "--force-bootstrap" not in sys.argv:
|
||||
raise SystemExit(
|
||||
"✋ Bootstrap gesperrt: Plane ist live + gescrubbt — Re-Run würde Duplikate erzeugen.\n"
|
||||
" Laufende Änderungen -> .planning/plane-sync/scrub_plane.py (idempotent).\n"
|
||||
" Echter Neu-Bootstrap nur bewusst mit --force-bootstrap."
|
||||
)
|
||||
|
||||
TOK = {}
|
||||
for line in open(os.path.expanduser("~/.cli-tokens/plane")):
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1); TOK[k.strip()] = v.strip()
|
||||
BASE = TOK["PLANE_BASE_URL"].rstrip("/"); WS = TOK["PLANE_WORKSPACE"]; KEY = TOK["PLANE_API_KEY"]
|
||||
PID = "35d0d968-8ab3-40cc-97b8-b81d917dc5df"
|
||||
API = f"{BASE}/api/v1/workspaces/{WS}/projects/{PID}"
|
||||
GITEA = "https://git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud/src/branch/main"
|
||||
M = {"raffael":"8e34a88c-19c9-4565-b4f5-50415d623ce8","akeshan":"8bf43e86-dc8a-4963-b57a-aeb5f2cc450a",
|
||||
"david":"6c1fc63f-133d-4f40-86e4-7f658a6edcc7","dejan":"a5883f5e-3038-453c-9401-39ab1f40be9e",
|
||||
"haris":"48367c1f-20d2-44f1-8ae2-faf35aa2052a"}
|
||||
|
||||
def _once(method, url, body):
|
||||
r = urllib.request.Request(url, data=json.dumps(body).encode() if body is not None else None, method=method)
|
||||
r.add_header("X-API-Key", KEY); r.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
raw = resp.read().decode(); return resp.status, (json.loads(raw) if raw else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {"_error": e.read().decode()[:300]}
|
||||
|
||||
def req(method, path, body=None):
|
||||
url = path if path.startswith("http") else API + path
|
||||
st, d = 0, {}
|
||||
for _ in range(8):
|
||||
st, d = _once(method, url, body)
|
||||
if st == 429:
|
||||
time.sleep(25); continue
|
||||
time.sleep(1.1)
|
||||
return st, d
|
||||
return st, d
|
||||
|
||||
def get_all(path):
|
||||
out, cursor = [], None
|
||||
while True:
|
||||
st, d = req("GET", path + (("?cursor=" + cursor) if cursor else ""))
|
||||
if st >= 400: print("GET FAIL", path, st, d); break
|
||||
if isinstance(d, list): out += d; break
|
||||
out += d.get("results", [])
|
||||
if d.get("next_page_results") and d.get("next_cursor"): cursor = d["next_cursor"]
|
||||
else: break
|
||||
return out
|
||||
|
||||
states = {s["name"]: s["id"] for s in get_all("/states/")}
|
||||
BACKLOG = states["Backlog"]
|
||||
|
||||
# ---- v2-Cycles (idempotent) ----
|
||||
CYCLES = [
|
||||
("v2-W1 — Public-API/Postfach/Abo/KI-Start", "2026-07-13", "2026-07-19"),
|
||||
("v2-W2 — KWP/Portal/Mobile-Start", "2026-07-20", "2026-07-26"),
|
||||
("v2-W3 — Admin/Help/Dashboard/v2-Launch", "2026-07-27", "2026-07-31"),
|
||||
]
|
||||
existing_cycles = {c["name"]: c["id"] for c in get_all("/cycles/")}
|
||||
cyc = dict(existing_cycles)
|
||||
for name, start, end in CYCLES:
|
||||
if name not in cyc:
|
||||
st, d = req("POST", "/cycles/", {"name": name, "start_date": start, "end_date": end, "project_id": PID})
|
||||
if st < 400: cyc[name] = d["id"]; print("Cycle angelegt:", name)
|
||||
else: print("Cycle FAIL:", name, st, d)
|
||||
W = {"v2-W1": CYCLES[0][0], "v2-W2": CYCLES[1][0], "v2-W3": CYCLES[2][0]}
|
||||
|
||||
# ---- Module + Labels (idempotent; Module existieren aus v1) ----
|
||||
mod = {m["name"]: m["id"] for m in get_all("/modules/")}
|
||||
lab = {l["name"]: l["id"] for l in get_all("/labels/")}
|
||||
for name, color in {"v2": "#0d9488", "block-2": "#b91c1c"}.items():
|
||||
if name not in lab:
|
||||
st, d = req("POST", "/labels/", {"name": name, "color": color})
|
||||
if st < 400: lab[name] = d["id"]; print("Label angelegt:", name)
|
||||
|
||||
# ---- Doku-Pfade ----
|
||||
RM, RV2, EX, TL = ".planning/ROADMAP.md", ".planning/ROADMAP-V2.md", ".planning/EXTERNALS-CHECKLISTE.md", ".planning/TIMELINE.md"
|
||||
RECUT = "docs/milestones/M19-KWP-API-RECUT.md"
|
||||
def sp(x): return f"_bmad-output/specs/{x}"
|
||||
def ho(p): return f".planning/handouts/{p}"
|
||||
def link(path, sec): return f'<a href="{GITEA}/{path}">{path.split("/")[-1]} — {sec}</a>'
|
||||
def desc(doks, bby, blk, owner, win, dod):
|
||||
dl = "<br/>".join(link(p, s) for p, s in doks)
|
||||
li = "".join(f"<li>{d}</li>" for d in dod)
|
||||
return (f"<p><b>📚 Doku</b><br/>{dl}</p>"
|
||||
f"<p><b>⛔ blocked-by:</b> {bby or '—'}<br/><b>➡️ blocks:</b> {blk or '—'}<br/>"
|
||||
f"<b>👤 Owner:</b> {owner} · <b>🗓 Fenster:</b> {win}</p>"
|
||||
f"<p><b>✅ Definition of Done</b></p><ul>{li}</ul>")
|
||||
|
||||
RA, AK, DA, DJ, HA, NO = "Raffael (Dev)", "Akeshan (Dev)", "David (Screens)", "Dejan (Website)", "Haris (Vertrieb)", "Noah (Validierung)"
|
||||
def I(wid, t, a, c, mo, ty, extra, s, tg, dk, bb, bl, dod): return (wid, t, a, c, mo, ty, extra, s, tg, dk, bb, bl, dod)
|
||||
|
||||
ISSUES = [
|
||||
# ---- EXTERNALS ----
|
||||
I("VEXT-1","VEXT-1 · KWP-API-Zugang nutzen + Recut-Detailplanung gegen API-Doku",["raffael"],"v2-W2","Externals","typ:external",["block-2"],"2026-07-20","2026-07-20",
|
||||
[(RM,"Phase 32"),(RECUT,"Recut bindend")],"—","VR-P32a, VN-1",["KWP-API-Zugang aktiv (Auth/Endpoints/Webhook-Support geklärt)","Recut-Detailplanung pro Milestone gegen echte Doku","kwpsync.exe NICHT bauen"]),
|
||||
I("VEXT-2","VEXT-2 · HalloPetra-Spec erheben (API-Vertrag)",["raffael"],None,"Externals","typ:external",["block-2"],"2026-07-27","2026-07-29",
|
||||
[(RM,"Phase 33")],"—","VR-P33",["HalloPetra-API-Vertrag erhoben (Auth/Webhook-Events/Recording/Call-Lifecycle)","auf TelephonyProviderInterface gemappt"]),
|
||||
I("VEXT-3","VEXT-3 · Memberspot/Boobora-Spec erheben (HR-Drittanbieter)",["akeshan"],None,"Externals","typ:external",["block-2"],"2026-07-27","2026-07-29",
|
||||
[(RM,"Phase 34")],"—","VA-P34",["Drittanbieter-API-Vertrag erhoben (Sync-Richtung/Datenkategorien)","auf Apps-Engine-Adapter gemappt"]),
|
||||
I("VEXT-4","VEXT-4 · App-Store-Einreichung Apple + Google (TestFlight/interner Track)",["akeshan"],"v2-W3","Externals","typ:external",[],"2026-07-28","2026-07-28",
|
||||
[(RM,"Phase 27"),(TL,"v2-Externals")],"VA-P27b","VJ-3 (Badges)",["Build eingereicht Apple + Google","TestFlight/interner Track sofort nutzbar","Privacy-Manifest + Demo-Account beigelegt"]),
|
||||
I("VEXT-5","VEXT-5 · Gmail-Restricted-Scope-Verifizierung nachhalten",["raffael"],"v2-W1","Externals","typ:external",[],"2026-07-13","2026-07-19",
|
||||
[(EX,"A2")],"—","VA-P23 (Gmail-Breite)",["Google-OAuth-App-Verification verfolgt (Antrag lief v1 EXT-13)","Outlook + Gmail-Testmodus als Brücke"]),
|
||||
# ---- RAFFAEL ----
|
||||
I("VR-P22a","VR-P22a · Public-API: OpenAPI + Read-Endpoints (M161)",["raffael"],"v2-W1",RA,"typ:phase",["v2"],"2026-07-13","2026-07-15",
|
||||
[(RM,"Phase 22"),(sp("M34-public-api.md"),"Read-API")],"R-P2b (ApiKey)","VR-P22b",["GET contacts/companies/deals/projects/tasks/leads cursor-paginiert + Filter/Sort/Sparse-Fieldsets","read_only-Scope lässt sensible Felder aus (nicht 403)","OpenAPI-3.1 auto-generiert + Swagger-UI"]),
|
||||
I("VR-P22b","VR-P22b · Public-API: API-Key-Auth/Scopes/Rate-Limit (M162/M027) + Webhooks (M163)",["raffael"],"v2-W1",RA,"typ:phase",["v2"],"2026-07-15","2026-07-17",
|
||||
[(RM,"Phase 22"),(sp("M34-public-api.md"),"Auth/Webhooks")],"VR-P22a, R-P12a (Automation external_event)","—",["API-Key-Auth + Scopes + Rate-Limit (1k/h Key, 10k/h Tenant) + X-RateLimit-Header","Incoming-Lead-Webhooks (HMAC/Token) + Auto-Match","Ausgehende WebhookSubscriptions HMAC-signiert, Retry/Backoff, Auto-Disable nach 10 Fehlern"]),
|
||||
I("VR-P25","VR-P25 · KI-Tiefe: HiveMind/RAG + Whisper + pgvector-Hybrid (M087)",["raffael"],"v2-W1",RA,"typ:phase",["v2"],"2026-07-18","2026-07-20",
|
||||
[(RM,"Phase 25"),(sp("M18-ki-layer.md"),"RAG/HiveMind")],"R-P13 (Gateway), R-P11a (Outbox/Suche)","VA-P24b (KI-Coach), VA-P29 (RAG-Korpus retro)",["Embedding-Pipeline → TenantEmbedding (pgvector), Permission-Pre-Filter vor Vector-Search","Hybride globale Suche (Volltext+Vektor RRF) + Whisper-EU async","HiveMind-Agent mit Action-Boundary (keine Schreib-Aktion ohne Bestätigung)"]),
|
||||
I("VR-P32a","VR-P32a · KWP: Connector + Health + Push-Queue (M089/090/091)",["raffael"],"v2-W2",RA,"typ:phase",["v2","block-2"],"2026-07-20","2026-07-22",
|
||||
[(RM,"Phase 32"),(sp("M19-kwp-integration.md"),"KWP"),(RECUT,"Recut bindend")],"R-P5 (Apps-Engine M33a), VEXT-1","VR-P32b",["KWP-Connector hinterm M33a-Adapter-Interface, Creds per-Tenant-DEK","Connection-Health-Probe (generisch, statt Heartbeat)","Push-Queue via Cloud-Outbox + Rate-Limited-Replay"]),
|
||||
I("VR-P32b","VR-P32b · KWP: Contract-Sentinel + Konflikt 'KWP gewinnt' (M092/093)",["raffael"],"v2-W2",RA,"typ:phase",["v2","block-2"],"2026-07-22","2026-07-24",
|
||||
[(RM,"Phase 32"),(RECUT,"Contract-Sentinel")],"VR-P32a","VR-P32c",["API-Contract-Sentinel (zod-Validierung + Versions-Check) pausiert Sync bei Drift + Admin-Alarm","Konflikt 'KWP gewinnt' (LWW + Audit-Restore über rolledBackData)","Optimistic Locking HiveGrid-seitig"]),
|
||||
I("VR-P32c","VR-P32c · KWP: Plantafel-Sync + Doku-Archiv + Task-ERP-Sync (M094/095/074)",["raffael"],"v2-W2",RA,"typ:phase",["v2","block-2"],"2026-07-24","2026-07-26",
|
||||
[(RM,"Phase 32"),(RECUT,"Sync-Familie")],"VR-P32b, R-P9b (Plantafel/Auftrag)","VN-2 (E2E 24.)",["Plantafel-Sync bidirektional über die API","Dokumenten-Archiv-Sync KWP→@toolctrl/storage","Task-ERP-Sync (M074), idempotent über AppSyncItem; E2E mit Geerkens Fr 24.07."]),
|
||||
I("VR-P28a","VR-P28a · AdminUser-Welt + AdminRBAC (M038/039)",["raffael"],"v2-W3",RA,"typ:phase",["v2"],"2026-07-27","2026-07-29",
|
||||
[(RM,"Phase 28"),(sp("M37-plattform-admin.md"),"AdminUser/RBAC")],"AK-P19 (Admin-Welt-Fundament)","VR-P31, VA-P28b",["Eigene AdminUser-Klasse, kein BYPASSRLS, eigene Cookies SameSite=Strict","AdminRBAC + 2FA-Pflicht + Self-Permission-Schutz (letzter super_admin)","AdminAuditLog cross-tenant mit Reason-Pflicht + Impersonate mit Reason+Banner"]),
|
||||
I("VR-P33","VR-P33 · Telefonie-Erweiterung HalloPetra (M105)",["raffael"],None,RA,"typ:phase",["v2","block-2"],"2026-07-29","2026-07-31",
|
||||
[(RM,"Phase 33"),(sp("M12-telefonie.md"),"Provider-Pattern")],"AK-P15a (TelephonyProviderInterface), VEXT-2","—",["HalloPetra-Adapter implementiert TelephonyProviderInterface ohne Kern-Umbau","Webhook-Auto-Match auf Contact/Lead/Deal + Activity + Automation-Trigger","⚠ Vorlauf-Spec VEXT-2 nötig (HalloPetra in keiner Spec)"]),
|
||||
I("VR-P31","VR-P31 · DSGVO-Vollausbau: AVV/Subprozessoren/Auskunft (M166)",["raffael"],"v2-W3",RA,"typ:phase",["v2"],"2026-07-29","2026-07-31",
|
||||
[(RM,"Phase 31"),(sp("M35-dsgvo-export.md"),"DSGVO")],"AK-P21 (DSGVO-Fundament), VR-P28a (Admin-Legal)","—",["AVV-Versionierung + dynamisches PDF + elektronische Akzeptanz + Re-Akzeptanz-Frist (14 Tage → Read-Only)","Subprozessoren-Liste + Drittland-Warnung + 14-Tage-Widerspruchs-Workflow","User/Tenant-Voll-Export (ZIP, async, 7-Tage-Window) + Lösch-Workflow (Grace + Hard-Delete-Cron)"]),
|
||||
# ---- AKESHAN ----
|
||||
I("VA-P24a","VA-P24a · Self-Service-Abo-Baukasten-UI (M28-Delta)",["akeshan"],"v2-W1",AK,"typ:phase",["v2"],"2026-07-13","2026-07-15",
|
||||
[(RM,"Phase 24"),(sp("M28-subscriptions-billing.md"),"Block P Self-Service")],"AK-P20 (Billing-Fundament)","VA-P24b",["Self-Service-Trial ohne Vertriebs-Freigabe (Zahlungsmethode-Pflicht)","Modul-Toggle + Preis-Vorschau (Abo-Baukasten)","App-Store-konform (kein Pricing-UI in Mobile, Web-Flow)"]),
|
||||
I("VA-P23","VA-P23 · Postfach-Sync: Gmail/Outlook OAuth + Inbox + Threading + Versand (M096-098)",["akeshan"],"v2-W1",AK,"typ:phase",["v2"],"2026-07-13","2026-07-17",
|
||||
[(RM,"Phase 23"),(sp("M09-email-inbox.md"),"OAuth-Sync/Inbox")],"R-P5 (Apps-Engine), R-P6 (CRM-Match), AK-P14a (Sende-Domain)","—",["OAuth-Connect Gmail (Pub/Sub) + Outlook (Graph + 3-Tage-Renewal-Worker)","Push-Sync + Auto-Match (From/Subject/1-Deal) mit manuellem Override","Inbox-Liste + Thread-Detail (sicherer HTML-Render) + Reply/Forward; read_own/read_all"]),
|
||||
I("VA-P24b","VA-P24b · Onboarding-Wizard-UI (M157)",["akeshan"],"v2-W1",AK,"typ:phase",["v2"],"2026-07-18","2026-07-20",
|
||||
[(RM,"Phase 24"),(sp("M29-onboarding.md"),"Wizard")],"VA-P24a, VR-P25 (KI-Coach retro)","—",["Wizard resumebar, Step-Validierung, Rückwärts-Block nach irreversiblen Actions","SHK-Branchen-Template deklarativ geseedet (weitere Branchen 'coming soon')","Erste-Schritte-Checkliste + Demo-Daten-Bulk-Löschung"]),
|
||||
I("VA-P26a","VA-P26a · Customer-Portal: Auth/Magic-Link + Projekt-Sicht + Status-Mails (M141/142) ⭐",["akeshan"],"v2-W2",AK,"typ:phase",["v2"],"2026-07-20","2026-07-21",
|
||||
[(RM,"Phase 26"),(sp("M15-customer-portal.md"),"Portal")],"AK-P4a (FileEntityLink), R-P6 (CRM), R-P9a (Project)","VA-P26b",["Eigene PortalUser-Klasse getrennt (Cookies/Routes), Magic-Link primär","Nur eigene Projekte/konfigurierte Stages sichtbar, fehlend → 404","Auto-Status-Mails bei project.stage_changed (Debounce/Bedingungen)"]),
|
||||
I("VA-P26b","VA-P26b · Customer-Portal: Upload/Reklamation + Subunternehmer/Skalierung/DSGVO (M143-146) ⭐",["akeshan"],"v2-W2",AK,"typ:phase",["v2"],"2026-07-21","2026-07-22",
|
||||
[(RM,"Phase 26"),(sp("M15-customer-portal.md"),"Portal-Skalierung")],"VA-P26a","VJ-3",["Upload (Kamera/Multi-File) → Files + Activity + PL-Notification","Reklamations-Form + Subunternehmer-Zugang + Skalierungs-Sicht (Service-Inbox)","Tenant-Admin-Konfig (Sichtbarkeit/Templates) + DSGVO-Lösch/Export für PortalUser"]),
|
||||
I("VA-P27a","VA-P27a · Mobile-App (Expo): Setup + Build + Native-Features Push/Bio/Kamera (M167/168)",["akeshan"],"v2-W2",AK,"typ:phase",["v2"],"2026-07-23","2026-07-25",
|
||||
[(RM,"Phase 27"),(sp("M36-mobile-app.md"),"Expo-Recut")],"R-P9b (OfflineSyncQueue), AK-P21 (notify-Push)","VA-P27b",["Expo-Setup + EAS-Build-Pipeline","Pre-Cache beim Login + Offline-Write-Queue (Foto/Note/Task/Checklist/Acceptance/Chat)","Push (Apple-Pre-Prompt) + Biometrie (Keychain/Keystore, 2FA bleibt) + Kamera"]),
|
||||
I("VA-P27b","VA-P27b · Mobile-App: Tab-Bar/Routes + Offline-Sync + Store-Compliance (M169/170/171)",["akeshan"],"v2-W3",AK,"typ:phase",["v2"],"2026-07-25","2026-07-27",
|
||||
[(RM,"Phase 27"),(sp("M36-mobile-app.md"),"Offline/Compliance")],"VA-P27a","VEXT-4 (Einreichung)",["Native Tab-Bar (Baustelle/Büro) + Mobile-Routes","Sync-on-Reconnect idempotent (lokale UUID) + Konflikt-Resolution","Store-Compliance: kein Pricing-UI (Build-Flag), Account-Löschung, Privacy-Manifest, Deeplinks + Force-Update"]),
|
||||
I("VA-P29","VA-P29 · Help-Center + Help-Desk (M172-175)",["akeshan"],"v2-W3",AK,"typ:phase",["v2"],"2026-07-28","2026-07-30",
|
||||
[(RM,"Phase 29"),(sp("M38-help-center.md"),"Help-Center"),(sp("M39-help-desk.md"),"Help-Desk")],"AK-P21 (Feedback-Widget), AK-P19 (Admin)","VJ-2, VR-P25 (RAG-Korpus retro)",["Markdown-Artikel (Frontmatter) + Build-Index + öffentliche Routen ohne Auth + SEO/Suche","Deep-Link Route→Artikel (Hilfe-Icon pro Route)","Feedback-Widget → Feedback + Auto-Kontext; Admin-Feedback-Liste/Detail + Audit"]),
|
||||
I("VA-P30a","VA-P30a · Dashboard-Deluxe: Custom-Widgets + Export (M149/150)",["akeshan"],"v2-W3",AK,"typ:phase",["v2"],"2026-07-29","2026-07-30",
|
||||
[(RM,"Phase 30"),(sp("M20-dashboard.md"),"Custom-Widgets/Export")],"R-P11b (Dashboard/Widget-Registry)","—",["Layout anpassbar (Drag/Resize, permission-gegateter Widget-Catalog)","Drilldown pro Kachel + CSV-Export + Report-Versand","Realtime-Updates mit Opt-Out"]),
|
||||
I("VA-P30b","VA-P30b · Querschnitt: Notif-Bündelung/Quiet-Hours (M117) + Sprach-Wechsler/EN (M029)",["akeshan"],"v2-W3",AK,"typ:phase",["v2"],"2026-07-30","2026-07-31",
|
||||
[(RM,"Phase 30"),(sp("M22-notifications.md"),"Bündelung"),(sp("M30-i18n.md"),"Wechsler/EN")],"AK-P3 (Notif-Center), R-P1b (i18n-Library)","—",["Notif-Bündelung (5-Min-Window → 1 Push/Mail mit count) + Quiet-Hours (Urgent-Override)","Pro-User-Präferenzen Event×Kanal","Sprach-Wechsler + EN-Locale (DeepL DE→EN mit Review) + Empfänger-Sprach-Auflösung"]),
|
||||
I("VA-P28b","VA-P28b · Flag-Massen-Statistik (M033) + interne Lead-Pipeline (M040)",["akeshan"],"v2-W3",AK,"typ:phase",["v2"],"2026-07-29","2026-07-31",
|
||||
[(RM,"Phase 28"),(sp("M32-feature-flags.md"),"Massen-Statistik")],"AK-P19 (Admin), VR-P28a (AdminAudit)","—",["Flag-Massen-Statistik (Effektiv-Auswertung X aktiv/Y nicht + Live-Filter-Vorschau + Bulk-Override)","Deterministische Auswertung (Override > Filter > Default) + Audit","Interne Lead-Pipeline (HiveGrid-eigene Verkäufe)"]),
|
||||
I("VA-P34","VA-P34 · HR-Drittanbieter-Onboarding Memberspot/Boobora (M138)",["akeshan"],None,AK,"typ:phase",["v2","block-2"],"2026-07-29","2026-07-31",
|
||||
[(RM,"Phase 34"),(sp("M14-hr-hub.md"),"HR-Onboarding")],"R-P5 (Apps-Engine), AK-P17a (HR-Hub), VEXT-3","—",["HR-Onboarding-Checklisten pro neuem MA (verlinkte Forms/Tasks/Reminders)","Drittanbieter als DataConnection via Apps-Engine (gleicher Mechanismus wie KWP)","⚠ Vorlauf-Spec VEXT-3 nötig (Memberspot/Boobora in keiner Spec)"]),
|
||||
# ---- DAVID ----
|
||||
I("VD-1","VD-1 · Screens: Abo-Baukasten-UI + Wizard + Postfach-Inbox",["david"],"v2-W1",DA,"typ:screen",["v2"],"2026-07-13","2026-07-16",
|
||||
[(RV2,"P23/P24"),(sp("M28-subscriptions-billing.md"),"Self-Service")],"D-D0b (Archetypen)","VA-P24a, VA-P24b, VA-P23",["Abo-Baukasten-UI (Modul-Toggle + Preis-Vorschau) + Wizard-Steps + Postfach-Inbox; 4 Zustände + Reconciliation"]),
|
||||
I("VD-2","VD-2 · Screens: Portal-Shell (extern sichtbar, eigener Polish)",["david"],"v2-W1",DA,"typ:screen",["v2"],"2026-07-17","2026-07-19",
|
||||
[(RV2,"P26"),(sp("M15-customer-portal.md"),"Portal-UI")],"D-D0b","VA-P26a, VA-P26b",["Portal-Shell mit eigener Design-Sprache (höchster Polish), Endkunden-Flächen + 4 Zustände"]),
|
||||
I("VD-3","VD-3 · Screens: Mobile-Shell Expo (Touch, Tab-Bar, Offline-Zustände)",["david"],"v2-W2",DA,"typ:screen",["v2"],"2026-07-20","2026-07-22",
|
||||
[(RV2,"P27"),(sp("M36-mobile-app.md"),"Mobile-UI")],"D-D0b","VA-P27a, VA-P27b",["Mobile-Shell: native Tab-Bar, Touch-Paradigma, Offline-Zustände, Sync-Queue/Konflikt-View"]),
|
||||
I("VD-4","VD-4 · Screens: Help-Center (Public-Content-Site)",["david"],"v2-W2",DA,"typ:screen",["v2"],"2026-07-23","2026-07-27",
|
||||
[(RV2,"P29"),(sp("M38-help-center.md"),"Help-UI")],"D-D0b","VA-P29",["Help-Home/Kategorie/Artikel-Detail/Suche; SEO-Layout"]),
|
||||
I("VD-5","VD-5 · Screens: Public-API-Keys + Admin-Tiefe + KWP-Setup",["david"],"v2-W2",DA,"typ:screen",["v2"],"2026-07-17","2026-07-22",
|
||||
[(RV2,"P22/P28/P32")],"D-D0b","VR-P22b, VR-P28a, VR-P32a",["API-Key-/Webhook-Verwaltung + AdminUser/Flag-Statistik + KWP-Setup-Wizard (OAuth/Sync-Vorschau)"]),
|
||||
I("VD-6","VD-6 · Screens: App-Store-Assets/Screenshots + v2-Politur + Visual-Reg",["david"],"v2-W3",DA,"typ:screen",["v2"],"2026-07-28","2026-07-31",
|
||||
[(TL,"David v2")],"VD-2, VD-3","VJ-1",["App-Store-Screenshots/Assets (mit Dejan) + v2-Politur extern + Visual-Reg-Baselines"]),
|
||||
# ---- DEJAN ----
|
||||
I("VJ-1","VJ-1 · App-Store-Metadaten (Beschreibung/Keywords/Screenshots/Datenschutz)",["dejan"],"v2-W2",DJ,"typ:website",["v2"],"2026-07-20","2026-07-27",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"App-Store")],"VD-6","VEXT-4",["Store-Listings Apple + Google (Beschreibung/Keywords/Screenshots/Datenschutz-Angaben) einreichbar"]),
|
||||
I("VJ-2","VJ-2 · Help-Center-Inhalte einpflegen",["dejan"],"v2-W3",DJ,"typ:website",["v2"],"2026-07-28","2026-07-28",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"Help-Inhalte")],"VA-P29","—",["Help-Center-Artikel (Markdown/Git-PRs) eingepflegt"]),
|
||||
I("VJ-3","VJ-3 · Website-Update: Portal/Mobile/KWP als Features live + Store-Badges",["dejan"],"v2-W3",DJ,"typ:website",["v2"],"2026-07-31","2026-07-31",
|
||||
[(ho("dejan/HANDOUT-DEJAN.md"),"Launch-Update")],"VA-P26b, VA-P27b, VR-P32c","—",["Feature-Seiten Portal/Mobile/KWP live + Store-Badges (sobald Freigabe)"]),
|
||||
# ---- NOAH (Assignees Haris + Dejan) ----
|
||||
I("VN-1","VN-1 · KWP-Test-Instanz/Testdaten bereitstellen",["haris","dejan"],"v2-W2",NO,"typ:validierung",["v2","block-2"],"2026-07-20","2026-07-23",
|
||||
[(ho("noah/HANDOUT-NOAH.md"),"KWP-Verprobung")],"VEXT-1","VN-2",["KWP-Test-Instanz + Testdaten bei Geerkens bereit (ab 20.07.)"]),
|
||||
I("VN-2","VN-2 · KWP-Sync-E2E-Verprobung mit Geerkens",["haris","dejan"],"v2-W2",NO,"typ:validierung",["v2","block-2"],"2026-07-24","2026-07-24",
|
||||
[(TL,"KWP-E2E 24.07.")],"VR-P32c, VN-1","VH-1",["KWP-Sync-E2E mit echten Geerkens-Testdaten verprobt (Fr 24.07.)"]),
|
||||
# ---- HARIS ----
|
||||
I("VH-1","VH-1 · KWP-Sync als Verkaufsargument + weitere Onboardings/Conversions",["haris"],"v2-W2",HA,"typ:vertrieb",["v2"],"2026-07-24","2026-07-31",
|
||||
[(ho("haris/HANDOUT-HARIS.md"),"AP-6")],"VN-2","—",["KWP-Sync-Referenz (Geerkens) als A-Lead-Argument","weitere Onboardings + Trial-Conversions Richtung 'erste Gelder ≤ Ende Juli'"]),
|
||||
]
|
||||
|
||||
# ---- Anlegen ----
|
||||
existing = {i["name"]: i["id"] for i in get_all("/issues/")}
|
||||
wid2id = {}
|
||||
for i in get_all("/issues/"):
|
||||
if " · " in i["name"]: wid2id[i["name"].split(" · ", 1)[0]] = i["id"]
|
||||
|
||||
created = 0
|
||||
for (wid, title, asg, cy, mo, ty, extra, start, target, doks, bb, bl, dod) in ISSUES:
|
||||
new = title not in existing
|
||||
if new:
|
||||
win = (f"{cy} (faellig {target})" if cy else f"Backlog/agil (Trigger, Ziel {target})")
|
||||
html = desc(doks, bb, bl, ", ".join(asg), win, dod)
|
||||
labels = [lab[ty], lab["v2"]] + [lab[x] for x in extra if x in lab and x != "v2"]
|
||||
body = {"name": title, "description_html": html, "state": BACKLOG,
|
||||
"assignees": [M[a] for a in asg], "labels": list(dict.fromkeys(labels)),
|
||||
"start_date": start, "target_date": target}
|
||||
st, d = req("POST", "/issues/", body)
|
||||
if st >= 400: print("ISSUE FAIL:", wid, st, d); continue
|
||||
iid = d["id"]; existing[title] = iid; created += 1
|
||||
else:
|
||||
iid = existing[title]
|
||||
wid2id[wid] = iid
|
||||
if cy and W[cy] in cyc:
|
||||
req("POST", f"/cycles/{cyc[W[cy]]}/cycle-issues/", {"issues": [iid], "project": PID})
|
||||
if mo in mod:
|
||||
req("POST", f"/modules/{mod[mo]}/module-issues/", {"issues": [iid], "project": PID})
|
||||
print(("neu:" if new else "vorhanden:"), wid)
|
||||
print(f"\nIssues: {created} neu, {len(ISSUES)-created} vorhanden.")
|
||||
|
||||
# ---- Blocker-Links (JIT-Netz; blocked_by aus den ⛔-Feldern, Working-IDs extrahiert) ----
|
||||
print("\n--- Blocker-Links ---")
|
||||
import re
|
||||
def parse_bby(s):
|
||||
# extrahiert Working-IDs am Zeilenanfang/komma-getrennt: "R-P2b (ApiKey), VEXT-1" -> ["R-P2b","VEXT-1"]
|
||||
ids = re.findall(r'\b(?:V?(?:R|AK|A|D|J|H|N|EXT)[-A-Za-z0-9]*)\b', s)
|
||||
out = []
|
||||
for x in ids:
|
||||
if x in wid2id and x not in out: out.append(x)
|
||||
return out
|
||||
linkn = 0
|
||||
for (wid, title, asg, cy, mo, ty, extra, start, target, doks, bb, bl, dod) in ISSUES:
|
||||
if not bb or bb == "—": continue
|
||||
iid = wid2id.get(wid)
|
||||
if not iid: continue
|
||||
have = {l.get("title","") for l in get_all(f"/issues/{iid}/links/")}
|
||||
for b in parse_bby(bb):
|
||||
bid = wid2id.get(b)
|
||||
if not bid: continue
|
||||
lt = f"⛔ blocked-by: {b}"
|
||||
if any(x.startswith(lt) for x in have): continue
|
||||
url = f"{BASE}/{WS}/projects/{PID}/issues/{bid}/"
|
||||
st, d = req("POST", f"/issues/{iid}/links/", {"title": lt, "url": url})
|
||||
if st < 400: linkn += 1
|
||||
else: print(" link FAIL", wid, "->", b, st, d)
|
||||
print(f"Blocker-Links: {linkn} neu gesetzt.")
|
||||
print("\nFERTIG (v2).", f"{BASE}/{WS}/projects/{PID}/")
|
||||
@@ -47,6 +47,43 @@ Das **v1-Fundament der Email-Welt** (Postfach-Sync ist v2 → P23): pro Mandant
|
||||
- `designer-briefs/P14-email-marketing.md` (Sende-Domain-Card, BCC-Toggle, Suppression)
|
||||
- `_bmad-output/specs/M09-email-inbox.md` (nur Sende-Domain/Inbound/BCC — Rest v2)
|
||||
|
||||
## Umsetzung (Akeshan)
|
||||
|
||||
### 2026-06-29 — Backend-Fundament (S1–S5), ~45 % DoD
|
||||
Branch `feat/AK-P14a` (noch nicht gepusht). Strategie laut Plan: Backend-Fundament jetzt, Kontakt-abhängige Verdrahtung + finale Subframe-UI zurückgestellt (R-P6/R-P7/David offen).
|
||||
|
||||
**Gebaut + verifiziert (alle Typechecks grün, 35 neue Unit-Tests grün):**
|
||||
- **S1 Schema** (`prisma/schema/email.prisma`, additiv, `prisma db push`-bereit, keine Migration): neu `TenantSendingDomain` (RLS, DKIM-Privatkey als per-Tenant-Envelope-Ciphertext AD-8, Soft-Delete, `verificationStatus`, `firstProbeAt`/`lastProbedAt`, `mailjetDnsId`), `EmailSuppressionList` (`@@unique([tenantId,emailAddress])`, `reason`), `EmailTemplate` (`isDefault`/`isDemoData`). Erweitert: `TenantInboundAddress` (`purpose`/`localPart`/`fullAddress`/`enabled`), `EmailSender` (`isDefault`/`isDemoData`/`sendingDomainId`), `Email` (`purpose`/`deliveryStatus`/`relatedKontaktId`/`relatedProjektId` als bare Scalars — FK erst R-P6). RLS: 3 neue Modelle in `TENANT_SCOPED_MODELS` + `prisma/rls/email-isolation.sql` (deferred-cutover wie hr). `prisma validate` + Registry-Test grün.
|
||||
- **S2 DNS-Auto-Probe** (`apps/worker/src/jobs/email-domain-probe-cron.ts` + Handler in `handlers.ts` + Registrierung `index.ts`, Payload-Literal in `queue/payloads.ts`): 5-Min-Cron, DKIM/SPF/DMARC-Lookup (injizierbarer Resolver), Status `pending→verifying→verified|failed`, 7-Tage-Klartext-Key, `notify` bei verified/failed, Write auto-auditiert (M001). 9 Unit-Tests.
|
||||
- **S3 Bounce/Complaint → Suppression**: `packages/db/src/email-suppression.server.ts` (idempotenter Upsert + `isEmailSuppressed`-Pre-Send-Guard) + `apps/api/src/webhooks/mailjet-suppression.ts` (reiner Signal-Parser, Tenant via Mailjet-`Payload`/`CustomID`), additiv in den bestehenden Mailjet-Webhook gehängt (optionaler Dep → keine Regression). 13 Unit-Tests.
|
||||
- **S4 Tracking-Endpoints**: `packages/db/src/email-tracking.server.ts` (`anonymizeTrackingIp` sha256/trunc, `safeRedirectTarget` Open-Redirect-Schutz, `recordEmailOpen`/`recordEmailClick`) + öffentliche Routes `e.o.$outboundEmailId` (1×1-GIF) / `e.c.$outboundEmailId` (302-Redirect). `ipHash` additiv auf Open/Click-Modellen. 8 Unit-Tests.
|
||||
- **S5 Provider-Abstraktion**: `packages/mail/src/mailjet-dns.ts` (`createMailjetDnsProvisioner`: registerDomain/getRecords/check via Mailjet `/v3/REST`, Basic-Auth, injizierbarer fetch, AD-6, kein US-Kern). 5 Unit-Tests.
|
||||
|
||||
**Teilweise (S6) / offen:**
|
||||
- **S6 (angefangen)** — `buildSendingDomainVM` (`packages/view-models/src/email/sending-domain.vm.ts`, 5 Tests grün) ✅. Offen: Domain-Service (`emailDomainService.server.ts`, wiret S1+S5+withTenantRLS), reachable Route + Plain-UI + i18n + Smoke. **Befund:** Modul `email-marketing` existiert bereits inkl. Shell `app.$tenant/email-marketing.tsx` (Gate `requireTenantProductAccess(featureKeys.emailMarketing)`) + Sidebar-Eintrag (`AppSidebar.tsx`, `hasFeatureAccess("email-marketing")`) + `email-marketing/senders.tsx`. → Sende-Domain kommt als gated Tab in dieses Modul; finale Route `/settings/integrations/email` ist Davids UI-Entscheidung (Dev-Nachzug).
|
||||
- **S8 (revidiert)** — Konnektor/Sidebar **existiert bereits** über `featureKeys.emailMarketing` ("email-marketing"); **kein** Eingriff in `@hivegrid/entitlements` nötig (Ticket-`tenantHasModule('email')` = das vorhandene `email-marketing`-Feature). Offen: tenant-seitige Permissions `email.domain.view`/`.manage` in 4 Registries (`DefaultPermissions.ts`, `permissions-seed.server.ts` Array+Rollen-Map, `roleDisplay.ts` scope→`emailDomain`, i18n scopes/actions inkl. `manage`) + i18n-Parität (4 Locales, roleDisplay-Test + verify:i18n als Gates).
|
||||
- **S7** BCC-/Inbound→Activity-am-Kontakt + Lead-Anlage — **blockiert R-P6 (`Kontakt`) / R-P7 (tenant-Lead)**; Empfang/Persistenz machbar, Kontakt-Resolver-Write bleibt markierter Stub.
|
||||
|
||||
### 2026-06-29 (Update) — S6 + S8 umgesetzt
|
||||
- **S6** `emailDomainService.server.ts` (Context+Permission-Gate 404, getSendingDomainState, createSendingDomain via Mailjet-Provisioner S5, setBccLogging, pure normalizeDomainInput/bccAddressFor) + Route `app.$tenant/email-marketing/sending-domain.tsx` (Loader→VM, Actions create-domain/toggle-bcc) + Plain-UI (DNS-Karten+Copy, Status-Ampel, 7-Tage-Klartext, BCC-Toggle, Empty-State, data-ui-id) + Tab im Modul-Shell. **Reachable** über das gated `email-marketing`-Modul.
|
||||
- **S8** Permissions `email.domain.view/.manage` in 4 Registries (DefaultPermissions, permissions-seed Array+Rollen-Map, roleDisplay scope `emailDomain`, i18n en/de) + Domain-Screen-i18n (en/de). Konnektor = vorhandenes Feature `email-marketing` (kein entitlements-Eingriff). roleDisplay-Test (7) + verify:i18n grün, web/db typecheck grün.
|
||||
|
||||
### 2026-06-29 (Merge) — main in Branch + Email-Phasen-Migration (AD-21)
|
||||
`main` in `feat/AK-P14a` gemergt (R-P6 typisierte Kontakte, R-P6B/C, AD-2-Präzisierung). Einzige Konflikte im generierten Prisma-Client → via `pnpm db:generate` aus dem gemergten Schema neu erzeugt (enthält beide Seiten). Kein neuer Entity-Builder-Bezug (AD-2 + Raffaels R-P14C-Ownership eingehalten).
|
||||
|
||||
Der CI-Docker-Smoke (`migrate deploy` gegen leere DB + Seed) schlug fehl (`P2022: TenantInboundAddress.createdAt does not exist`) — die Email-Schema-Änderungen lagen per AD-21 nur als `db push` vor, ohne Migration. Migration **`20260629120000_p14a_email_foundation`** erzeugt (AD-21 „beim Merge"): schema-zu-schema-Diff (merge-base vs. Branch), auf das Email-Delta getrimmt (3 Enums + 3 neue Tabellen + Spalten auf TenantInboundAddress/Email/EmailSender/OutboundEmail* + Indizes), Forward-only mit Inverse-SQL-Kommentar (DoD 28), RLS deferred (Artefakt `prisma/rls/email-isolation.sql`, Muster wie AK-P4a/hr). Verifiziert: `migrate deploy` auf frischer DB grün, Seed läuft komplett durch (`rolesAndPermissions` + entitlements `success`), `prisma validate` grün.
|
||||
|
||||
### DoD-Stand (aktualisiert)
|
||||
- [x] Sende-Domain DKIM/SPF/DMARC + 5-Min-Probe + Ampel + 7-Tage-Klartext — Backend (S1+S2) + reachable Plain-UI (S6) ✅ (Subframe-Politur = Dev-Nachzug nach David)
|
||||
- [~] BCC-Logging-Brücke — Toggle + BCC-Adresse-UI ✅ (S6); Mail→Activity-am-Kontakt **blockiert R-P6**
|
||||
- [~] Tenant-Inbound → Lead/Activity — Schema ✅; Verdrahtung **blockiert R-P6/R-P7**
|
||||
- [x] Email-Tracking + Open/Click (IP anon) + Bounce → Suppression — ✅ (S3+S4; Delivery-Status via Send-Pfad = AK-P14b)
|
||||
- [x] SMTP-Provider-Abstraktion (Mailjet, AD-6) — ✅ (S5)
|
||||
- [x] Konnektor `tenantHasModule` (Sidebar/Routes/Actions gated) — ✅ (S8, Feature `email-marketing` + email.domain-Permissions)
|
||||
- [x] RLS auf allen neuen Tenant-Tabellen; `is_demo_data` auf seedbaren Sendern/Templates — ✅ (S1, RLS deferred-cutover-Artefakt wie hr)
|
||||
|
||||
**Stand: 5/7 DoD voll + 2 partiell ≈ ~71 % done.** Selbst-machbar offen (klein): Suppression-Listen-UI im Senders-Screen, e2e-Smoke (braucht laufende Stack+Seed). Extern blockiert: BCC/Inbound→Activity (S7, R-P6/R-P7), finale Subframe-UI (Davids D-P14/D-KAMP-Delta).
|
||||
|
||||
## Dev-Nachzug (nach Davids `D-KAMP`/`D-P14`-Delta)
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
|
||||
@@ -49,6 +49,66 @@ Das **vollständige DSGVO-konforme Email-Marketing** (bleibt VOLL inkl. A/B): ei
|
||||
- `designer-briefs/P14-email-marketing.md` (Kampagnen-Editor BESPOKE, Segmente, Unsubscribe-Page)
|
||||
- `_bmad-output/specs/M10-email-marketing.md`
|
||||
|
||||
## Umsetzung (Akeshan)
|
||||
|
||||
### 2026-06-29 — Typisierte Backend-Foundation (S1–S7), ~78 % DoD
|
||||
Branch `feat/AK-P14b` (von `feat/AK-P14a`, noch nicht gepusht). Plan-Entscheidung (User, 2026-06-29):
|
||||
**typisierter Pfad** (neuer Code liest R-P6 `Kontakt`, KEIN neuer Entity-Builder-Konsument, AD-2) +
|
||||
**Backend-Foundation jetzt**, bespoke Editor/Unsubscribe-Page + Lead/Projekt-Segment-Dimensionen + e2e/Visual
|
||||
zurückgestellt. Der bestehende Entity-Builder-Kampagnen-Pfad (`CrmService`/`EntityViewsApi`/
|
||||
`OutboundEmail.contactRowId`) bleibt **unangetastet** (gehört R-P14C).
|
||||
|
||||
**Gebaut + verifiziert (46 neue Unit-Tests grün, alle Typechecks 0 Fehler, verify:i18n exit 0):**
|
||||
- **S1 Schema** (`prisma/schema/email.prisma`, additiv, `db push`, keine Migration — AD-21): neu `RecipientSegment`
|
||||
(+`SegmentSnapshotMember`), `CampaignVariant` (A/B), `KontaktMarketingConsent` (in email.prisma, **nicht**
|
||||
crm.prisma), `Unsubscribe`; `Campaign` (+`recipientSegmentId`/`scheduledAt`/`timezone`/`previewText`/
|
||||
`footerHtml`/`isDemoData`/`archivedAt`), `OutboundEmail` (+`kontaktId`/`campaignVariantId`/`unsubscribeToken`/
|
||||
`trackingToken`). Bare `tenantId`/`kontaktId`-Scalars. RLS-Artefakt `prisma/rls/email-isolation.sql` +
|
||||
`TENANT_SCOPED_MODELS`-Registry nachgezogen. `models.test.ts` (11) grün.
|
||||
- **S2 Segment-Engine** (typisiert, Kontakt-only): pure `segmentFilter.ts` (Validierung Tiefen-/Node-Cap,
|
||||
Lookup-Collection, JSONB→`Prisma.KontaktWhereInput`) + `SegmentService.server.ts` (Lookup-Auflösung Tag/
|
||||
Custom-Feld, Auto-Exclusion archiviert/keine-Email/Unsubscribe/Suppression/kein-Double-OptIn als
|
||||
non-overlapping Breakdown, Live-Count+Sample, Snapshot). 13 Tests.
|
||||
- **S3 Versand-Engine**: pure `personalization.ts` (Token inkl. `cf.<key>`, idempotente Footer-Injektion) +
|
||||
`campaignSendPlan.ts` (A/B per d'Hondt interleaved, Throttle→BullMQ-`delay`, timezone-`startAt`) +
|
||||
`CampaignSendService.server.ts` (Audience→harter Checklisten-Gate→Suppression-Re-Check→OutboundEmail-Row→
|
||||
ein `mail.transactional`-Job je Empfänger mit List-Unsubscribe-Header; kein neuer Worker-Queue). 14 Tests.
|
||||
- **S4 Double-Opt-In + Ein-Klick-Abmeldung**: `marketingToken.server.ts` (HMAC unsub/optin, 5 Tests) +
|
||||
`ConsentService.server.ts` (startDoubleOptIn/confirmOptIn/unsubscribeKontakt, idempotent) + public Routes
|
||||
`optin.$token` + `unsubscribe.$token` (erweitert um Marketing-Token, RFC-8058 One-Click). i18n `marketing.optin.*`.
|
||||
- **S5 VMs** (`packages/view-models/src/email/`, pure): `presend-checklist.vm` (kanonischer Checklisten-Kern,
|
||||
von der Send-Engine importiert), `campaign-detail.vm` (KPI%/Klick-Map/Pro-Empfänger/A-B), `campaign-list.vm`,
|
||||
`segment.vm`. 7 Tests.
|
||||
- **S6 Permissions**: `emailMarketing.view/.manage` in 4 Registries (permissions-seed Array+Rollen-Map,
|
||||
DefaultPermissions, roleDisplay-Scope, i18n en/de). roleDisplay.test (7) grün.
|
||||
- **S7 Segmente-Route** (`app.$tenant/email-marketing/segments.tsx`, reachable, gated, i18n en/de, Empty-State):
|
||||
Liste + Erstellen aus AND/OR-Filter + Live-Preview gegen typisierte Kontakte. Tab im Modul-Shell.
|
||||
|
||||
**Proof:** 46/46 neue Unit-Tests grün; `@hivegrid/web`+`@hivegrid/db`+`@hivegrid/view-models`+`@hivegrid/cli`
|
||||
typecheck 0 Fehler; `prisma validate` + `db push` (lokal) grün; `verify:i18n` exit 0 (Parität en/de);
|
||||
hardcoded-strings auf Baseline; lefthook pre-commit je Commit grün. 7 Commits (S1–S7) auf `feat/AK-P14b`.
|
||||
|
||||
### DoD-Stand
|
||||
- [~] Block-/Rich-Text-Editor + Tokens (inkl. Custom-Felder) + Pflicht-Footer + Desktop/Mobile-Vorschau —
|
||||
Token-/Footer-Logik ✅ (pure, getestet); Rich-Text-Editor + Vorschau-UI **blockiert (David D-KAMP)**.
|
||||
- [x] Segmente: verschachtelter AND/OR-Baum, Live-Auflösung, Auto-Exclusion, dynamisch/statisch — ✅
|
||||
(Kontakt-Dimensionen). Lead/Projekt-Dimension **blockiert (R-P7/R-P8 offen)**.
|
||||
- [x] Versand-Engine (BullMQ, Rate-Limit/Throttling, timezone-Scheduling) — ✅ (Throttle via per-Empfänger-
|
||||
`delay`-Spreizung; worker-seitiger Limiter unverändert).
|
||||
- [~] Double-Opt-In + Suppression-Check vor Versand + One-Click-Unsubscribe + List-Unsubscribe-Header — ✅
|
||||
Kern; volle Form-Widget-Verdrahtung (Submission→Opt-In-Mail-Trigger) **teilweise** (Service da, Auto-Trigger offen).
|
||||
- [x] Pflicht-Checkliste (Domain/Unsub/Impressum/Subject/>0/Daily-Cap) + Block + Fix-Link — ✅ Logik + harter
|
||||
Server-Gate; Checklisten-**UI** Teil des bespoke Editors (blockiert David).
|
||||
- [x] Reporting (Open/Click/Bounce/Abmeldung %, Klick-Map, Pro-Empfänger) + A/B-Varianten — ✅ VMs + A/B-Schema/
|
||||
-Versand; Reporting-**UI** blockiert (David).
|
||||
- [x] Konnektor `tenantHasModule('email')` + RLS + `is_demo_data` — ✅ (Feature `email-marketing` + RLS-Artefakt
|
||||
+ `emailMarketing.*`-Permissions).
|
||||
|
||||
**Stand: 5/7 DoD voll + 2 partiell ≈ ~78 % done.** Extern blockiert: bespoke Kampagnen-Editor + Reporting-UI +
|
||||
Unsubscribe-Page-Branding (Davids `D-KAMP`/`D-P14`-Delta), Lead/Projekt-Segmentdimension (R-P7/R-P8), e2e-Smoke
|
||||
(braucht Stack+Seed) + Visual-Regression (braucht finale UI), Form-Widget→Opt-In-Auto-Trigger. Selbst-machbar
|
||||
offen: Form-Widget-Trigger verdrahten (klein). **Nicht** anfassen: Alt-Pfad-Migration (R-P14C, Raffael).
|
||||
|
||||
## Dev-Nachzug (nach Davids `D-KAMP`/`D-P14`-Delta)
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- **Konflikt-Risiko:** gering — additive Tabellen; `apps.prisma` + `crm.prisma` nur lesend für Relationen.
|
||||
|
||||
## Was wir bauen
|
||||
Den **AirCall-Adapter** als Referenz-Implementierung hinter dem `TelephonyProviderInterface` (HalloPetra dockt v2 an → P33): **Click-to-Call** und **Inbound-Anrufe** über eine Apps-Engine-`DataConnection` (verschlüsselte OAuth-Tokens, Webhook-Secret). Unbekannte Nummern legen automatisch einen **Lead** an, bekannte werden auf Kontakt/Lead/Projekt **gematcht** (Hard/Soft mit Confidence + Override). Anrufe landen im **Activity-Stream** (in/out/verpasst), nach dem Anruf gibt es einen **Markdown-Notiz-Editor** (Whisper-Diktat ist v2) und eine Recording-Anzeige. **Anruf-Events** stehen als Automation-Trigger bereit (verpasster Anruf → Rückruf-Aufgabe, verdrahtet in P12).
|
||||
Den **AirCall-Adapter** als Referenz-Implementierung hinter dem `TelephonyProviderInterface` (HalloPetra dockt v2 an → P33): **Click-to-Call** und **Inbound-Anrufe** über eine Apps-Engine-`DataConnection` (verschlüsselte OAuth-Tokens, Webhook-Secret). Unbekannte Nummern legen automatisch einen **Lead** an, bekannte werden auf Kontakt/Lead/Projekt **gematcht** (Hard/Soft mit Confidence + Override). Anrufe landen im **Activity-Stream** (in/out/verpasst), nach dem Anruf gibt es einen **Markdown-Notiz-Editor** und eine Recording-Anzeige. **Whisper-Diktat entfällt nach AD-26.** **Anruf-Events** stehen als Automation-Trigger bereit (verpasster Anruf → Rückruf-Aufgabe, verdrahtet in P12).
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-CHAT` + `D-P15`)
|
||||
| Screen [Route] | Kern-Inhalt |
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-P15a` (P15-Vorlauf), `AK-P4a` (Files — Chat-Anhänge via FileEntityLink)
|
||||
- **Blocks:** **Demo 2** (Noah-Demo #2, Gate W3)
|
||||
- **Läuft parallel zu:** Raffaels Kern-Kette. Eigene `prisma/schema/chat.prisma` (AD-21); entity-linked Channels referenzieren Projekt/Deal aus dem CRM-Kern (R-P6/P8) read-only. **Sync-Punkt:** ProjektMitglied-Sync braucht den Projekt-Kern (P8/P9).
|
||||
- **Läuft parallel zu:** Raffaels Kern-Kette. Eigene `prisma/schema/chat.prisma` (AD-21); entity-linked Channels referenzieren Projekt aus dem CRM-Kern (R-P6/P8) read-only. **Sync-Punkt:** ProjektMitglied-Sync braucht den Projekt-Kern (P8/P9).
|
||||
- **Konflikt-Risiko:** keines auf Schema-Ebene; `crm.prisma`/`files.prisma` nur lesend.
|
||||
|
||||
## Was wir bauen
|
||||
Den **Real-Time-Team-Chat (Bespoke, AD-9)**: WebSocket-Messaging mit **Presence, Typing-Indicator, Read-Receipts** über Redis-Pub/Sub (Scale-Out), drei **Channel-Typen** (public/private Group, DM, entity-linked Projekt/Deal mit ProjektMitglied-Sync), **@Mentions** (User/Projekt/Deal/Kontakt/Firma) mit Autocomplete, **Threads, Reactions, Pins** (max 50), **File-Uploads** (via Files), Link-Previews, **Mark-as-Task** und **Cross-Channel-Suche**. Mobile-First für Monteure (vereinfachte View, Foto-Direct-Upload, Push). Ein **Admin-Audit-Modus** (`read_all_channels`) erfordert Begründungs-Pflicht je Session + DM-Audit-Banner und wird audit-geloggt.
|
||||
Den **Real-Time-Team-Chat (Bespoke, AD-9)**: WebSocket-Messaging mit **Presence, Typing-Indicator, Read-Receipts** über Redis-Pub/Sub (Scale-Out), drei **Channel-Typen** (public/private Group, DM, entity-linked Projekt mit ProjektMitglied-Sync), **@Mentions** (User/Projekt/Kontakt/Firma) mit Autocomplete, **Threads, Reactions, Pins** (max 50), **File-Uploads** (via Files), Link-Previews, **Mark-as-Task** und **Cross-Channel-Suche**. Mobile-First für Monteure (vereinfachte View, Foto-Direct-Upload, Push). Ein **Admin-Audit-Modus** (`read_all_channels`) erfordert Begründungs-Pflicht je Session + DM-Audit-Banner und wird audit-geloggt.
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-CHAT` + `D-P15`)
|
||||
| Screen [Route] | Kern-Inhalt |
|
||||
@@ -29,10 +29,10 @@ Den **Real-Time-Team-Chat (Bespoke, AD-9)**: WebSocket-Messaging mit **Presence,
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **WebSocket-Real-Time**: Presence, Typing, Read-Receipts, Redis-Pub/Sub (AD-9), Optimistic-UI, Reconnect
|
||||
- [ ] **3 Channel-Typen**: public/private Group, DM, entity-linked (Projekt/Deal) mit **ProjektMitglied-Sync**
|
||||
- [ ] **3 Channel-Typen**: public/private Group, DM, entity-linked (Projekt) mit **ProjektMitglied-Sync**
|
||||
- [ ] **@Mentions** mit Autocomplete + **Threads** + **Reactions** + **Pins** (max 50) + **File-Uploads** (Files) + Link-Previews + **Mark-as-Task**
|
||||
- [ ] Channel-Index (Filter/Suche/Pin/Mute/Archiv + Channel-Browser) + **Cross-Channel-Suche** + Mentions-Aggregat
|
||||
- [ ] Entity-linked Channel erscheint im Projekt-/Deal-Detail (P8/P9)
|
||||
- [ ] Entity-linked Channel erscheint im Projekt-Detail (P8/P9)
|
||||
- [ ] **Admin-Audit** (`read_all_channels`): Begründungs-Pflicht je Session + DM-Audit-Banner, audit-geloggt
|
||||
- [ ] **Mobile-First-Variante** für Monteure (vereinfachte View, Foto-Direct-Upload, Push)
|
||||
- [ ] Konnektor `tenantHasModule('chat')`; RLS auf allen Tenant-Tabellen
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P16 — Kalender, Booking, Forms & Signatur |
|
||||
| **Cycle/Fenster** | W4 — USP, Härtung & Launch · fällig 2026-07-04 · Gate W4 |
|
||||
| **Status** | geplant |
|
||||
| **Status** | in Arbeit (~83 % DoD, Backend+Routen+Sync+Worker+Write-Pfad fertig; Rest David D-P16/D-BOOK + Staging-DB) |
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `EXT-10` (Google-Cloud-Projekt + OAuth-Consent für Calendar-API), `R-P5` (Apps-Engine — Kalender läuft als `DataConnection`)
|
||||
@@ -25,12 +25,12 @@ Den **bidirektionalen Kalender-Sync mit Google** (Outlook-Adapter → v2, Interf
|
||||
| Kalender-Verbindungs-Detail [`/settings/integrations/calendar/:id`] | gewählte Kalender, Sync-Status, Konflikt-Liste (letzte 50, Klartext + Resolution) |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **Bidirektionaler Google-Sync** (CalendarAdapter, Auto-Token-Refresh, Webhook-Subscriptions); Outlook → v2 (Interface provider-agnostisch)
|
||||
- [ ] HiveGrid-Termine (Booking/Projekt/Plantafel) gehen **extern**, externe Events kommen **read-only** für Verfügbarkeit
|
||||
- [ ] **Connection pro User**; externe Event-Inhalte nie an andere User leaken (nur „belegt von X bis Y")
|
||||
- [ ] **Last-Write-Wins-Konflikt** mit Klartext-Notification + Konflikt-Log (letzte 50)
|
||||
- [x] **Bidirektionaler Google-Sync** (CalendarAdapter, Auto-Token-Refresh, Webhook-Subscriptions); Outlook → v2 (Interface provider-agnostisch)
|
||||
- [x] HiveGrid-Termine (Booking/Projekt/Plantafel) gehen **extern**, externe Events kommen **read-only** für Verfügbarkeit
|
||||
- [x] **Connection pro User**; externe Event-Inhalte nie an andere User leaken (nur „belegt von X bis Y")
|
||||
- [x] **Last-Write-Wins-Konflikt** mit Klartext-Notification + Konflikt-Log (letzte 50)
|
||||
- [ ] Kalender-Ansicht mit Quell-Markern (HiveGrid + extern)
|
||||
- [ ] Konnektor `tenantHasModule('calendar')`; RLS auf allen Tenant-Tabellen; OAuth verschlüsselt (AD-8, via Apps-Engine)
|
||||
- [x] Konnektor `tenantHasModule('calendar')`; RLS auf allen Tenant-Tabellen; OAuth verschlüsselt (AD-8, via Apps-Engine)
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/calendar.prisma`): `CalendarConnection` (provider google, oauth_refresh_token **verschlüsselt**, status — **pro User**), `CalendarEvent` (source internal/external_synced, starts/ends_at, is_busy, internal_entity_link polymorph), `CalendarEventMapping` (internal↔external_id), `CalendarSyncCursor` (cursor_token), `CalendarWebhookEvent` (payload). `apps.prisma` nur lesend (DataConnection-Bezug, AD-21).
|
||||
@@ -50,3 +50,73 @@ Den **bidirektionalen Kalender-Sync mit Google** (Outlook-Adapter → v2, Interf
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P16a` aktualisiert
|
||||
|
||||
## Umsetzungs-Notiz (2026-06-30, Akeshan) — ~70 % DoD
|
||||
|
||||
Branch `feat/AK-P16a`, 6 lokale Commits (`ca0a410` → `4ac8cb07`), nichts gepusht,
|
||||
Plane-Status unverändert. Alle Verifikations-Gates grün: `pnpm prisma validate`,
|
||||
`@hivegrid/{web,db,view-models,integrations}` typecheck **0 Fehler**, `verify:i18n`
|
||||
exit 0, **36 Calendar-Tests grün** (8 Files: availability 8, conflict 6, mapping 3,
|
||||
oauthState 5, webhook-renewal 5, VM 5, auth-boundary 2, drift 2). lefthook pre-commit
|
||||
je Commit grün.
|
||||
|
||||
**DoD-Stand:**
|
||||
- [x] **Connection pro User + Leak-Schutz (DoD 3)** — `calendar.prisma` per-User,
|
||||
`availability.ts` exponiert nur Busy-Intervalle (getestet), Owner-Check im Guard.
|
||||
- [x] **Konnektor + RLS + OAuth verschlüsselt (DoD 6)** — Gate via
|
||||
`tenantHasModule('integrations')` (Freigabe 2026-06-29: Kalender = Apps-Engine-
|
||||
Integration), `calendar-isolation.sql` (deferred cutover), Token-Seal per-Tenant-DEK
|
||||
(AD-8).
|
||||
- [~] **Bidirektionaler Sync (DoD 1)** — CalendarAdapter + Google-REST/OAuth-Client
|
||||
(fetch, kein SDK), Auto-Token-Refresh (Adapter + `ensureFreshCreds`), Webhook
|
||||
subscribe (Connect) + receive (`/webhooks/calendar/google`) + Renewal-Entscheidung.
|
||||
**Offen:** Worker-Cron (Refresh/Renewal/Webhook-Drain) + HiveGrid→extern Write-Pfad.
|
||||
- [~] **HiveGrid extern / externe read-only (DoD 2)** — externer Delta→Cache-Sync
|
||||
(Cursor, 410→Full-Resync) fertig + Initial-Sync nach Connect. **Offen:** Write-Pfad
|
||||
HiveGrid→Google (`CalendarWriteService`). **Update 2026-06-30 (AK-P16b gemerged-ready):**
|
||||
AK-P16b legt nun je Buchung ein lokales `CalendarEvent` (`source: hivegrid_internal`,
|
||||
`isBusyForAvailability`) an → die Quell-Events für den Write-Pfad **existieren jetzt**;
|
||||
der Write-Pfad ist damit **entsperrt** (nur noch P9/Plantafel offen). Bauen bleibt
|
||||
AK-P16a-Scope (Nachzug), schließt zugleich AK-P16b-DoD-2-extern.
|
||||
- [~] **Last-Write-Wins + Konflikt-Log (DoD 4)** — Resolver-Logik (getestet) +
|
||||
`CalendarConflict`-Tabelle + Detail-VM mit Konflikt-Liste. **Offen:** Persistenz +
|
||||
`notify()` am Write/Mapped-Pfad (entsteht mit dem Write-Pfad).
|
||||
- [~] **Kalender-Ansicht Quell-Marker (DoD 5)** — erreichbare Route `/calendar` + VM
|
||||
mit Quell-Markern. **Blockiert:** bespoke Monats-/Wochen-Grid → David `D-P16`
|
||||
(+ Visual-Regression, Hot-Swap-Adapter).
|
||||
|
||||
**Selbst machbar offen:** Worker-Crons (Architektur-Entscheid: Sync-Orchestrierung
|
||||
liegt in `apps/web`, Worker kann sie nicht importieren — entweder nach
|
||||
`packages/integrations` ziehen oder interner Trigger-Endpoint), `CalendarWriteService`
|
||||
+ Konflikt-Persistenz. **Extern blockiert:** bespoke UI (David `D-P16`), `db push` +
|
||||
RLS-Live-Check + Visual-Regression (Staging-DB), Live-OAuth (EXT-15-Google-Verify).
|
||||
|
||||
**Hinweise:** Redirect-URI an EXT-10 ausgerichtet (`/integrations/google/oauth/callback`,
|
||||
App-Root). Env-Var-Namen `GOOGLE_CALENDAR_CLIENT_ID/_SECRET/_REDIRECT_URI` +
|
||||
`GOOGLE_CALENDAR_WEBHOOK_URL` — Coolify-Abgleich beim Live-Test nötig. Connect/Disconnect
|
||||
sind Account-Events → Audit (M001-Middleware), kein Activity-Stream (CRM-Subject-only).
|
||||
|
||||
## Umsetzungs-Notiz (2026-06-30, Akeshan) — ~83 % DoD
|
||||
|
||||
Nachzug für den übrig gebliebenen AK-P16a-Teil auf `feat/AK-P16a`: Worker-Crons
|
||||
und Maintenance-Handler für `calendar-token-refresh`, `calendar-webhook-renewal`,
|
||||
`calendar-webhook-drain` und `calendar-internal-write-sync` ergänzt. Die
|
||||
Calendar-Orchestrierung liegt jetzt worker-sicher in `@hivegrid/integrations`
|
||||
(`calendar-worker-tasks.server`) und importiert keine `apps/web`-Module. Der
|
||||
Write-Pfad spiegelt lokale `CalendarEvent`-Rows (`source: hivegrid_internal`,
|
||||
aktuell aus AK-P16b-Bookings) nach Google, persistiert `CalendarEventMapping`,
|
||||
zieht Updates/Deletes nach und schreibt bei Provider-ETag-Konflikten
|
||||
`CalendarConflict` + In-App-Notification mit `calendar.conflict.*`.
|
||||
|
||||
**DoD-Stand:** 5/6 DoD = ~83 % done. Selbst machbarer Backend-Rest ist erledigt.
|
||||
Offen bleibt DoD 5 als extern blockiert: bespoke Monats-/Wochen-Grid mit
|
||||
Quell-Markern und Visual-/Hot-Swap-Abschluss warten auf David `D-P16`/`D-BOOK`.
|
||||
Staging-DB-Checks (`db push`, RLS-Live) bleiben wie bisher env-blockiert; keine
|
||||
Migration auf dem Branch erzeugt (AD-21).
|
||||
|
||||
**Proof grün:** `pnpm prisma validate`; `@hivegrid/{db,queue,integrations,worker,web,view-models}`
|
||||
typecheck; `pnpm run verify:i18n`; gezielte Vitest-Suite **40 Tests grün**
|
||||
(Calendar Services/Auth 24, Calendar Worker/Cron 3, Webhook-Renewal 5,
|
||||
Calendar/Booking VMs 8). Zusätzlich wurde ein AK-P16b-VM-Testimport auf
|
||||
NodeNext-konformes `.js` korrigiert, damit `@hivegrid/view-models` wieder
|
||||
typecheck-grün ist.
|
||||
@@ -6,7 +6,7 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P16 — Kalender, Booking, Forms & Signatur |
|
||||
| **Cycle/Fenster** | W4 — USP, Härtung & Launch · fällig 2026-07-05 · Gate W4 |
|
||||
| **Status** | geplant |
|
||||
| **Status** | in Arbeit (~92 % gewichtet; 5/7 DoD vollständig, 2/7 teilfertig; selbst machbarer Server-Rest erledigt, Rest David D-P16/AK-P16c + Staging-DB-Ausführung) |
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-P16a` (Kalender-Sync — Slot-Verfügbarkeit braucht Busy-Slots aus Google)
|
||||
@@ -27,13 +27,13 @@
|
||||
| Verfügbarkeit [`/availability`] | Wöchentliche Standard-Regeln (mehrere Zeitfenster/Tag), Datums-Ausnahmen (frei/Custom-Window), Timezone |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Öffentliche **Booking-Seiten** berechnen Slots **timezone-aware** (Regeln, Ausnahmen, Kalender-Sync, Puffer, Vorlauf)
|
||||
- [ ] Nach Buchung: **Lead oder Activity** (Email-Match), **FormSubmission**, **Bestätigungs-Email** (.ics) — **Race-Schutz** beim Submit
|
||||
- [ ] **iFrame-Embed** + Public-URL mit Copy
|
||||
- [ ] Nur **eine** Zuteilungs-Strategie „Erster der frei ist" (Round-Robin/Provider-Wahl → v2)
|
||||
- [ ] **Storno/Verschiebung/Re-Assign/No-Show** (M129 VOLL) inkl. Auto-Mail bei Cancel
|
||||
- [ ] Permission-Bundle `bookings.*` (read_own/read_all/write/admin) serverseitig Owner-gefiltert
|
||||
- [ ] Konnektor `tenantHasModule('booking')`; RLS auf allen Tenant-Tabellen
|
||||
- [x] Öffentliche **Booking-Seiten** berechnen Slots **timezone-aware** (Regeln, Ausnahmen, Kalender-Sync, Puffer, Vorlauf)
|
||||
- [~] Nach Buchung: **Lead oder Activity** (Email-Match), **FormSubmission**, **Bestätigungs-Email** (.ics) — **Race-Schutz** beim Submit · *Kontakt-Email-Match → Activity, sonst echtes `InboxLead` + `Appointment.leadId`; bei gesetzter `intakeFormId` echtes `FormSubmission` + `Appointment.formSubmissionId`, sonst `intakeAnswers` Json-Fallback*
|
||||
- [x] **iFrame-Embed** + Public-URL mit Copy
|
||||
- [x] Nur **eine** Zuteilungs-Strategie „Erster der frei ist" (Round-Robin/Provider-Wahl → v2)
|
||||
- [~] **Storno/Verschiebung/Re-Assign/No-Show** (M129 VOLL) inkl. Auto-Mail bei Cancel · *Service + Route-Action-Contract vollständig; Reschedule/Re-Assign-Picker-UI → David D-P16*
|
||||
- [x] Permission-Bundle `bookings.*` (read_own/read_all/write/admin) serverseitig Owner-gefiltert
|
||||
- [x] Konnektor `tenantHasModule('booking')`; RLS auf allen Tenant-Tabellen *(RLS-SQL als deferred Cutover wie Kalender)*
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/booking.prisma`): `BookingPage` (slug, duration_minutes, assignment_strategy, is_published, intake_form_id), `BookingPageProvider` (M:N User), `AvailabilityRule`/`AvailabilityException`, `Appointment` (provider_user_id, start/end, status scheduled/held/cancelled/no_show/rescheduled, kontakt_id|lead_id, form_submission_id). `calendar.prisma`/`crm.prisma`/`forms.prisma` nur über Relationen (AD-21).
|
||||
@@ -53,3 +53,105 @@
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P16b` aktualisiert
|
||||
|
||||
## Umsetzungs-Notiz (2026-06-30, Akeshan) — ~85 % DoD
|
||||
|
||||
Branch `feat/AK-P16b` (von `feat/AK-P16a` abgezweigt, AD-21), **8 Commits** (`fecf6f19f`
|
||||
→ `b262dabe8`), nicht gepusht, Plane-Status unverändert. Alle Verifikations-Gates grün:
|
||||
`pnpm prisma validate`, `@hivegrid/{web,db,view-models,entitlements,queue}` +
|
||||
`@hivegrid/activity` typecheck **0 Fehler**, `verify:i18n` (i18n-check) exit 0,
|
||||
**26 Booking-Unit-Tests grün** (slotEngine 11, ics 5, transitions 5, VM 3,
|
||||
auth-boundary 2). lefthook pre-commit je Commit grün.
|
||||
|
||||
**4 Forks (User-Freigabe 2026-06-30):** (1) Lead-Gap → Kontakt-Match/Create +
|
||||
`logActivity`, `leadId` reserviert R-P7. (2) Intake-Gap → `intakeAnswers` Json am
|
||||
Appointment, `formSubmissionId` reserviert AK-P16c. (3) Modul-Gate → eigener
|
||||
`booking` Feature-Key (`tenantHasModule('booking')`). (4) Scope → Backend +
|
||||
funktionale Routen inkl. minimal-funktionaler öffentlicher Strecke.
|
||||
|
||||
**DoD-Stand:**
|
||||
- [x] **Slot-Engine timezone-aware (DoD 1)** — `slotEngine.ts` pure (Regeln/Ausnahmen,
|
||||
Buffer, min-notice/max-advance, Increment, `first_available`-Union); Google-Busy
|
||||
via AK-P16a `availability.ts` (leak-safe). 11 Tests inkl. DST. `/b/{tenant}/{slug}`.
|
||||
- [~] **Post-Booking + Race (DoD 2)** — Serializable-Tx Slot-Re-Check + Postgres
|
||||
`EXCLUDE USING gist`-Backstop (`booking-constraints.sql`); Kontakt-Match/Create +
|
||||
`appointment_scheduled`-Activity; `intakeAnswers` Json; lokales CalendarEvent;
|
||||
Bestätigungsmail + **.ics-Attachment** (`buildIcs` pure). **Offen:** echte
|
||||
`FormSubmission` (AK-P16c), echtes CRM-`Lead` (R-P7) — bewusst reserviert.
|
||||
- [x] **iFrame-Embed + Public-URL (DoD 3)** — `/b/.../embed` (frame-ancestors) +
|
||||
Embed-Code-Generator + Public-URL-Copy im Page-Editor.
|
||||
- [x] **Eine Strategie (DoD 4)** — `first_available` implementiert; Enum trägt alle 3
|
||||
(round_robin/specific_provider_choice → v2, additiv).
|
||||
- [~] **Lifecycle M129 (DoD 5)** — `BookingLifecycleService`: held/no_show/cancel/
|
||||
reschedule/reassign mit lokalem CalendarEvent-Nachzug + Auto-Mail + Activity +
|
||||
`notify()`; Detail-Route mit held/no_show/cancel-Actions (CSRF+Owner). **Offen:**
|
||||
Reschedule-/Re-Assign-**Picker-UI** (Slot-/Provider-Auswahl) → David D-P16.
|
||||
- [x] **bookings.* owner-gefiltert (DoD 6)** — 4 Registries, `bookingAccess.server.ts`
|
||||
(Modul-Gate + Permission + Appointment-Owner-Check, 404 statt 403), Liste
|
||||
server-seitig owner-gefiltert. Auth-Boundary-Test.
|
||||
- [x] **Konnektor + RLS (DoD 7)** — `booking` Feature-Key + Gate; `booking-isolation.sql`
|
||||
(deferred Cutover wie Kalender), `booking-constraints.sql` (gist + btree_gist).
|
||||
|
||||
**Selbst machbar offen (~15 %):** `/availability`-Editor-UI (M06 F — Modelle +
|
||||
Slot-Engine konsumieren Verfügbarkeit bereits; nur die Pflege-Oberfläche fehlt) ·
|
||||
Reschedule/Re-Assign-Picker-Verdrahtung. **Staging-DB-blockiert:** `prisma db push` +
|
||||
`psql -f booking-constraints.sql` + RLS-Live-Check + **Race-Integrationstest**
|
||||
(db-gated) — lokal keine DB gestartet, wie AK-P16a deferred. **David D-P16-blockiert:**
|
||||
bespoke öffentliche Booking-Page + Reschedule-Picker + Visual-Regression (DoD 21) +
|
||||
Hot-Swap-Drill (DoD 20).
|
||||
|
||||
**Hinweise:** Mail-Attachment-Durchreichung im Queue-Provider ergänzt (Payload trug
|
||||
`attachments` bereits, nur der Provider-Hop ließ sie fallen). 3 Booking-Activity-Typen
|
||||
(`appointment_cancelled/rescheduled/no_show`) zur Activity-Union ergänzt. Connect/
|
||||
Lifecycle sind CRM-Subject-Activities (kein eigenes Booking-Subject).
|
||||
|
||||
## Umsetzungs-Notiz (2026-07-01, Raffael) — ~90 % gewichtet, 5/7 DoD vollständig
|
||||
|
||||
Direkt auf `main` serverseitig nachgezogen, um den ohne Screens selbst machbaren
|
||||
P16b-Rest zu maximieren.
|
||||
|
||||
**Nachzug:**
|
||||
- Public Booking erzeugt bei fehlendem Kontakt jetzt ein echtes `InboxLead` über
|
||||
`createLead(...)`, verknüpft es über `Appointment.leadId` und schreibt die Activity
|
||||
auf Kontakt oder Lead. Der alte automatische Kontakt-Create ist raus.
|
||||
- Detail-Route akzeptiert serverseitig die M129-Intents `reschedule` und `reassign`
|
||||
mit `bookings.write` bzw. `bookings.admin`; die Picker-UI bleibt D-P16-blockiert.
|
||||
- `AvailabilityService.server.ts` ergänzt den atomaren Replace-/Read-Contract für
|
||||
`AvailabilityRule`/`AvailabilityException` inkl. Zeitfenster-, Datum- und
|
||||
Timezone-Validierung. Die `/availability`-Oberfläche bleibt screen-blockiert.
|
||||
- Tests ergänzt: Availability-Service-Unit-Test, Post-Booking-Static-Guard,
|
||||
Lifecycle-Action-Authorization-Guard.
|
||||
|
||||
**DoD-Stand nach Nachzug:**
|
||||
- Vollständig abgehakt: DoD 1, 3, 4, 6, 7 = **5/7 vollständig**.
|
||||
- Teilfertig: DoD 2 ist serverseitig bis auf echte `FormSubmission` erledigt
|
||||
(`AK-P16c`); DoD 5 ist serverseitig erledigt, aber Reschedule/Reassign-Picker-UI
|
||||
wartet auf David D-P16.
|
||||
- Offen/blockiert: echte `FormSubmission` (`AK-P16c`), D-P16-Screens/Visuals,
|
||||
Staging-DB-Cutover (`prisma db push`, `booking-constraints.sql`, RLS-Live-Check,
|
||||
Race-Integrationstest).
|
||||
|
||||
**Proof lokal:** `pnpm exec vitest run apps/web/app/modules/booking/services/AvailabilityService.server.test.ts apps/web/app/modules/booking/services/booking-postbooking-boundary.test.ts apps/web/app/modules/booking/routes/booking-auth-boundary.test.ts` → 8/8 grün; `pnpm typecheck` → grün.
|
||||
|
||||
## Umsetzungs-Notiz (2026-07-01, Raffael) — ~92 % gewichtet, selbst machbarer Server-Rest erledigt
|
||||
|
||||
Noch ohne Screens nachgezogen:
|
||||
- `BookingPage.intakeFormId` wird im Public-Booking-Resolve mitgeladen.
|
||||
- Public Booking schreibt innerhalb derselben Serializable-Transaktion eine echte
|
||||
`FormSubmission`, sobald die BookingPage ein Intake-Form referenziert, und verlinkt
|
||||
`Appointment.formSubmissionId`. Ohne `intakeFormId` bleibt `intakeAnswers` der
|
||||
Fallback, damit bestehende BookingPages nicht brechen.
|
||||
- Statische DB-Artefakt-Guards ergänzt: `booking-constraints.sql` muss die
|
||||
`appointment_no_overlap`-Exclusion-Constraint für `scheduled`/`held` enthalten;
|
||||
`booking-isolation.sql` muss alle fünf Booking-Tabellen mit RLS/FORCE/GUC-Policy
|
||||
und `hivegrid_app`-Grant abdecken.
|
||||
|
||||
**DoD-Stand nach Nachzug:**
|
||||
- DoD 2 ist serverseitig maximal: Lead/Activity, FormSubmission bei konfiguriertem
|
||||
Intake-Form, .ics-Mail, Serializable Slot-Re-Check und SQL-Backstop sind vorhanden.
|
||||
Vollständig grün wird der Punkt erst mit AK-P16c-Renderer/Form-Auswahl und echter
|
||||
Staging-DB-Ausführung.
|
||||
- DoD 5 bleibt screen-blockiert für den Reschedule-/Reassign-Picker; Service und
|
||||
Action-Contract sind bereits erledigt.
|
||||
|
||||
**Proof lokal:** `pnpm exec vitest run apps/web/app/modules/booking/services/booking-postbooking-boundary.test.ts apps/web/app/modules/booking/services/booking-db-artifacts.test.ts apps/web/app/modules/booking/services/AvailabilityService.server.test.ts apps/web/app/modules/booking/routes/booking-auth-boundary.test.ts` → 11/11 grün; `pnpm typecheck`, `pnpm lint`, `pnpm verify:i18n`, `pnpm test` und `pnpm build` → grün.
|
||||
@@ -6,7 +6,7 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P16 — Kalender, Booking, Forms & Signatur |
|
||||
| **Cycle/Fenster** | W4 — USP, Härtung & Launch · fällig 2026-07-06 · Gate W4 |
|
||||
| **Status** | geplant |
|
||||
| **Status** | Dev-Anteil umgesetzt · Design-Delta offen |
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-INFRA2` (mCaptcha + Rate-Limit-Infra für öffentliche Endpunkte), `R-P7` (Lead-Inbox — Post-Submit-Action „Lead anlegen")
|
||||
@@ -26,13 +26,47 @@ Einen Drag-&-Drop-**Form-Builder auf Google-Forms-Niveau**: **12 Feld-Typen**, *
|
||||
| Submissions [`/forms/:id/submissions`] | Antwort-Tabelle (Spalten = Felder), Filter, CSV; Detail: alle Antworten, Submitter-Metadata, **Action-Log mit Re-Run**, File-/Foto-Vorschau, PDF-Export |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **12 Feld-Typen** + **Conditional Logic** + Validierung pro Feld + geseedete **SHK-Templates** + Live-Vorschau
|
||||
- [ ] **Embed**: öffentliche URL + QR + iFrame (personalisierte Links / JS-Snippet → v2)
|
||||
- [ ] **Post-Submit-Actions** (Lead/Kontakt/Projekt/Task, Email, Webhook, Automation-Trigger, Auto-Match) mit **Field-Mapping** + **ActionLog** (Re-Run)
|
||||
- [ ] **Rate-Limit + mCaptcha + DSGVO-Pflicht** auf öffentlichen Endpunkten; Race-Schutz beim Submit
|
||||
- [ ] **Form-Renderer als wiederverwendbare Komponente** (Booking-Intake, HR-Anträge)
|
||||
- [ ] Permission-Bundle `forms.*` (basic/builder/publisher/admin) serverseitig Owner-gefiltert
|
||||
- [ ] Konnektor `tenantHasModule('forms')`; RLS auf allen Tenant-Tabellen
|
||||
- [x] **12 Feld-Typen** + **Conditional Logic** + Validierung pro Feld + geseedete **SHK-Templates** + Live-Vorschau
|
||||
- [x] **Embed**: öffentliche URL + QR + iFrame (personalisierte Links / JS-Snippet → v2)
|
||||
- [x] **Post-Submit-Actions** (Lead/Kontakt/Projekt/Task, Email, Webhook, Automation-Trigger, Auto-Match) mit **Field-Mapping** + **ActionLog** (Re-Run)
|
||||
- [x] **Rate-Limit + mCaptcha + DSGVO-Pflicht** auf öffentlichen Endpunkten; Race-Schutz beim Submit
|
||||
- [x] **Form-Renderer als wiederverwendbare Komponente** (Booking-Intake, HR-Anträge)
|
||||
- [x] Permission-Bundle `forms.*` (basic/builder/publisher/admin) serverseitig Owner-gefiltert
|
||||
- [x] Konnektor `tenantHasModule('forms')`; RLS auf allen Tenant-Tabellen
|
||||
|
||||
## Umsetzungs-Notiz
|
||||
|
||||
**2026-07-01:** AK-P16c umgesetzt auf Branch `feat/AK-P16c-complete`.
|
||||
|
||||
- Form-Datenmodell in `prisma/schema/formWidget.prisma` erweitert (`FormField`, `FormFile`, `FormAction`, `FormActionLog`, Slug/Status/Subtype/Public-Flags, Submission-Idempotency/GDPR/Linked-Entity). Keine Migration erzeugt, AD-21 Feature-Branch-Regel eingehalten; Prisma Client regeneriert.
|
||||
- Form-Widget-Schema auf 12 Feldtypen plus Honeypot gehoben, Conditional-Logic-Engine mit Tests ergänzt und Entity-Builder/`entity-row` aus dem Routing-Kontrakt entfernt.
|
||||
- Web-Builder erweitert: Palette, Feldvalidierung, Conditional-Logic-Editor, Live-Vorschau, SHK-Template-Upsert, Public URL `/f/{tenant}/{slug}`, QR/iFrame, Publish/Re-Run, Owner-Filter und `forms.*` Permissions.
|
||||
- Public Submit gehärtet: Origin-Allowlist, Rate-Limit, mCaptcha, DSGVO-Pflicht, sichtbarkeitsgefilterte Validierung, Idempotency-Key/Race-Schutz und Outbox-Publish.
|
||||
- Worker-Pipeline erweitert: Post-Submit-Actions für Auto-Match, Lead/Kontakt/Projekt/Task, Team-/Submitter-Mail, Webhook und Automation-Trigger mit `FormActionLog`.
|
||||
- Feature/Permission-Wiring ergänzt: Entitlements, Rollen-Seed, Sidebar, UI-Registry, Tenant-Isolation-Report/RLS-Datei.
|
||||
|
||||
**2026-07-01 Nachzug selbst machbarer Rest:** Admin-Konfiguration für `FormAction` im Builder ergänzt und Save-Action synchronisiert `FormAction`-Rows. Public Submit akzeptiert multipart Uploads fuer Datei-/Foto-Felder, persistiert `File`/`FormFile`/`FileEntityLink`, queued Scan/Preview und haelt das JSON-Payload-Limit getrennt vom Upload-Request-Limit. Submissions-Ansicht zeigt feldbasierte Antwort-Tabelle mit Statusfilter, CSV/PDF-Export, Submitter-Metadata, ActionLog/Re-Run und Datei-Links. Public Renderer aus den Routes nach `apps/web/app/modules/forms/public/PublicFormRenderer.tsx` extrahiert; `/embed/forms/:formKey` und `/f/:tenant/:slug` nutzen denselben Renderer. Builder-Save synchronisiert `FormField`-Rows aus der Definition.
|
||||
|
||||
**Proof:** `pnpm exec prisma validate`; `pnpm --filter @toolctrl/web typecheck`; `pnpm --filter @toolctrl/worker typecheck`; `pnpm --filter @toolctrl/form-widget test`; `pnpm exec vitest run apps/worker/src/jobs/form-submission-route.test.ts apps/worker/src/index.test.ts packages/entitlements/src`; `pnpm exec vitest run apps/web/app/modules/formWidget/builder 'apps/web/app/routes/api.embed.forms.$formKey.submit.test.ts' 'apps/web/app/routes/app.$tenant/forms.$id.defaults.test.ts' 'apps/web/app/routes/app.$tenant/forms.$id.action.test.ts' 'apps/web/app/routes/app.$tenant/forms.$id.i18n.test.ts'`; `pnpm lint`.
|
||||
|
||||
**Stand:** 7/7 Dev-DoD grün = 100 % selbst machbarer Dev-Anteil. Ticket noch nicht final Done, solange Davids `D-FORM`/`D-P16` Design-Delta und der anschliessende Doku-/Plane-Abgleich offen sind.
|
||||
|
||||
**Push-Status:** Commit/Push erfolgt auf User-Kommando am 2026-07-01; exakten Hash siehe Git-Historie/Push-Bestaetigung.
|
||||
|
||||
**Plane:** Kommentar `2d5037a5-5c59-4a07-9a53-8a597d9b6de4` am 2026-07-01 gesetzt. Ticket bewusst nicht auf Done gesetzt, weil Davids Design-Delta noch offen ist.
|
||||
|
||||
## Noch offen
|
||||
|
||||
**Selbst machbar im Ticket-Scope:**
|
||||
- Aktuell keine offenen selbst machbaren Dev-Punkte identifiziert. Der funktionale AK-P16c-Umfang aus der DoD ist umgesetzt und lokal verifiziert.
|
||||
|
||||
**Extern/blockiert:**
|
||||
- Davids `D-FORM`/`D-P16` Design-Delta ist noch nicht eingearbeitet. Aktuell ist es eine funktionale Implementierung, aber noch nicht der finale bespoke Subframe-/Design-Stand.
|
||||
- Nach Davids Delta fehlen die geplanten Doku-Nachzüge: Spec/ENTITY-MODEL, REQUIREMENTS/PHASE-SOURCES und Plane-Ticket final abgleichen.
|
||||
|
||||
**Bewusst v2 laut Ticket-Text:**
|
||||
- Personalisierte Links/FormLink bleiben v2.
|
||||
- JS-Snippet ist laut Ticket-Text v2; v1 deckt Public URL, QR und iFrame ab.
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/forms.prisma`): `Form` (slug, subtype standard/acceptance_form/onboarding/survey, status, is_public), `FormField` (field_key, field_type 12 Typen, required, validation_rules), `FormSubmission` (submission_data JSON, linked_entity_type/id polymorph), `FormFile` (→ File), `FormAction` (action_type send_email/create_lead/create_projekt/webhook/trigger_automation, action_config). `crm.prisma`/`tasks.prisma`/`automation.prisma` nur über Actions berührt (AD-21).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# AK-P16d · P16 Angebot/Signatur (PandaDoc)
|
||||
# AK-P16d · P16 Angebot/Signatur (SES-Default + BYOK-Adapter)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -6,14 +6,13 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P16 — Kalender, Booking, Forms & Signatur |
|
||||
| **Cycle/Fenster** | W4 — USP, Härtung & Launch · fällig 2026-07-07 · Gate W4 |
|
||||
| **Status** | geplant |
|
||||
| **Status** | in Umsetzung |
|
||||
|
||||
> ⚠️ **Scope-Update (AD-25, 2026-06-21 — Raffael):** Signatur-**Default** ist jetzt der **gehärtete SES-Eigenbau**
|
||||
> (kein PandaDoc-Zwang), umgesetzt über `defineAdapter` (Adapter `ses-selfhosted`). **PandaDoc bleibt nur als
|
||||
> optionaler BYOK-Adapter** (`pandadoc-byok`, Tenant bringt eigenen Key) — Anbindungs-Spec in [[EXT-17]]. Dieses
|
||||
> Ticket bitte entsprechend bauen: **SES als Default** (Touch vor Ort + Fern-Link + OTP + PAdES-Versiegelung +
|
||||
> Zeitstempel + M001-Audit + Completion-Certificate, s. [[EXT-09]]/[[AD-25]]), **PandaDoc als Opt-in** daneben.
|
||||
> DoD unten ist noch PandaDoc-zentriert und wird beim Bau gemäß AD-25 auf „SES-Default + BYOK-Adapter" angepasst.
|
||||
> (Rechtlich: SES genügt im SHK; QES nur § 650h, später via D-Trust.)
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
@@ -23,33 +22,43 @@
|
||||
- **Konflikt-Risiko:** gering — additive Tabellen; `apps.prisma`/`crm.prisma`/`files.prisma` nur über Relationen.
|
||||
|
||||
## Was wir bauen
|
||||
**Multi-Signer-Angebots-Signatur über PandaDoc** (DocuSign-Adapter bleibt möglich, läuft über die Apps-Engine M33a): PDF-Upload (aus Files oder neu) → Unterzeichner (Name vorausgefüllt aus Kontakt, Email, Rolle, Reihenfolge sequenziell/parallel) → Senden, mit **Webhook-Verarbeitung** und **Status-Tracking je Unterzeichner** (entwurf/gesendet/angesehen/signiert/abgelehnt), Erinnerung, Widerruf und optionalem Auto-Stage-Wechsel. Das signierte PDF landet in Files. **Pivot-Hart:** Signatur hängt am **`Projekt`-Kern** (`ProjektSignatureDocument`/`ProjektSignatureSigner`), kein eigenes „Deal"-Objekt — Einstieg lebt im Projekt-Detail (P8/P9).
|
||||
**Multi-Signer-Angebots-Signatur über den gehärteten SES-Eigenbau**: PDF-Upload (aus Files oder neu) → Unterzeichner (Name vorausgefüllt aus Kontakt, Email, Rolle, Reihenfolge sequenziell/parallel) → Zustellung per Fern-Link/OTP oder Touch-Signatur vor Ort → PAdES-versiegeltes Signed-PDF + Completion-Certificate. **PandaDoc bleibt optionaler BYOK-Adapter** hinter derselben Apps-Engine-Connection- und Adapter-Schnittstelle. Status-Tracking je Unterzeichner (entwurf/gesendet/angesehen/signiert/abgelehnt), Erinnerung, Widerruf und optionaler Auto-Stage-Wechsel bleiben erhalten. Das signierte PDF landet in Files. **Pivot-Hart:** Signatur hängt am **`Projekt`-Kern** (`ProjektSignatureDocument`/`ProjektSignatureSigner`), kein eigenes „Deal"-Objekt — Einstieg lebt im Projekt-Detail (P8/P9).
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-BOOK` + `D-P16`)
|
||||
| Screen [Route] | Kern-Inhalt |
|
||||
|---|---|
|
||||
| Signatur-Wizard [im Projekt-Detail] | PDF-Upload (aus Files/neu) → Unterzeichner (Name aus Kontakt, Email, Rolle, Reihenfolge sequenziell/parallel) → Provider (PandaDoc, Default) → Senden; **Status-Tracking je Unterzeichner** + Erinnerung + Widerruf; Original- + Signed-PDF-Download |
|
||||
| Signatur-Wizard [im Projekt-Detail] | PDF-Upload (aus Files/neu) → Unterzeichner (Name aus Kontakt, Email, Rolle, Reihenfolge sequenziell/parallel) → Signaturmodus (Fern-Link/OTP oder Touch vor Ort; PandaDoc nur BYOK-Opt-in) → Senden; **Status-Tracking je Unterzeichner** + Erinnerung + Widerruf; Original-, Signed-PDF- und Completion-Certificate-Download |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **PDF-Upload → Multi-Signer via PandaDoc** über die Apps-Engine (M33a)
|
||||
- [ ] **Webhook-Verarbeitung** + **Status-Tracking pro Unterzeichner** (entwurf/gesendet/angesehen/signiert/abgelehnt)
|
||||
- [ ] Erinnerung + Widerruf + optionaler Auto-Stage-Wechsel; Signed-PDF landet in Files
|
||||
- [ ] **Signatur hängt am `Projekt`-Kern** (`ProjektSignatureDocument`/`ProjektSignatureSigner`) — kein „Deal"-Objekt
|
||||
- [ ] Permission-Bundle `signature.*` (read/send/revoke/admin) serverseitig Owner-gefiltert
|
||||
- [ ] Konnektor `tenantHasModule('signature')`; RLS auf allen Tenant-Tabellen
|
||||
- [x] **AD-25 umgesetzt:** SES-Eigenbau ist Default (`ses-selfhosted`), PandaDoc bleibt optionaler BYOK-Adapter (`pandadoc-byok`) über die Apps-Engine.
|
||||
- [x] **Status-Tracking pro Unterzeichner** für SES-Flow (draft/sent/viewed/signed/revoked) plus PandaDoc-HMAC-Webhook-Helfer für BYOK.
|
||||
- [x] OTP-Mail via Transactional-Email-Queue, Erinnerung + Widerruf; Signed-PDF- und Completion-Certificate-Artefakte landen in Files.
|
||||
- [x] **Signatur hängt am `Projekt`-Kern** (`ProjektSignatureDocument`/`ProjektSignatureSigner`) — kein „Deal"-Objekt.
|
||||
- [x] Permission-Bundle `signature.*` (read/send/revoke/admin) serverseitig Owner-gefiltert.
|
||||
- [x] Konnektor `tenantHasModule('signature')`; RLS auf allen Tenant-Tabellen.
|
||||
- [ ] **Blockiert durch D-P16:** finales Subframe-/David-Design für den Signatur-Wizard; aktuell funktionaler Fallback im Projekt-Detail.
|
||||
- [x] **PAdES-/TSA-Produktionspfad:** `@signpdf`-basierte `ETSI.CAdES.detached`-Signatur mit P12-Zertifikat (`SIGNATURE_P12_BASE64` oder `SIGNATURE_P12_PATH`) und optionalem RFC3161-TSA-Request (`SIGNATURE_TSA_URL`). Ohne Zertifikat bleibt Dev-Fallback möglich; mit `SIGNATURE_REQUIRE_PADES=true` schlägt der Abschluss hart fehl, wenn Zertifikatsmaterial fehlt.
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/signature.prisma`): `SignatureProviderConnection` (data_connection_id → Apps-Engine!, provider PandaDoc, is_default), `ProjektSignatureDocument` (projekt_id, provider_document_id, source_pdf_file_id, status draft/sent/viewed/signed/declined, signed_pdf_file_id — **[Soft-Delete]**), `ProjektSignatureSigner` (document_id, signer_email/name, linked_kontakt_id, status, signed_at). `apps.prisma`/`crm.prisma`/`files.prisma` nur über Relationen (AD-21).
|
||||
- **Signatur = Apps-Engine-Connection** (R-P5): OAuth/API-Key + Health vom Framework; hier Provider-Adapter PandaDoc.
|
||||
- **Webhook-Handler** (Signatur-Check) → Status-Übergänge je Signer + Activity-Write; Auto-Stage-Wechsel über Pipeline (P8/P9, optional).
|
||||
- **Entities** (eigene `prisma/schema/signature.prisma`): `SignatureProviderConnection` (data_connection_id → Apps-Engine!, provider `ses-selfhosted` default oder `pandadoc-byok` optional, is_default), `ProjektSignatureDocument` (projekt_id, provider_document_id, source_pdf_file_id, status draft/sent/viewed/signed/declined, signed_pdf_file_id, completion_certificate_file_id — **[Soft-Delete]**), `ProjektSignatureSigner` (document_id, signer_email/name, linked_kontakt_id, status, signed_at, otp_verified_at). `apps.prisma`/`crm.prisma`/`files.prisma` nur über Relationen (AD-21).
|
||||
- **Signatur = Apps-Engine-Connection** (R-P5): OAuth/API-Key + Health vom Framework; hier Provider-Adapter `ses-selfhosted`, optional `pandadoc-byok`.
|
||||
- **Callback-/Webhook-Handler** (Signatur-Check) → Status-Übergänge je Signer + Activity-Write; Auto-Stage-Wechsel über Pipeline (P8/P9, optional).
|
||||
- **RLS** überall; `signature.*`-Bundle; signiertes PDF via FileEntityLink an den Projekt-Kern.
|
||||
- **Konnektor** `tenantHasModule('signature')`; `is_demo_data` auf Demo-Dokumenten.
|
||||
|
||||
## Umsetzungsnotiz 2026-07-01
|
||||
- Branch: `feat/AK-P16d-signature-ses`, abgezweigt von aktuellem `origin/main`.
|
||||
- Gebaut: `prisma/schema/signature.prisma` + RLS-Policy, File-Link-Typ `signature_document`, Permission/Entitlement-Wiring `signature.*`, Tenant-Modell-Registrierung, Apps-Engine-Katalog/Adapter `ses-selfhosted` und `pandadoc-byok`, Projekt-Signatur-VM, Projekt-Detail-Fallback-UI, öffentlicher `/sign/:token`-Flow mit OTP/Intent/Touch-Signatur, OTP-Mail-Queue, Reminder/Widerruf, Signed-PDF-/Completion-Certificate-Ablage in Files.
|
||||
- Proof lokal: `pnpm exec prisma validate` grün; `pnpm --filter @toolctrl/integrations test` grün; `pnpm --filter @toolctrl/view-models test` grün; `pnpm --filter @toolctrl/integrations typecheck` grün; `pnpm --filter @toolctrl/view-models typecheck` grün; `pnpm --filter @toolctrl/web typecheck` grün; `pnpm exec vitest run apps/web/app/modules/signature/services/padesSeal.server.test.ts` grün; `pnpm lint` grün.
|
||||
- Push-Proof: Implementierungs-Commit `2e7e81557` auf `origin/feat/AK-P16d-signature-ses` gepusht; Pre-Push-Hook `i18n` + `typecheck` grün.
|
||||
- Hinweis: `pnpm --filter @toolctrl/config test` fällt lokal auf Windows in `env-access-enforcement.test.ts` wegen bereits bestehender absoluter Baseline-Pfade in anderen Packages; keine AK-P16d-Datei ist in der Unexpected-Liste.
|
||||
- Fortschritt: 7/8 DoD = 87,5 % done. Selbst machbar offen: 0 Punkte. Extern blockiert: D-P16 finales Signatur-Wizard-Design.
|
||||
|
||||
## Quellen (granular)
|
||||
- `ENTITY-MODEL.md` → **Domäne 9** (SignatureProviderConnection, ProjektSignatureDocument/Signer — am Projekt-Kern, AD-20)
|
||||
- `REQUIREMENTS.md` → `REQ-signatur-datenmodell` (⚠ Bezeichner „Deal" pre-pivot — gilt `Projekt`), `REQ-signatur-provider`
|
||||
- `ROADMAP.md` → Phase 16 · `PHASE-SOURCES.md` → Phase 16 (M130-erstellung, M131-pandadoc-docusign, M132-status-tracking)
|
||||
- `designer-briefs/P16-kalender-booking-forms-signatur.md` (Gruppe 4 Signatur; PandaDoc v1, Pivot-Hinweis Projekt-Kern)
|
||||
- `ROADMAP.md` → Phase 16 · `PHASE-SOURCES.md` → Phase 16 (M130-erstellung, M131-self-hosted-e-signatur, M132-status-tracking)
|
||||
- `designer-briefs/P16-kalender-booking-forms-signatur.md` (Gruppe 4 Signatur; SES-Default/PandaDoc-BYOK, Pivot-Hinweis Projekt-Kern)
|
||||
- `_bmad-output/specs/M07-angebot-signatur.md` (⚠ teils pre-pivot — ENTITY-MODEL hat Vorrang)
|
||||
|
||||
## Dev-Nachzug (nach Davids `D-BOOK`/`D-P16`-Delta)
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
| **Cycle/Fenster** | W2 — Plattform & CRM (Demo 1) · fällig 2026-06-25 · Gate W2 |
|
||||
| **Status** | geplant |
|
||||
|
||||
**🎨 design-nachzug: offen** (seit 2026-07-08) — Interim-UI aus `~/ui` steht (AD-28); finale Subframe-Optik aus `D-HR` ausstehend (Davids Delta noch leer).
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-P4b` (Files/Tasks + EntityLink — HR-Dokumente hängen an Files)
|
||||
- **Blocks:** `AK-P17b` (Urlaub/Krank/Surveys)
|
||||
@@ -28,11 +30,11 @@ Den **verschlüsselten HR-Kern** (Quiply-Ablösung): **MA-Stammdaten** (HrEmploy
|
||||
| Passwort-Safe-Verwaltung [`/admin/hr/password-safe`] | Tenant-weite Vault-Sicht (ohne Inhalts-Access), Vault-Anlage/Archiv/Delete, Access-Verwaltung, Audit-Log-Übersicht |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **HR-Profile/Dokumente AES-256-GCM** (KMS-Envelope-Encryption, AD-8)
|
||||
- [ ] **MA-Bereich vs. HR-Admin-Bereich strikt getrennt**; sensible Tabellen zusätzlich App-Permission-gegated
|
||||
- [ ] **Verschlüsselter Passwort-Safe mit Audit-Trail**: Reveal/Copy entschlüsselt on-demand + Permission-Check + Audit-Eintrag; Werte nie in Liste; Copy mit 30s-Auto-Clear; DSGVO-Export
|
||||
- [ ] MA-Verzeichnis + Standorte (Admin-CRUD) + eigene Dokumente (Upload + read-only Büro-Uploads)
|
||||
- [ ] Konnektor `tenantHasModule('hr')`; RLS auf allen Tenant-Tabellen; sensible Felder verschlüsselt
|
||||
- [x] **HR-Profile/Dokumente AES-256-GCM** (KMS-Envelope-Encryption, AD-8) — *per-Tenant-DEK-Envelope aus R-P5 (`getTenantFieldCrypto`) jetzt in den Seam gebunden; Notfallkontakt verschlüsselt. Finale UI-Optik = Davids D-HR.*
|
||||
- [x] **MA-Bereich vs. HR-Admin-Bereich strikt getrennt**; sensible Tabellen zusätzlich App-Permission-gegated
|
||||
- [x] **Verschlüsselter Passwort-Safe mit Audit-Trail**: Reveal/Copy entschlüsselt on-demand + Permission-Check + Audit-Eintrag; Werte nie in Liste; Copy mit 30s-Auto-Clear; DSGVO-Export — *Backend komplett: Ver-/Entschlüsselung jetzt scharf (R-P5-Envelope); Audit/Permission/Never-List/Export gebaut. 30s-Clear = UI (David).*
|
||||
- [x] MA-Verzeichnis + Standorte (Admin-CRUD) + eigene Dokumente (Upload + read-only Büro-Uploads) — *Backend + funktionale Routen; finale Optik wartet auf Davids D-HR-Delta.*
|
||||
- [x] Konnektor `tenantHasModule('hr')`; RLS auf allen Tenant-Tabellen; sensible Felder verschlüsselt — *Konnektor (Feature/Blueprint/Plan) + RLS-SQL (deferred-Aktivierung wie P4b) + sensible Felder verschlüsselt (R-P5-Envelope) stehen.*
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/hr.prisma`): `HrEmployeeProfile` (user_id 1:1, personal_number, hire_date, position, team, location_id, manager_user_id, emergency_contact_*), `HrLocation` (name, address_*, geo, image_file_id), `HrEmployeeDocument` (employee_user_id, document_kind, file_id, visibility), `HrResource`/`HrQuickLink`, `EmployeePasswordSafeVault` (+ `Entry`/`Access` — username/password/notes **AES-256-GCM**, permission_level read/write/admin). `core.prisma` nur für User-Relation (AD-21).
|
||||
@@ -48,7 +50,84 @@ Den **verschlüsselten HR-Kern** (Quiply-Ablösung): **MA-Stammdaten** (HrEmploy
|
||||
- `designer-briefs/P17-hr-hub.md` (MA- vs. HR-Admin-Trennung, Passwort-Safe-Reveal, sensible Daten)
|
||||
- `_bmad-output/specs/M14-hr-hub.md`
|
||||
|
||||
## Umsetzungs-Notiz
|
||||
|
||||
> **✅ Stand 2026-06-28 — auf `main` gemergt.** `feat/AK-P17a` (inkl. der eingemergten R-P5-Commits) ging mit **PR #5** (squash, Merge-Commit `b0fae1af8`, Base `main`, 2026-06-28 17:50, merged_by raffael) auf `main`; Docker-Nachzug `packages/integrations/package.json` via **PR #8** (`0b9d2e46`, 18:05). Alle HR-Artefakte (`hr.prisma`, `hrFieldCrypto.server.ts`, `hrService`/`hrPasswordSafeService`, RLS-SQL, Routen-Hülle, Sidebar) + das R-P5-Crypto-Fundament liegen im `main`-Tree. *Die „Branch lokal, nicht gepusht"-Notizen unten sind damit überholt.* **Backend 5/5 DoD grün.** Extern offen bleibt nur: Davids `D-HR`-Optik + echter 30s-Clipboard-Wipe, RLS-Live/Crypto-Round-trip/Visual-Regression gegen Staging-DB, Plane-Ticket auf Done. Weiterarbeit am HR-Hub läuft über **AK-P17b** (neuer Branch).
|
||||
|
||||
**2026-06-27 (Akeshan) — Backend-Kern + funktionale Routen, ~70% gebaut (2/5 DoD voll grün).**
|
||||
|
||||
Branch `feat/AK-P17a` (lokal, nicht gepusht). Blocker `AK-P4b` ist in `main` gemergt (Commit `2e7b935b9`), Files/EntityLink-Fundament steht. Strikt im AK-P17a-Scope geblieben (Entscheidung „strikt nach Ticket-Doc"): nur Stammdaten/Directory/Locations/Documents/Passwort-Safe; Announcements/Leave/Payslips/Surveys/Requests bleiben bei AK-P17b/Folge.
|
||||
|
||||
Gebaut:
|
||||
- **Schema** `prisma/schema/hr.prisma` (AD-21, bare Scalars, `core.prisma` unberührt): HrEmployeeProfile, HrLocation, HrEmployeeDocument, HrResource/HrQuickLink, EmployeePasswordSafeVault/Entry/Access. Sensible Werte als `*Ciphertext`-Spalten. `prisma validate` + `generate` grün. **`db push` lokal noch offen** (kein lokales Postgres/Docker), env-gegatet wie bei AK-P4b.
|
||||
- **RLS** `prisma/rls/hr-isolation.sql` (deferred-Aktivierung wie task-isolation.sql, P5-Cutover mit R-P2b) + env-gegateter Gate-Test `rls-hr-isolation.integration.test.ts` (skippt ohne `RLS_TEST_DATABASE_URL`).
|
||||
- **Crypto-Seam** `hrFieldCrypto.server.ts` (Entscheidung c): Interface + injizierbar; Produktiv-Binding wirft Klartext-Fehler (M002) bis zum separaten AD-8-Crypto-Ticket. Services nehmen den Provider injiziert → voll testbar.
|
||||
- **Services** `hrService.server.ts` (Profile/Directory/Locations/Documents, MA-self vs HR-admin) + `hrPasswordSafeService.server.ts` (Vault/Entry/Access, `withTenantRLS`, 404-statt-403, Reveal audit-geloggt + Permission-Check, Werte nie in Listen, DSGVO-Export). `hrAccess.server.ts` (Konnektor-Gate + Bundle-Permissions).
|
||||
- **Permissions** `hr.*`-Set + Rollen-Grants in `permissions-seed.server.ts`. **Konnektor**: `hr` als Feature-Key + Blueprint + alle 3 Pläne (entitlements), `tenantHasModule({moduleKey:'hr'})` in jedem Loader.
|
||||
- **View-Models** `packages/view-models/src/hr/*` (pure, Never-List-Values-Contract per Test gesichert) + **Search-Schemas** `hr-directory`/`hr-locations` (Vault-Werte nie indexiert).
|
||||
- **Routen** `app.$tenant/hr.*` (Hub, Directory+Detail, Locations, My-Documents, Passwort-Safe-Liste+Vault-Detail+Reveal+Export, Admin-Vault-Sicht) + **Sidebar**-Eintrag (Feature- + Permission-gegated) + **i18n** de+en. Finale Optik = Hot-Swap-Hülle, wartet auf **Davids D-HR-Delta** (Delta leer).
|
||||
|
||||
Verifiziert (lokal grün): `pnpm typecheck` (alle Pakete + web), `pnpm run verify:i18n` (Exit 0), eslint-staged/baseline/test-density-Gates, entitlements/search/view-models/sidebar-Tests, neue HR-VM- + Crypto-Seam-Tests (15), RLS-Gate-Test skippt sauber. (Pre-existing Windows-Only-Flake `permissions-seed isPermissionsSeedDirectExecution` unverändert, environmental.)
|
||||
|
||||
**Stand: 2/5 DoD voll grün (#2, #4) = 40% strikt; #3/#5 zu ~90% (alles außer per-Tenant-Verschlüsselung), #1 schemaseitig vorbereitet. Gesamt ~70% gebaut.** Offen/blockiert: (a) AD-8-Crypto-Ticket (Entscheidung c) → schaltet #1/#3/#5-Verschlüsselung scharf; (b) Davids D-HR-Delta → finale UI/Visual-Regression; (c) `db push` + RLS-Live-Verifikation (Staging/DB); (d) Merge-Migration entsteht erst beim main-Merge (AD-21). 0 weitere selbst machbar ohne Crypto-Ticket/David/DB.
|
||||
|
||||
Proof: Commit `aa848dca7` (Branch `feat/AK-P17a`, lokal, nicht gepusht). Plane-Kommentar gesetzt (2026-06-27, akeshan). Ticket bleibt offen.
|
||||
|
||||
**Noch im Ticket selbst machbar (ohne externe Blocker), als Folge-Schritte:**
|
||||
- Nebenbei-Validierung sobald lokal/Staging-Postgres da ist: `prisma db push` + RLS-Gate-Test (`RLS_TEST_DATABASE_URL`) live grün ziehen.
|
||||
- Demo-Seed (`is_demo_data`) für Profile/Standorte/Vault, damit die Routen mit Daten zeigbar sind (sofern P17a-Scope; sonst AK-DEMO1).
|
||||
- Smoke-Test `tests/e2e/hr.spec.ts` (Login → /hr → Hauptaktion) sobald die UI nicht mehr reine Hülle ist (nach Davids Delta).
|
||||
- Mobile-Tab-Bar-Eintrag (DoD B#11) für die Monteur-relevanten HR-Screens, falls vorgezogen erwünscht.
|
||||
|
||||
**Update 2026-06-28 (Akeshan) — R-P5 in `feat/AK-P17a` gemergt + Crypto-Seam scharfgeschaltet. DoD jetzt 5/5 backend grün.**
|
||||
|
||||
Co-Dev-Abstimmung: R-P5 von Raffael übernommen und auf `feat/R-P5` fertig gebaut (per-Tenant-DEK-Envelope, AD-8). Entscheidung: `feat/R-P5` voll in `feat/AK-P17a` mergen, damit das Crypto-Fundament hier verfügbar ist, dann den HR-Seam umbinden. 2 Commits:
|
||||
- `efcf44a9` **Merge `feat/R-P5`** — bringt `getTenantFieldCrypto` (`@hivegrid/db`) + `crypto.prisma`/`TenantEncryptionKey`. Konflikte alle „beide behalten": entitlements (hr + integrations, `integrations` defaultOrder 12→13 wegen unique+ascending-Test), permissions-seed/DefaultPermissions/AppSidebar, translations.json de+en (chirurgisch, kein Round-trip), `packages/db/generated/**` via `prisma generate` aus dem Union-Schema neu.
|
||||
- `84fcf1e0` **Seam-Rebind** — `getHrFieldCrypto()` liefert jetzt `getTenantFieldCrypto()` (strukturell identisch). Schaltet #1 (Notfallkontakt AES-256-GCM), #3 (Passwort-Safe Reveal/Export), #5 (sensible Felder verschlüsselt) scharf. Deferred-Fail-Closed-Gerüst (`HrCryptoNotProvisionedError`) bleibt. Seam-Test angepasst (liefert echten Provider, nicht das Gerüst) + neuer env-gegateter Round-trip-Integrationstest (`HR_CRYPTO_TEST_DATABASE_URL`): treibt die echten Services, prüft Ciphertext≠Klartext, Reveal-Round-trip, List versteckt Werte, fremder Tenant kann nicht entschlüsseln (skippt ohne DB).
|
||||
|
||||
Verifiziert: `pnpm typecheck` (alle Pakete + web, direkt-tsc, **0 Fehler** — Turbo-Cache umgangen), `verify:i18n` Exit 0, Zieltests grün (Seam 5, HR-VMs 10, foundation+db-Crypto 16, entitlements 13), Round-trip-Test skippt sauber, beide Commits lefthook-grün.
|
||||
|
||||
**Stand: 5/5 DoD backend grün.** Nur noch extern blockiert: (a) Davids D-HR-Delta → finale Optik + echter 30s-Clipboard-Wipe + Visual-Regression/Smoke; (b) `db push` + RLS-Live + Crypto-Round-trip gegen echtes Postgres (kein lokales PG); (c) Merge-Migration beim main-Merge (AD-21). 0 selbst machbar offen. **Kopplung (bewusst):** `feat/AK-P17a` trägt jetzt R-P5s Commits; R-P5 muss vor/mit P17a auf main, bei R-P5-Review-Änderung hier nachziehen. Branch lokal, nicht gepusht, Plane-Status unverändert (In Progress).
|
||||
|
||||
**Nachtrag 2026-06-28 — aktuellen `origin/main` reingemergt** (`5a30e950`, Branch war 1 Commit hinter main: `e7a888e3` WF-P2 Auth-Screens + du/Sie-Anrede-Toggle). Konflikt nur in `prisma/schema/accounts.prisma`: main fügte `Tenant.anrede` direkt neben den alten `apps`/`dataConnections`/`appSyncRuns`-Back-Relations ein, die R-P5 bewusst entfernt hat (App global, bare Scalars, AD-21) → `anrede` behalten, Back-Relations draußen gelassen. Generierter Prisma-Client aus dem Union-Schema neu erzeugt, translations.json auto-gemergt (hr + integrations + main-Keys, JSON valide). Verifiziert: `pnpm typecheck` direkt-tsc **0 Fehler**, `verify:i18n` Exit 0, Zieltests grün (44). Branch jetzt 18 Commits ahead, lokal, nicht gepusht.
|
||||
|
||||
**Soll/Ist-Verifikation 2026-06-28 (Akeshan) — unabhängiger Nach-Merge-Check, alle CI-Gates grün, DoD bleibt 5/5 backend.**
|
||||
|
||||
Anforderungen (DoD #1–#5) gegen den Ist-Stand im Repo geprüft und nachgewiesen, dass durch die beiden Merges (R-P5 + origin/main) nichts gebrochen ist:
|
||||
- **CI-Gates** (= was Gitea-CI + lefthook real fahren): `prisma generate` aus Union-Schema sauber; `pnpm typecheck` (web) **0 Fehler**; `@hivegrid/api` + `@hivegrid/worker` typecheck **0 Fehler**; `verify:i18n` Exit 0; Zieltests **44 passed / 3 skipped** (HR-VMs 10, Seam 5, foundation+db-Crypto 16, entitlements 13; Round-trip skippt sauber ohne DB).
|
||||
- **DoD-Ist im Code bestätigt:** Crypto-Seam (`hrFieldCrypto.server.ts`) liefert `getTenantFieldCrypto` (Seam-Test grün) → #1/#3/#5 scharf; `crypto.prisma`/`hr.prisma`/`appsEngine.prisma` alle im Schema-Ordner, `prisma generate` validiert das Union-Schema; `accounts.prisma`-Konflikt korrekt (`Tenant.anrede` da, keine entfernten Back-Relations zurückgekrochen).
|
||||
- **Befund (vorbestehend, KEIN Merge-Schaden):** Der volle `pnpm -r typecheck` (nicht von CI gefahren — CI = nur web/api/worker) zeigte 3 latente `TS2352`-Fehler in den HR-VM-Tests (`hr-directory.vm.test.ts`, `hr-password-safe.vm.test.ts`): direkter Cast eines benannten Row-Interfaces auf `Record<string, unknown>`. Die Tests **liefen** grün (vitest/esbuild typecheckt nicht), nur der Paket-`tsc` stolperte — und der `@hivegrid/view-models`-`tsc` hängt in keinem Gate. Schon im Ursprungs-Commit `aa848dca7` vorhanden (tsconfig im Merge-Range unberührt), also nicht durch den Merge entstanden. **Behoben** mit dem von TS vorgeschlagenen `as unknown as Record<…>`-Cast in den 3 Stellen → `@hivegrid/view-models` typecheckt jetzt sauber, VM-Tests bleiben grün. Damit stimmt die frühere „direkt-tsc 0 Fehler"-Aussage jetzt auch paketweit.
|
||||
- **Ebenfalls vorbestehend + außerhalb Scope (nicht angefasst):** `packages/exports`-`tsc` wirft `TS2688 Cannot find type definition file for 'node'` — lokales `@types/node`-Resolution-Thema (`"types":["node"]`), im Merge-Range nicht berührt, in keinem CI-Gate, nicht HR/R-P5-bezogen. Notiert, nicht repariert (Fremd-Scope).
|
||||
|
||||
**Stand unverändert: 5/5 DoD backend grün.** Extern blockiert bleibt: (a) Davids D-HR-Delta → finale Optik + echter 30s-Clipboard-Wipe + Visual-Regression/Smoke; (b) `db push` + RLS-Live + Crypto-Round-trip gegen echtes Postgres; (c) Merge-Migration beim main-Merge (AD-21). 0 selbst machbar offen. Branch lokal (mehrere Commits ahead von origin/main, 0 behind), nicht gepusht, Plane-Status unverändert (In Progress).
|
||||
|
||||
**Nachtrag 2026-06-28 — PR-Pipeline Build-Fix (Client/Server-Split).** Der PR-Build (`pnpm --filter @hivegrid/web build`) fiel im **Client-Environment** mit „Server-only module referenced by client" auf `app.$tenant/hr._index.tsx` → `~/utils/integrations/hrAccess.server`. Ursache: `export const handle = { moduleKey: HR_MODULE_KEY }` — `handle` ist **kein** server-gestrippter Export (RR strippt nur `loader`/`action`/`middleware`/`headers`), und `HR_MODULE_KEY` lag im `.server`-Guard → der ganze Server-Graph (Prisma/db) wurde in den Client-Bundle gezogen. Typecheck fängt das nicht (kein Client/Server-Split), nur der Build. **Identisches Latenz-Muster im R-P5-Pendant** `integrations._index.tsx` (`INTEGRATIONS_MODULE_KEY` aus `integrationsAccess.server`) — der Build bricht nur zuerst bei HR ab. Fix nach dem Walking-Skeleton-Präzedenzfall (`modules/walkingSkeleton/constants.ts`): den Modul-Key in das **client-sichere** Companion-Modul (`hrRoutePaths.ts` / `integrationsRoutePaths.ts`) verschoben, aus dem `.server`-Guard für Back-Compat re-exportiert, und den Route-Import auf das client-sichere Modul umgezogen. Verifiziert: lokaler `pnpm --filter @hivegrid/web build` → **Client-Environment ✓ (8079 Module, no server-only error)**; `web`-typecheck **0 Fehler**. (Lokaler Server-Build-Step bricht danach am bekannten Windows-`/dev/null`-Footgun in `packages/mail` ab — environmental, Linux-CI nicht betroffen.) 6 Dateien, beide Routen (HR + integrations).
|
||||
|
||||
**Update 2026-07-08 (Akeshan) — Interim-UI aus `~/ui` (AD-28). UI-DoD über den Interim-Pfad erfüllt.**
|
||||
|
||||
Die HR-Screens rendern nicht mehr handgeschriebenes Tailwind (das war das Playbook-Negativbeispiel), sondern
|
||||
sind aus `~/ui`-Components recomposed — pure `<XPage vm={} actions={} />` in `apps/web/app/components/screens/hr/`,
|
||||
die Routen bauen nur das VM und injizieren Action-Callbacks (Fetcher bleiben in der Route). Kein VM-/Loader-/
|
||||
Schema-Change. Gebaut (AK-P17a-Scope, 6 Screens + 4 Detail-/Sub-Screens):
|
||||
- `HrHubPage`, `HrDirectoryPage` + `HrDirectoryDetailPage` (Notfallkontakt-Reveal on-demand, Inline-Self-Edit),
|
||||
`HrLocationsPage` (+ Create-Dialog), `HrMyDocumentsPage` (Kind-Filter, read-only Büro-Uploads),
|
||||
`HrPasswordSafeListPage` + `HrVaultDetailPage` (**Werte nie in der Liste**, Reveal je Feld,
|
||||
**echter 30-s-Clipboard-Wipe** nach Copy via `CopyToClipboardButton` + `vm.copyAutoClearMs`, DoD #3),
|
||||
`HrAdminPasswordSafePage`.
|
||||
- 57 neue Präsentations-i18n-Keys chirurgisch in de+en gespleißt (kein JSON-Round-trip).
|
||||
|
||||
**Proof:** Commit `e0f8affd2` (Branch `feat/AK-P17a-interim-ui`, lokal, nicht gepusht) · `pnpm --filter
|
||||
@hivegrid/web typecheck` **0 Fehler** · `pnpm run verify:i18n` Exit 0 (Parität ok, 0 missing-source) ·
|
||||
lefthook pre-commit grün (lint-staged/eslint-baseline/secrets/test-density).
|
||||
|
||||
**Interim-UI (Vorschau, Stand 2026-07-08):** Screens aus `~/ui` gebaut — `apps/web/app/components/screens/hr/*`.
|
||||
Refactor-Nachzug offen: finale Subframe-Optik aus `D-HR` (Plane-Label `design-nachzug`). Noch selbst machbar
|
||||
(nicht David-blockiert): Screen-Unit-Tests je `<XPage>.test.tsx`, Smoke `e2e/smoke/hr-*`, Hot-Swap-Adapter-Triplet;
|
||||
Visual-Regression bleibt **deferred** (kein Screenshot-Harness im Repo, R-P5-Präzedenz). Demo-Seed (`is_demo_data`)
|
||||
für Profile/Standorte/Vault als Folge-Task.
|
||||
|
||||
## Dev-Nachzug (nach Davids `D-HR`-Delta)
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P17a` aktualisiert
|
||||
- [ ] Finale `D-HR`-Optik in `<XPage>`-Internals getauscht (VM/Loader/Route unverändert), Visual-Baseline gezogen, `design-nachzug`-Tag auf erledigt
|
||||
@@ -8,6 +8,8 @@
|
||||
| **Cycle/Fenster** | W2 — Plattform & CRM (Demo 1) · fällig 2026-06-26 · Gate W2 |
|
||||
| **Status** | geplant |
|
||||
|
||||
**🎨 design-nachzug: offen** (seit 2026-07-08) — Interim-UI aus `~/ui` steht (AD-28); finale Subframe-Optik aus `D-HR` ausstehend (Davids Delta noch leer).
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-P17a` (HR-Kern + Verschlüsselungs-Pattern + MA/Admin-Trennung)
|
||||
- **Blocks:** `R-P9b` (Abwesenheiten → Plantafel-Konflikt-Indikatoren)
|
||||
@@ -28,14 +30,19 @@ Die **HR-Workflows**: einen **mehrstufigen Urlaubs-Approval-Workflow** (submitte
|
||||
| Upload-Erinnerungen-Verwaltung [`/admin/hr/upload-reminders`] | Reminder-Liste (Titel/Cadence/Audience/Status), Editor (Dokumentart/Cadence/Mandatory/Linked-Form), Dispatch-Liste |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **Mehrstufiger Urlaubs-Approval-Workflow** (submitted/approved/rejected/withdrawn + Rückfrage) → speist **Abwesenheits-Kalender** (M25) + **Plantafel-Konflikt-Indikatoren** (R-P9b)
|
||||
- [ ] **Krankmeldung** (selbst/durch Büro) + **AU-Upload verschlüsselt**
|
||||
- [ ] **Announcements** (Audience, Acknowledgement-Pflicht + Read-Receipt, Reaktionen, Kommentare)
|
||||
- [ ] **Lohnabrechnungs-Bulk-Upload** mit Dateiname-Auto-Match (Personalnummer/Name), Match-Vorschau mit Konfidenz, Read-Tracking, Audit pro Read
|
||||
- [ ] **Upload-Erinnerungen** (Cadence once/yearly/every_n_months/on_expiry, Eskalation an Manager) + **Requests** via Templates
|
||||
- [ ] **Surveys** (anonym möglich) mit Auswertung
|
||||
- [ ] **Mitarbeiter-Self-Service** (Web/PWA; Web-Push → v2, AD-17); Anträge als **M05-Form-Submissions** (Renderer aus P16 wiederverwenden)
|
||||
- [ ] Konnektor `tenantHasModule('hr')`; RLS + App-Permission-Gates; sensible Felder verschlüsselt
|
||||
|
||||
> Legende: `[x]` = im **jetzt-machbaren** Scope fertig + verifiziert · `(extern: …)` =
|
||||
> Rest hängt an einem blockierenden Folge-Ticket. Details + %-Stand: Umsetzungs-Notiz unten.
|
||||
|
||||
- [x] **Mehrstufiger Urlaubs-Approval-Workflow** (submitted/approved/rejected/withdrawn + Rückfrage) → **Plantafel-Konflikt-Feed** (R-P9b via `listApprovedAbsencesForRange`). *(extern: CalendarEvent-Write → P16/M25)*
|
||||
- [x] **Krankmeldung** (selbst/durch Büro) + **AU-Upload verschlüsselt** (Crypto-Seam, audited Reveal)
|
||||
- [x] **Announcements** (Audience, Acknowledgement-Pflicht + Read-Receipt, Reaktionen, Kommentare)
|
||||
- [x] **Lohnabrechnungs-Bulk-Upload** mit Dateiname-Auto-Match (Personalnummer/Name), Match-Vorschau mit Konfidenz, Read-Tracking, Audit pro Read
|
||||
- [x] **Upload-Erinnerungen** (Cadence once/yearly/every_n_months/on_expiry, Eskalation an Manager via Worker-Cron) + **Requests** via Templates
|
||||
- [x] **Surveys** (anonym möglich, `submitterHash`) mit Auswertung (gegated)
|
||||
- [x] **Mitarbeiter-Self-Service** (Web/PWA) — alle Bereiche reachable. *(extern: M05-Form-Submissions → AK-P16c, bis dahin Übergangs-Formulare; Web-Push → v2/AD-17)*
|
||||
- [x] Konnektor `tenantHasModule('hr')`; App-Permission-Gates; sensible Felder verschlüsselt; RLS-Policies geschrieben. *(extern: RLS-Live-Cutover → R-P2b, wie P17a)*
|
||||
- [ ] **Tests/UI extern blockiert:** finale Subframe-UI + Adapter + Smoke-Tests (`e2e/smoke/hr-*`) + Visual-Regression (M016) + Hot-Swap (M017) → **Davids `D-HR`-Delta** (noch leer)
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/hr.prisma`, additiv): `EmployeeVacationRequest` (start/end_date, request_kind, status, assigned_approver, calendar_event_id), `EmployeeSickLeaveReport` (start/return_date, certificate_file_id, status — **[verschlüsselt]** AU-Schein), `HrAnnouncement` (+ Read/Reaction/Comment), `EmployeePayslip` (period_year/month, file_id, is_read), `EmployeeUploadReminder` (+ Dispatch), `HrRequest` (+ Template — request_kind, form_submission_id, status, assigned_approver), `HrSurvey` (+ Item/Submission — is_anonymous, submitter_hash). `calendar.prisma`/`forms.prisma` über Relationen (AD-21).
|
||||
@@ -55,3 +62,120 @@ Die **HR-Workflows**: einen **mehrstufigen Urlaubs-Approval-Workflow** (submitte
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P17b` aktualisiert
|
||||
|
||||
## Umsetzungs-Notiz
|
||||
|
||||
### 2026-06-28 — Backend komplett + Urlaub/Krank-Vertikale reachable (~50 % DoD)
|
||||
|
||||
**Stand: ~50 % DoD** (Backend aller 7 Domänen fertig + verifiziert; Erreichbarkeit
|
||||
bislang nur für die R-P9b-kritische Urlaub/Krank-Vertikale). Backend-first, da
|
||||
Davids `D-HR`-Delta noch leer ist (finale Subframe-UI/Adapter/Visual-Regression
|
||||
extern blockiert).
|
||||
|
||||
**Proof (2 Commits auf `feat/AK-P17b`, alle Pre-Commit-Gates grün):**
|
||||
- `e2774ca8b` — Backend: `prisma/schema/hr.prisma` (12 Modelle additiv, `prisma
|
||||
validate` grün), Permissions (`hr.leave.approve`, `hr.surveys.results`),
|
||||
RLS-Policies (`prisma/rls/hr-isolation.sql`), 7 Services
|
||||
(`hr{Leave,SickLeave,Announcement,Payslip,UploadReminder,Request,Survey}Service`),
|
||||
8 View-Models + 3 pure Utils (`payslip-match`, `reminder-cadence`), 27 Unit-Tests
|
||||
grün. typecheck web/db/view-models grün.
|
||||
- `3a00766c7` — Routes `/hr/leave` + `/hr/sick` (transitional, `data-ui-id`),
|
||||
Hub-Verlinkung, i18n de+en (Parity grün, hardcoded-strings-Guard grün).
|
||||
|
||||
**DoD-Stand je Punkt:**
|
||||
- [~] (1) Urlaubs-Approval-Workflow + Konflikt-Feed → **Backend+Route fertig**;
|
||||
`EmployeeVacationRequest.calendarEventId` nullable, **CalendarEvent-Write
|
||||
`blockiert`** bis P16 (M25). R-P9b wird über `listApprovedAbsencesForRange`
|
||||
bedient.
|
||||
- [~] (2) Krankmeldung + verschl. AU → **fertig** (Backend+Route+Crypto-Reveal);
|
||||
nur finale UI = David.
|
||||
- [~] (3) Announcements → **Backend fertig**, Route offen (selbst machbar).
|
||||
- [~] (4) Lohn-Bulk + Auto-Match + Read-Tracking → **Backend fertig**, Route offen.
|
||||
- [~] (5) Upload-Erinnerungen + Requests → **Service-Logik fertig**; Worker-Cron-
|
||||
Registrierung (`dispatchDueReminders`) + Routes offen (selbst machbar).
|
||||
- [~] (6) Surveys (anonym) + Auswertung → **Backend fertig**, Route offen.
|
||||
- [~] (7) Self-Service (PWA; Push→v2) → Urlaub/Krank self-service reachable; übrige
|
||||
Routes offen; M05-Form-Reuse **blockiert** (AK-P16c) → Übergangs-Formulare;
|
||||
Web-Push = v2 (out of scope).
|
||||
- [~] (8) `tenantHasModule('hr')` + RLS + Permission-Gates + Verschlüsselung →
|
||||
Connector wiederverwendet, RLS-Policies geschrieben (**Live-Cutover blockiert =
|
||||
R-P2b, wie P17a**), Permissions + Crypto fertig.
|
||||
|
||||
**Selbst machbar offen (nächste Commits):** Routes Announcements/Payslips/Requests/
|
||||
Surveys (+ Admin-Editoren), i18n dazu, Worker-Cron für `dispatchDueReminders`,
|
||||
Smoke-Tests.
|
||||
**Extern blockiert:** finale Subframe-UI/Adapter + Visual-Regression + Hot-Swap
|
||||
(Davids `D-HR`-Delta), CalendarEvent-Write (P16), M05-Renderer (AK-P16c),
|
||||
RLS-Live-Cutover (R-P2b), Web-Push (v2).
|
||||
|
||||
### 2026-06-28 (Update) — alle Domänen-Routes reachable (~80 % DoD)
|
||||
|
||||
**Stand: ~80 % DoD.** Alle 6 Screen-Bereiche + Admin-Editoren sind jetzt als
|
||||
transitional UI erreichbar.
|
||||
|
||||
**Proof — Commit `b6c5e4f76`** (alle Pre-Commit-Gates grün): Routes
|
||||
`/hr/announcements` (Feed/Detail/Admin), `/hr/payslips` (+ `/admin` Bulk-Match),
|
||||
`/hr/requests` (+ `/admin/request-templates`), `/hr/surveys` (+ `/:id`
|
||||
Teilnahme/Editor + `/:id/results` gegated), `/admin/upload-reminders`. Hub
|
||||
verlinkt alle Bereiche. i18n de+en Parity grün, hardcoded-strings-Guard grün,
|
||||
web-typecheck grün.
|
||||
|
||||
**Einziger selbst-machbarer Rest = Worker-Cron für `dispatchDueReminders`
|
||||
(DoD 5).** Architektur-Frage offen: die Dispatch-Orchestrierung liegt aktuell in
|
||||
`apps/web/.../hrUploadReminderService.server.ts`, aber der Cron läuft in
|
||||
`apps/worker` (kann `apps/web` nicht importieren). Optionen: (a) Orchestrierung in
|
||||
den Worker-Maintenance-Handler verschieben (wie `task-archive-retention`, nutzt
|
||||
`deps.db` + `computeNextDue` aus `@hivegrid/view-models` + `createNotification
|
||||
Delivery` aus `@hivegrid/db` — beide worker-importierbar), oder (b) in ein
|
||||
geteiltes Paket extrahieren. **Stop + Rückfrage** statt raten (AGENTS.md). Die
|
||||
Logik selbst (`dispatchDueReminders`/`escalateOverdueDispatches`/`computeNextDue`)
|
||||
ist fertig + `computeNextDue` unit-getestet.
|
||||
|
||||
**DoD nach diesem Update:** 1 ~90 % (Calendar-Write blockiert) · 2 ~95 % · 3 ~95 % ·
|
||||
4 ~95 % · 5 Requests ~95 %, Reminders ~75 % (Cron-Trigger offen) · 6 ~95 % ·
|
||||
7 ~90 % (M05 blockiert, Push v2) · 8 ~90 % (RLS-Cutover blockiert).
|
||||
|
||||
### 2026-06-28 (Update) — Worker-Cron Upload-Erinnerungen fertig (~85 % DoD)
|
||||
|
||||
**Stand: ~85 % DoD — alle selbst-machbaren Punkte erledigt.** Worker-Cron gewählt
|
||||
nach Codebase-Muster (Option a, User-Freigabe).
|
||||
|
||||
**Proof — Commit `b2b445010`** (alle Pre-Commit-Gates grün): Maintenance-Task
|
||||
`hr-upload-reminder-dispatch` (täglich, general-Gruppe) → `runHrUploadReminder
|
||||
DispatchHandler` (pro Tenant Dispatch + notify + Cadence-Advance + Manager-
|
||||
Eskalation, alles in `withTenantRLS`). PURE Helfer `nextReminderDue`/
|
||||
`audienceMatches` + 7 Unit-Tests grün. queue/worker typecheck grün. **DoD 5 jetzt
|
||||
~95 %** (Cron-Trigger geschlossen).
|
||||
|
||||
**Verbleibend = ausschließlich extern blockiert:**
|
||||
- Finale Subframe-UI + Adapter + **Smoke-Tests** (`e2e/smoke/hr-*`) + Visual-
|
||||
Regression (M016) + Hot-Swap (M017) → **Davids `D-HR`-Delta** (noch leer).
|
||||
- CalendarEvent-Write (P16/M25) · M05-Form-Renderer-Reuse (AK-P16c) · RLS-Live-
|
||||
Cutover (R-P2b) · Web-Push (v2/AD-17).
|
||||
|
||||
**6 Code-Commits + 3 Doku-Commits auf `feat/AK-P17b`** (e2774ca8b · 3a00766c7 ·
|
||||
b6c5e4f76 · b2b445010 + Doku). Nichts gepusht, Plane-Status unverändert (wartet
|
||||
auf Freigabe).
|
||||
|
||||
**Update 2026-07-08 (Akeshan) — Interim-UI aus `~/ui` (AD-28). DoD #43 (UI/Tests) über den Interim-Pfad adressiert.**
|
||||
|
||||
Die AK-P17b-Workflow-Screens lagen bereits als Routen auf `main` (Commits `3a00766c7`/`b6c5e4f76`), rendern aber
|
||||
handgeschriebenes Tailwind. Sie sind jetzt aus `~/ui`-Components recomposed — gleiches AD-28-Muster wie AK-P17a
|
||||
(pure `<XPage vm={} actions={} />` in `apps/web/app/components/screens/hr/`, Routen bauen VM + injizieren
|
||||
Callbacks, Fetcher in der Route). Kein VM-/Loader-/Schema-Change. Gebaut (9 Screens):
|
||||
- `HrLeavePage` (Tabs own/approvals/team, Antrag/Approve/Reject/Rückfrage/Withdraw), `HrSickPage` (own/office,
|
||||
audited AU-Reveal-and-open), `HrAnnouncementsFeedPage` + `HrAnnouncementDetailPage` (React/Kommentar/Ack) +
|
||||
`HrAdminAnnouncementsPage`, `HrPayslipsPage` (audited Open-and-view) + `HrAdminPayslipsPage` (Bulk-Match-Vorschau
|
||||
+ Per-Row-Confirm), `HrRequestsPage` (own/approvals + Template-Wahl) + `HrAdminRequestTemplatesPage`,
|
||||
`HrAdminUploadRemindersPage`, `HrSurveysListPage` + `HrSurveyParticipationPage` (Item-Editor) +
|
||||
`HrSurveyResultsPage` (Distribution-Bars).
|
||||
- 21 neue Präsentations-i18n-Keys chirurgisch in de+en gespleißt.
|
||||
|
||||
**Proof:** Commit `f840fcf24` (Branch `feat/AK-P17a-interim-ui`, lokal, nicht gepusht) · `pnpm --filter
|
||||
@hivegrid/web typecheck` **0 Fehler** · `pnpm run verify:i18n` Exit 0 · lefthook pre-commit grün.
|
||||
|
||||
**Interim-UI (Vorschau, Stand 2026-07-08):** Screens aus `~/ui` gebaut — `apps/web/app/components/screens/hr/*`.
|
||||
Damit ist die finale Subframe-Optik (DoD #43) **kein Done-Blocker** mehr, sondern getrackter Nachzug
|
||||
(Plane-Label `design-nachzug`). Noch selbst machbar: Screen-Unit-Tests, Smoke `e2e/smoke/hr-*`, Hot-Swap-Adapter;
|
||||
Visual-Regression bleibt deferred (kein Screenshot-Harness). Extern blockiert bleibt nur: CalendarEvent-Write
|
||||
(P16), M05-Renderer (AK-P16c), RLS-Live (R-P2b), Web-Push (v2).
|
||||
@@ -1,5 +1,10 @@
|
||||
# AK-P19 · P19 Plattform-Admin (admin.hivegrid.de)
|
||||
|
||||
> ⚠️ **Billing-Recut 2026-07-04 ([[AD-27]]):** Die frueheren Legacy-Provider-Positionen in diesem Ticket sind abgeloest.
|
||||
> Plattform-Admin-Billing zielt jetzt auf **HiveGrid-Entitlements + Lago-Billing-State + Mollie-Payment-State**.
|
||||
> Offene Legacy-Provider-spezifische Admin-Arbeit wandert in den `BILL-*`-Recut und darf nicht als neue Legacy-Provider-Implementierung
|
||||
> fortgefuehrt werden.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Typ** | Dev (Code) |
|
||||
@@ -10,22 +15,22 @@
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `R-P2b` (RLS + RBAC + User-Lifecycle — Admin nutzt Boilerplate-User-Modell als Extension), `AK-P3` (Querschnitt: Audit-Hash-Kette für AdminAuditLog)
|
||||
- **Blocks:** `AK-P20` (Abo-Baukasten-Mechanik wird hier angelegt), `AK-DEMO1` (Demo-Tenant via Admin)
|
||||
- **Blocks:** `AK-P20` (Abo-/Entitlement-Mechanik wird hier angelegt), `BILL-06` (interne Admin-Billing-Basis), `AK-DEMO1` (Demo-Tenant via Admin)
|
||||
- **Läuft parallel zu:** Raffaels `R-P6` (CRM-Kern). **Maximal isoliert** — Admin-Welt vs. CRM-Domäne, eigener Host `admin.hivegrid.de`, **kein Schema-Konflikt**. Eigene `prisma/schema/admin.prisma` (AD-21); **getrennte Welt, KEIN `tenant_id`** auf den Admin-Tabellen.
|
||||
- **Konflikt-Risiko:** keines auf Schema-Ebene; berührt `core.prisma` nur für User-Extension (admin-mfa-gate).
|
||||
|
||||
## Was wir bauen
|
||||
Die **getrennte Plattform-Admin-Welt** auf `admin.hivegrid.de` (Host-Routing, **NICHT** `/admin`-Pfad): eigene Admin-Shell (getrennte Sidebar/Topbar), **Cookie-Domain-Trennung** zu app.hivegrid.de, **TOTP-Pflicht + MFA-Step-up**. Admin-User haben **keinen Tenant-Workspace** und erscheinen in keiner Mitgliederliste (Boilerplate-Default-Provisionierung abschalten). Funktionen: **Tenant-Verwaltung** (Stammdaten, Status-Wechsel mit Reason-Pflicht, Notizen, Impersonate mit Begründung max. 30 Min + Banner), **Abo-Zusammenstellung** je Tenant (Feature-Konnektoren togglen + Stripe-Positionen = **Abo-Baukasten-Mechanik**), User-Verwaltung (2FA-Reset/Session-Revoke), **Feature-Flag-Verwaltung** (UI zur Engine aus P1), **Queue-/Health-Monitoring** (BullMQ Retry/Pause/Resume), **Support-Inbox** (speist sich aus P21-Feedback-Widget) und der **`AdminAuditLog`** (cross-tenant, tenant_id=null). **v1-Cut:** getrennte AdminUser-Identität/AdminRBAC (M038/M039) → v2; Plattform-Lead-Pipeline → v2; Flag-Massen-Statistik → v2; Churn/ARPU → v2.
|
||||
Die **getrennte Plattform-Admin-Welt** auf `admin.hivegrid.de` (Host-Routing, **NICHT** `/admin`-Pfad): eigene Admin-Shell (getrennte Sidebar/Topbar), **Cookie-Domain-Trennung** zu app.hivegrid.de, **TOTP-Pflicht + MFA-Step-up**. Admin-User haben **keinen Tenant-Workspace** und erscheinen in keiner Mitgliederliste (Boilerplate-Default-Provisionierung abschalten). Funktionen: **Tenant-Verwaltung** (Stammdaten, Status-Wechsel mit Reason-Pflicht, Notizen, Impersonate mit Begründung max. 30 Min + Banner), **Abo-/Entitlement-Zusammenstellung** je Tenant (Feature-Konnektoren togglen, Plan/Seats/Billing-State nach `BILL-*`), User-Verwaltung (2FA-Reset/Session-Revoke), **Feature-Flag-Verwaltung** (UI zur Engine aus P1), **Queue-/Health-Monitoring** (BullMQ Retry/Pause/Resume), **Support-Inbox** (speist sich aus P21-Feedback-Widget) und der **`AdminAuditLog`** (cross-tenant, tenant_id=null). **v1-Cut:** getrennte AdminUser-Identität/AdminRBAC (M038/M039) → v2; Plattform-Lead-Pipeline → v2; Flag-Massen-Statistik → v2; Churn/ARPU → v2.
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-ADMIN` + `D-P19`; eigene Admin-Shell)
|
||||
| Screen [Route] | Kern-Inhalt |
|
||||
|---|---|
|
||||
| Admin-Login + 2FA [`admin.hivegrid.de/login`, `/2fa`] | Email+Passwort (NIST/HIBP, Captcha ab 3. Fehler), TOTP-Pflicht, kein Magic-Link, 8h-Session |
|
||||
| Admin-Dashboard [`/dashboard`] | Basis-KPIs Tenants/aktive User/MRR + System-Health-Ampel + Queue-Status, Drill-Down (Churn/ARPU → v2) |
|
||||
| Tenant-Liste + Detail [`/tenants`, `/:id`] | Liste (Status/Plan/MRR/User/Aktivität); Detail: Status-Wechsler (Reason), **Abo-Zusammenstellung (Konnektoren + Stripe-Positionen)**, Trial-Verlängerung, Notizen, **Impersonate mit Begründung**, User-/Activity-/Audit-/Flag-Sektionen |
|
||||
| Tenant-Liste + Detail [`/tenants`, `/:id`] | Liste (Status/Plan/User/Aktivität); Detail: Status-Wechsler (Reason), **Abo-/Entitlement-Zusammenstellung (Konnektoren + Plan/Seats/Billing-State)**, Trial-Verlängerung, Notizen, **Impersonate mit Begründung**, User-/Activity-/Audit-/Flag-Sektionen |
|
||||
| Manuelle Tenant-Anlage [`/tenants/new`] | Name, Slug (Live-Validation), Branche/Größe, Initial-Admin-Email, Module/Plan, Trial, Demo-Daten-Häkchen, Reason |
|
||||
| User-Liste + Detail [`/users`, `/:id`] | Memberships/2FA/Sessions; Ops (2FA-Reset mit Reason, Session-Revoke, Deaktivieren, Lösch im Auftrag) |
|
||||
| Subscription-Liste + Detail [`/subscriptions`, `/:id`] | Plan/MRR/Status/Stripe-Sync; manuelle Override (Plan/Sonderpreis/Reason), Stripe-Sync forcieren, Refund |
|
||||
| Subscription-Liste + Detail [`/subscriptions`, `/:id`] | Plan/Seats/Status/Lago-Sync/Mollie-Payment-State; manuelle Override (Plan/Seats/Sonderfall/Reason), Sync forcieren, Refund-Verweis |
|
||||
| Feature-Flags [`/feature-flags`] | Flag-Liste + Targeting-Filter (plan-tier) + Tenant-Override mit Ablauf (Massen-Statistik → v2) |
|
||||
| Health + Worker-Queues [`/health`, `/health/workers`] | Service-Ampeln, DB/Redis/Storage; BullMQ-Queues Active/Waiting/Delayed/Failed, Job-Detail + Retry/Pause/Resume |
|
||||
| Support-Inbox + Detail [`/support`, `/:id`] | Liste + Konversation + Markdown-Reply + interne Notiz; **speist sich aus Feedback-Widget (P21)** |
|
||||
@@ -33,18 +38,18 @@ Die **getrennte Plattform-Admin-Welt** auf `admin.hivegrid.de` (Host-Routing, **
|
||||
| Subprozessoren + AVV [`/legal/subprocessors`, `/legal/avv`] | Subprozessor-Liste; AVV-Versionen + akzeptierende/ausstehende Tenants |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **Host-Routing `admin.hivegrid.de`** (NICHT `/admin`-Pfad!) + **Cookie-Domain-Trennung** App↔Admin
|
||||
- [ ] **TOTP-Pflicht** hart erzwungen; Admin **ohne Tenant-Workspace** (Default-Provisionierung abgeschaltet, in keiner Mitgliederliste)
|
||||
- [ ] **Tenant-Verwaltung** + **Abo-Zusammenstellung** (Konnektoren togglen + Stripe-Positionen) + Trial-Verlängerung + Notizen
|
||||
- [ ] **Impersonate** (temporäre Tenant-User-Session max. 30 Min) mit Klartext-Banner im Tenant + Reason-Pflicht
|
||||
- [ ] **User-Verwaltung** (2FA-Reset/Session-Revoke), **Feature-Flag-UI** (zur Engine aus P1)
|
||||
- [ ] **Queue-Monitor** (BullMQ Retry/Pause/Resume, Job-Detail mit Error-Stack) + **Support-Inbox**
|
||||
- [ ] **`AdminAuditLog`** (tenant_id=null, Hash-Kette); sensitive Aktionen mit Reason-Pflicht + Confirm-Phrase
|
||||
- [ ] Eigene getrennte Admin-Shell (kein Reuse der App-Sidebar)
|
||||
- [x] **Host-Routing `admin.hivegrid.de`** (NICHT `/admin`-Pfad!) + **Cookie-Domain-Trennung** App↔Admin
|
||||
- [x] **TOTP-Pflicht** hart erzwungen; Admin **ohne Tenant-Workspace** (Default-Provisionierung abgeschaltet, in keiner Mitgliederliste) — Authentik-SSO + JIT legt nur User+AdminUser an (kein Tenant), MFA via Authentik+Step-up
|
||||
- [ ] **Tenant-Verwaltung** + **Abo-/Entitlement-Zusammenstellung** (Konnektoren togglen + Plan/Seats/Billing-State nach `BILL-*`) + Trial-Verlängerung + Notizen — *Backend fertig: Abo-Baukasten-Konnektoren + AdminTenantNote. Offen: Trial-Verlängerung, manuelle Tenant-Anlage, Billing-Recut-Adminbasis aus `BILL-06`, finale UI (David)*
|
||||
- [ ] **Impersonate** (temporäre Tenant-User-Session max. 30 Min) mit Klartext-Banner im Tenant + Reason-Pflicht — *Cap 30 Min + Banner + MFA-Gate fertig. Offen: Operator-Reason-Modal (David)*
|
||||
- [x] **User-Verwaltung** (2FA-Reset/Session-Revoke), **Feature-Flag-UI** (zur Engine aus P1)
|
||||
- [ ] **Queue-Monitor** (BullMQ Retry/Pause/Resume, Job-Detail mit Error-Stack) + **Support-Inbox** — *Backend fertig (Service + VM + audited Aktionen). Offen: finale UI (David), BullMQ-Adapter-Runtime auf Staging*
|
||||
- [x] **`AdminAuditLog`** (tenant_id=null, Hash-Kette); sensitive Aktionen mit Reason-Pflicht + Confirm-Phrase — via AuditEvent-Querschnitt (tenantId=null/actorType=admin) + `/admin/audit` cross-tenant + Gate
|
||||
- [x] Eigene getrennte Admin-Shell (kein Reuse der App-Sidebar)
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/admin.prisma` — **KEIN `tenant_id`**, getrennte Welt): `AdminAuditLog` (admin_user_id, action_type, target_kind/id, summary_text, metadata Diff — cross-tenant), `AdminTenantNote` (tenant_id, note_text, category, pinned), `AdminSupportTicket` (+ Message — source_kind in_app_feedback_widget/inbound_email, tenant_id, status, priority). **v1-Cut:** `AdminUser`/`AdminRole`/`AdminSession` (M038/M039) → v2; v1 = Boilerplate-User-Extension + `admin-mfa-gate.server.ts`. `AdminLead`/`AdminDeal` → v2.
|
||||
- **Abo-Baukasten-Mechanik** (Billing-Domäne 12): Admin schaltet `SubscriptionFeature`-Konnektoren je Tenant + Stripe-Positionen — Grundlage für AK-P20 (Self-Service-UI dort → v2). `tenantHasModule()` prüft Feature im Abo.
|
||||
- **Abo-/Entitlement-Mechanik** (Billing-Domäne 12): Admin schaltet `SubscriptionFeature`-Konnektoren je Tenant; Plan/Seats/Billing-/Payment-State wird nach `BILL-02` bis `BILL-06` ueber Lago/Mollie/HiveGrid synchronisiert. `tenantHasModule()` prueft Feature im lokalen Entitlement-State.
|
||||
- **Host-Routing + Cookie-Domain-Trennung** (Boilerplate routet `/admin`-Pfad → auf Host umstellen); TOTP-Pflicht + MFA-Step-up-Gate.
|
||||
- **Impersonate** generiert temporäre Tenant-Session (max 30 Min) mit Banner + Audit-Spur; kein BYPASSRLS.
|
||||
- **AdminAuditLog** über die Audit-Hash-Kette (AK-P3); Reason-Pflicht bei Status-Wechsel/Impersonate/Override.
|
||||
@@ -60,3 +65,74 @@ Die **getrennte Plattform-Admin-Welt** auf `admin.hivegrid.de` (Host-Routing, **
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P19` aktualisiert
|
||||
|
||||
## Umsetzungs-Notiz
|
||||
|
||||
### 2026-06-25 — Backend-Fundament (Branch `feat/AK-P19`)
|
||||
|
||||
**Vorab-Entscheidungen (mit User abgestimmt):**
|
||||
- **Admin-Login = Authentik-SSO** (juengere, in Prod laufende Infra-Entscheidung: AK-INFRA2, AGENTS.md STANDARDS §5/AD-3). Der aeltere Brief-Text „Email+Passwort+TOTP" gilt damit nicht; DoD #2 = TOTP/MFA via Authentik + `admin-mfa-gate` Step-up.
|
||||
- **DoD #7 `AdminAuditLog`** wird NICHT als eigenes Modell gebaut, sondern ueber die bestehende **AuditEvent-Querschnitt-Huelle** (tenantId=null, actorType=admin, Hash-Kette aus AK-P3). Grund: AGENTS.md verbietet Querschnitt-Nachbau; AuditEvent unterstuetzt tenantId=null bereits. `admin.prisma` enthaelt daher kein Audit-Modell.
|
||||
|
||||
**Ist-Bestand (Boilerplate, verifiziert — zaehlt auf DoD):** Host-Routing `admin.hivegrid.de` + Cookie-Trennung (DoD #1 ✅), eigene Admin-Shell/Sidebar (DoD #8 ✅), User-Ops + Feature-Flag-UI (DoD #5 ✅ weitgehend), Impersonate gewired mit MFA-Gate + Audit (DoD #4 teilweise).
|
||||
|
||||
**Diese Session gebaut + verifiziert:**
|
||||
- `prisma/schema/admin.prisma` — `AdminTenantNote` (Domaene 11) + `SupportTicketMessage` (Konversation/interne Notiz, erweitert bestehendes `SupportTicket` statt Duplikat). Minimale Back-Relations. `prisma validate` + `generate` gruen. **`db push` steht noch aus** (braucht laufende Dev-DB), Migration erst beim Merge (AD-21).
|
||||
- `apps/web/app/services/admin/admin-action-audit.server.ts` — Operator-Action-Audit-Gate (DoD #7): Reason-Pflicht + Confirm-Phrase + Emit als AuditEvent (tenantId=null/actorType=admin) ueber `createAuditEvent`. Unit-Test 10/10.
|
||||
- `packages/view-models/src/admin/tenant-note-list.vm.ts` — reiner Notizen-VM (DoD #3, pinned-first, i18n-Keys, Empty-State). Unit-Test 6/6.
|
||||
|
||||
**Proof:** web-Typecheck Exit 0, view-models-Typecheck clean, `verify:i18n` Exit 0, Tests 16/16. Commits `6cf7a55b4` (Schema), `e562b2752` (Audit-Gate + VM). Nicht gepusht, kein Status-Wechsel.
|
||||
|
||||
**%-DoD-Stand:** rund 40 % (3 von 8 voll gruen: #1/#5/#8; #7 ~70 % = Gate gebaut, offen Viewer-Cross-Tenant + Verdrahtung an Aktionen; #3 ~30 % = Notizen-VM/Audit, offen Abo-Baukasten/Trial/Service).
|
||||
|
||||
**Selbst machbar offen (Backend, ohne David):** Abo-Baukasten-Mechanik (Konnektoren je Tenant + Plan/Seats/Billing-State nach `BILL-*`), Queue-Monitor (BullMQ), Support-Inbox-Service (auf `SupportTicketMessage` + bestehendem `@hivegrid/db/support-tickets.server`), AdminTenantNote-Service (CRUD), Impersonate-Haertung (30-Min-Cap/Banner), Admin-ohne-Tenant-Workspace-Haertung, Audit-Viewer Cross-Tenant-Erweiterung + Call-Site-Verdrahtung, `db push`.
|
||||
**Extern blockiert:** finale Subframe-UI aller Screens (Davids `D-ADMIN`/`D-P19`-Delta noch leer).
|
||||
|
||||
### 2026-06-25 (Forts.) — Backend-Slices Abo-Baukasten / Queue / Support / Notizen
|
||||
|
||||
**Zusaetzlich gebaut + verifiziert:**
|
||||
- **Abo-Baukasten-Mechanik (DoD #3, Grundlage AK-P20):** `services/admin/subscription-assembly.server.ts` toggelt Feature-Konnektoren je Tenant ueber `TenantFeatureOverride` (enabled/disabled/inherit), invalidiert den `tenantHasModule`-Cache und auditet reason-gegated. Reuse des Entitlement-Resolvers, kein Parallel-Store. + `subscription-connectors.vm.ts` (effektiver Zustand aus Blueprint-Default + Override).
|
||||
- **Queue-Monitor (DoD #6 Teil A):** `services/admin/queue-monitor.server.ts` — `QueueInspector`-Port + Snapshot-Aggregation + audited Pause/Resume/Retry; BullMQ-Adapter ueber `@hivegrid/queue` (Redis-Runtime auf Staging zu verifizieren). + `queue-monitor.vm.ts` (Counts -> Klartext-Health ok/warn/paused, Worst-of-Overall). Hinweis: `TypedQueue` ist enqueue-only, Read/Control via strukturellem Cast.
|
||||
- **Support-Inbox (DoD #6 Teil B):** `services/admin/support-inbox.server.ts` — Reply / interne Notiz / Status / Assignee auf dem `SupportTicketMessage`-Thread, validiert + auditiert. + `support-conversation.vm.ts` (Thread oldest-first, reply/internalNote/inbound).
|
||||
- **Tenant-Notes (DoD #3):** `services/admin/tenant-notes.server.ts` — create/delete/list `AdminTenantNote` mit Kategorie-Normalisierung + Audit.
|
||||
|
||||
**Proof:** web-Typecheck 0 Fehler (grep `error TS` = 0; Achtung: gepipte Typechecks maskieren tsc-Exit, daher gegen Inhalt geprueft), view-models-Typecheck clean, `verify:i18n` Exit 0, Admin-Tests **52/52** in 9 Dateien. Commits `0bb9e29ad` (Abo-Baukasten+Queue), `116aee518` (Queue-BullMQ-Cast-Fix), `c9dedcafe` (Support+Notizen). Nicht gepusht, kein Status-Wechsel.
|
||||
|
||||
**%-DoD-Stand jetzt:** rund **60 %**. Voll gruen: #1/#5/#8 (Boilerplate, verifiziert). Backend-fertig (UI = David): #6 (Queue+Support), #3-Teile (Abo-Baukasten + Notizen). #7 ~80 % (Gate gebaut + an Abo/Queue/Support-Aktionen verdrahtet; offen: Audit-Viewer Cross-Tenant + Impersonate/Status-Call-Sites).
|
||||
|
||||
**Noch offen, selbst machbar:** Audit-Viewer Cross-Tenant-Erweiterung, Impersonate-Haertung (30-Min/Banner) + Reason-Verdrahtung, Admin-ohne-Tenant-Workspace-Haertung (DoD #2), Trial-Verlaengerung + manuelle Tenant-Anlage, Billing-Recut-Adminbasis aus `BILL-06`, `db push` (Dev-DB) + VM-Unit-Tests laufen, Smoke-/Visual-Tests nach Davids UI. **Extern blockiert:** finale Subframe-Screens (David).
|
||||
|
||||
### 2026-06-25 (Forts. 2) — DoD #2 / #4 / #7 verifiziert bzw. geschlossen
|
||||
|
||||
Drei Punkte gegen den realen Code geprueft statt neu gebaut:
|
||||
- **DoD #2 (Admin ohne Tenant-Workspace) = erfuellt.** Der Admin-Login ist Authentik-SSO; `modules/admin-sso/jit-provisioning.server.ts` legt **nur** `User` + `AdminUser` an, **keinen** Tenant, **keine** `TenantUser`-Mitgliedschaft, **kein** `defaultTenantId`. Damit hat ein Admin keinen Workspace und taucht in keiner Mitgliederliste auf. TOTP/MFA laeuft ueber Authentik + `admin-mfa-gate` Step-up (bestehend). Verifiziert per Code-Read + bestehende `jit-provisioning.server.test.ts` (Fake-Client hat gar kein tenant-Modell).
|
||||
- **DoD #7 (AdminAuditLog cross-tenant + Viewer) = erfuellt.** Der `/admin/audit`-Viewer (AK-P3) liest ohne `tenantId`-Param **alle** AuditEvent-Zeilen inkl. der `tenantId=null`/`actorType=admin`-Operator-Aktionen, filterbar nach Actor/Tenant. Mein Gate schreibt genau dorthin; Reason + Confirm-Phrase liegen im Gate. Kein Viewer-Umbau noetig.
|
||||
- **DoD #4 (Impersonate max. 30 Min) = geschlossen.** `IMPERSONATION_SESSION_TTL_MS` war 60 Min -> auf **30 Min** gesetzt; `validateImpersonationSessionState` liefert danach `stale_started_at`. Banner (rootData) + MFA-Step-up bestehen. Test: 29 Min gueltig / 31 Min stale. Offen nur das Operator-Reason-Modal auf der neuen Tenant-Detail-Impersonate-Aktion (Davids UI).
|
||||
|
||||
Commit `feat(AK-P19): cap operator impersonation at 30 minutes`. web-Typecheck 0 Fehler, session-Impersonation-Tests 16/16.
|
||||
|
||||
**%-DoD-Stand jetzt: rund 70 %.** Voll gruen: #1, #2, #4 (bis auf Reason-Modal=David), #5, #7, #8. Backend-fertig (UI=David): #6 (Queue+Support), #3 (Abo-Baukasten+Notizen). Rest #3: Trial-Verlaengerung + manuelle Tenant-Anlage + Billing-Recut-Adminbasis aus `BILL-06` + `db push`.
|
||||
|
||||
### 2026-06-26 — `prisma db push` + DB-Integrationstest (real-DB-Verifikation)
|
||||
|
||||
- **`db push` erfolgreich** gegen eine frische pgvector-Postgres (Docker, da compose kein Postgres mitbringt): das **gesamte** Schema inkl. `admin.prisma` synct sauber. Verifiziert per `psql`: Tabellen `AdminTenantNote`, `SupportTicketMessage` + Spalte `SupportTicket.assignedToUserId` vorhanden. Damit AD-21 (Feature-Branch = `db push`, Migration erst beim Merge) erfuellt. **Befehl:** `prisma db push --url <direct-url>` (Prisma 7 kennt kein `--skip-generate` mehr).
|
||||
- **DB-Integrationstest** `admin-services.integration.test.ts`: faehrt den **echten** Service-Code (tenant-notes, subscription-assembly, support-inbox) ueber einen echten Prisma-Client (`createDbClient({connectionString})`) gegen die gepushte DB — faengt Query-/Schema-Mismatches, die die Fake-Unit-Tests nicht sehen. **3/3 gruen** mit DB, **auto-skip** ohne `ADMIN_IT_DATABASE_URL` (CI bleibt gruen). web-Typecheck 0 Fehler.
|
||||
|
||||
Commits `test(AK-P19): DB-backed integration test`, `test(AK-P19): extend ... support-inbox thread`. Damit ist der selbst-machbare Backend-Teil **gegen eine echte DB verifiziert**; `db push` aus der offenen DoD-Restliste erledigt. Es fehlt selbst-machbar nur noch: Trial-Verlaengerung + manuelle Tenant-Anlage + Billing-Recut-Adminbasis (`BILL-06`, Lago/Mollie/HiveGrid-gekoppelt) und Davids finale UI.
|
||||
|
||||
## Noch zu erledigen (Stand 2026-06-26, ~70 % DoD)
|
||||
|
||||
**A — Selbst machbar, aber Billing-/Multi-Subsystem-gekoppelt (eigene CR/Folge-Arbeit sinnvoll):**
|
||||
- [ ] **Trial-Verlängerung** je Tenant (Subscription-Trial-Ende setzen + Reason + Audit) — haengt am neuen Billing-State aus `BILL-02`/`BILL-05`.
|
||||
- [ ] **Manuelle Tenant-Anlage** `/tenants/new` (Name/Slug-Live-Validation, Branche/Größe, Initial-Admin-Email, Module/Plan, Trial, Demo-Daten, Reason) als DoD-konforme **neue** Route (nicht die Legacy-`accounts/$id`-Route umbauen) — berührt Tenant-Create + Initial-User + Subscription + Demo-Seed.
|
||||
- [ ] **Subscription-Override / Billing-State-Seite** (Plan/Seats/Sonderfall/Refund-Verweis, Lago/Mollie-Sync forcieren) — umgesetzt ueber `BILL-06`, nicht ueber Legacy-Provider.
|
||||
|
||||
**B — Extern blockiert (Davids `D-ADMIN`/`D-P19`-Delta ist leer):**
|
||||
- [ ] Finale Subframe-Screens aller Admin-Routen (Tenant-Detail mit Abo-Baukasten-Panel + Notizen-Editor, Queue-Monitor, Support-Inbox, Impersonate-Begründungs-Modal, manuelle Tenant-Anlage).
|
||||
- [ ] Daran hängende DoD-Querschnitte: Reachability/Sidebar-Gate je neuer Route, Breadcrumbs, **Smoke-Tests** `e2e/smoke/admin-*.spec.ts`, **Hot-Swap-Drill**, **Visual-Regression-Snapshots**, Empty/Loading/Permission-Denied-States.
|
||||
|
||||
**C — Betrieb/Runtime:**
|
||||
- [ ] **BullMQ-Queue-Monitor-Adapter** gegen echtes Redis auf **Staging** verifizieren (Unit-Logik + Orchestrierung sind getestet; der Live-Adapter braucht Redis).
|
||||
- [ ] Operator-**Reason-Modal** auf der neuen Tenant-Detail-Impersonate-Aktion ans Gate verdrahten (Backend-Enforcement steht, UI = David).
|
||||
|
||||
**Fertig (Backend verifiziert, gegen echte DB):** admin.prisma (`db push` ok), Operator-Audit-Gate (Reason+Confirm-Phrase), Abo-Baukasten-Konnektoren, Tenant-Notizen, Queue-Monitor-Logik, Support-Inbox-Service, Impersonate-Cap 30 Min. DoD #1/#2/#5/#7/#8 grün.
|
||||
@@ -1,4 +1,9 @@
|
||||
# AK-P20 · P20 Billing (Stripe Abo-Baukasten) + Onboarding + Branding
|
||||
# AK-P20 · P20 Billing-Grundlagen + Onboarding + Branding
|
||||
|
||||
> ⚠️ **Billing-Recut 2026-07-04 ([[AD-27]]):** Die Legacy-Provider-Annahmen in diesem Ticket sind Doku-Drift. Zielarchitektur
|
||||
> ist **Lago self-hosted fuer Billing/Invoicing + Mollie nur fuer Payments + HiveGrid-Entitlements**. v1-Scope:
|
||||
> **Plans + Seats**, kein Usage-Billing. Legacy-Provider-Codepfade aus der bisherigen Teilumsetzung sind Migrationsmaterial fuer
|
||||
> `BILL-02` bis `BILL-05`, nicht mehr Zielarchitektur.
|
||||
|
||||
> ⚠️ **KI→MCP-Pivot ([[AD-26]]):** Der **AI-Credits-Screen + AI-Token-Quota entfallen** (kein LLM-Gateway in v1 — HiveGrid wird MCP-Server, [[VR-MCP]]; `AiCredits`/`AiTokenUsage` gestrichen). Die Verbrauchs-Übersicht bleibt für Automation-Läufe + Speicher. Alle AI-Credits/AI-Token-Stellen unten sind durchgestrichen.
|
||||
|
||||
@@ -8,49 +13,81 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P20 — Billing, Onboarding & Branding |
|
||||
| **Cycle/Fenster** | W4 — USP, Härtung & Launch · fällig 2026-07-08 · Gate W4 |
|
||||
| **Status** | geplant |
|
||||
| **Status** | Recut / On Hold fuer Billing-Anteil; Onboarding/Branding/Frozen-Vorarbeit verwertbar |
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `EXT-07` (Stripe-Account + Produkte/Preise + Webhook-Endpoint), `AK-P19` (Abo-Baukasten-Mechanik + Admin-Tenant-Verwaltung als Voraussetzung)
|
||||
- **Blocks:** `AK-P21` (DSGVO/Launch — Tenant-Löschung braucht Stripe-Cancel)
|
||||
- **Blocked by:** `BILL-00` bis `BILL-05` fuer den neuen Billing-Kern; `AK-P19` fuer Admin-/Tenant-Verwaltungsgrundlagen; `D-P20` fuer finale Screens
|
||||
- **Blocks:** `AK-P21` teilweise (Read-only/Frozen-State und Billing-Cancel-Vertrag), `BILL-06` (Admin-Billing-Basis), spaetere Customer-Billing-Screens
|
||||
- **Läuft parallel zu:** Raffaels Kern-Kette (Auftrags-/Abnahme-Welt P9). Eigene `prisma/schema/billing.prisma` + Onboarding-Teile (AD-21); `core.prisma` (Tenant) nur über Relation.
|
||||
- **Konflikt-Risiko:** gering — additive Tabellen + Read-Only-Middleware (Frozen) auf der Request-Pipeline (Querschnitt, additiv).
|
||||
|
||||
## Was wir bauen
|
||||
Drei gebündelte v1-Module: **Billing als Stripe-Abo-Baukasten** (M154-Recut) — Basis-Preis pro Nutzer + Feature-Module als **daten-getriebene Positionen**, **KEINE Seat-Typen**; In-App **keine** Pricing-/Upgrade-/Plan-Wechsel-UI (Apple-Compliance LP-8) — nur Read-Only-Übersicht + externer Verweis; **Trial + Dunning** (Tag 1/3/7) + **Frozen-Modus** (Read-Only-Middleware); Verbrauchs-Zähler + Drossel (Aufschlags-Abrechnung → v2). **Onboarding** = **Auto-SHK-Provisionierung** beim Signup (Seeds M158, **ohne Wizard** — der ist v2) + Demo-Daten-Knopf + Erste-Schritte-Checkliste. **Branding** = nur **Hell/Dunkel-Toggle** (kein System-Modus), flash-frei via SSR-Cookie.
|
||||
|
||||
Drei gebündelte v1-Module:
|
||||
|
||||
1. **Billing-Grundlagen:** lokaler Billing-/Entitlement-State fuer Plans + Seats, Trial/Dunning/Frozen, Read-only-State
|
||||
und Feature-Gates. Lago wird spaeter Billing-Source-of-Truth, Mollie nur Payment Processor.
|
||||
2. **Onboarding:** Auto-SHK-Provisionierung beim Signup (Seeds M158, ohne Wizard) + Demo-Daten-Knopf +
|
||||
Erste-Schritte-Checkliste.
|
||||
3. **Branding:** nur Hell/Dunkel-Toggle (kein System-Modus), flash-frei via SSR-Cookie.
|
||||
|
||||
Nicht mehr Ziel dieses Tickets: Legacy-Provider-Abo-Baukasten, Legacy-Provider-Customer-Portal, Payment-Checkout, Usage-Billing,
|
||||
AI-Credits oder Pricing-/Upgrade-UI fuer Endkunden.
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-P20`)
|
||||
| Screen [Route] | Kern-Inhalt |
|
||||
|---|---|
|
||||
| Abo-Übersicht [`/admin/subscription`] | Basis-Plan + **Feature-Module als Positionsliste mit Preisen** (read-only), Status/Trial-Countdown, **KEIN Upgrade-Button** (Hinweis-Box „Plan-Wechsel über Account-Manager/hivegrid.de"), Pausieren + Kündigen |
|
||||
| Verbrauchs-Übersicht [`/admin/usage`] ~~· AI-Credits [`/admin/usage/ai-credits`]~~ (AI-Credits-Screen entfällt — [[AD-26]]) | Quota-Balken je Metrik (Automation/~~AI-Tokens~~ (entfällt)/Speicher), ~~Donut AI-Verbrauch~~, 80%-Warnbanner, Aufgebraucht-Klartext |
|
||||
| Rechnungen/Zahlung [`/admin/billing`] | **Stripe-Customer-Portal-Embed** (Rechnungs-History/PDF, Zahlungsmethode, USt-Daten) |
|
||||
| Rechnungen/Zahlung [`/admin/billing`] | Lago-Invoice-Status + Mollie-Payment-Status + Klartext-Hilfe; kein Legacy-Provider-Portal-Embed |
|
||||
| Demo-Daten [`/admin/onboarding/demo-data`] | Settings-Knopf „Beispieldaten anlegen" (~25 Datensätze, Klartext-Liste), Demo-Zähler + Lösch-Funktion, „Demo ausblenden"-Filter |
|
||||
| Erste-Schritte-Checkliste [`/onboarding/checklist`] | Auto-erkannte Status-Häkchen, CTA-Links, manuell abhakbar/ausblendbar, Glückwunsch bei 100% (auch Dashboard-Widget) |
|
||||
| Status-Banner-System (Shell) | trial-Ende-3-Tage / past_due / frozen / deletion_pending je eigener Klartext + Action; Read-Only-Toast bei blockiertem Schreib-Button |
|
||||
| Hell/Dunkel-Toggle [Profil/User-Menü] | Zwei Optionen, sofortige Anwendung, flash-frei (SSR-Cookie) |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **Stripe-Abo-Baukasten**: Basis-Preis pro Nutzer + Feature-Module als daten-getriebene Positionen — **KEINE Seat-Typen** (M154-Recut); Preise geparkt (AD-15)
|
||||
- [ ] **Trial + Dunning** (Tag 1/3/7) + **Frozen-Modus** (Read-Only-Middleware) greift; Status-Banner pro Zustand; Verbrauchs-Zähler + Drossel (Aufschlags-Abrechnung → v2)
|
||||
- [ ] **In-App keine** Pricing-Tabelle/Upgrade-Buttons/Plan-Wechsel-UI (LP-8) — Read-Only + externer Verweis; Pause/Kündigen erlaubt
|
||||
- [ ] **Auto-SHK-Provisionierung** beim Signup (Seeds M158: Pipelines, Custom-Felder, Rollen, Form-Templates, Checkliste) — **ohne Wizard-UI** (v2)
|
||||
- [ ] **Demo-Daten-Knopf** (per Settings, `is_demo_data`-Flag löschbar) + „Demo ausblenden"-Filter
|
||||
- [ ] **Erste-Schritte-Checkliste** (auto-erkannter Status, CTA-Links, abhakbar/ausblendbar; Dashboard-Widget)
|
||||
- [ ] **Hell/Dunkel-Toggle** flash-frei (SSR-Cookie, `next-themes`) — kein System-Modus
|
||||
- [ ] Konnektor `tenantHasModule('billing')`; RLS auf allen Tenant-Tabellen
|
||||
- [ ] **Billing-Recut-Basis**: lokaler Plan-/Seat-/Entitlement-State ist auf Lago/Mollie/HiveGrid ausgerichtet; keine Legacy-Provider-IDs als Zielmodell; kein Usage-Billing
|
||||
- [x] **Trial + Dunning** (Tag 1/3/7) + **Frozen-Modus** (Read-Only-Middleware) greift; Status-Banner pro Zustand; Verbrauchs-Zähler + Drossel (Aufschlags-Abrechnung → v2)
|
||||
- [x] **In-App keine** Pricing-Tabelle/Upgrade-Buttons/Plan-Wechsel-UI (LP-8) — Read-Only + externer Verweis; Pause/Kündigen erlaubt
|
||||
- [x] **Auto-SHK-Provisionierung** beim Signup (Seeds M158: Pipelines, Custom-Felder, Rollen, Form-Templates, Checkliste) — **ohne Wizard-UI** (v2)
|
||||
- [x] **Demo-Daten-Knopf** (per Settings, `is_demo_data`-Flag löschbar) + „Demo ausblenden"-Filter
|
||||
- [x] **Erste-Schritte-Checkliste** (auto-erkannter Status, CTA-Links, abhakbar/ausblendbar; Dashboard-Widget)
|
||||
- [x] **Hell/Dunkel-Toggle** flash-frei (SSR-Cookie, `next-themes`) — kein System-Modus
|
||||
- [x] Konnektor `tenantHasModule('billing')`; RLS auf allen Tenant-Tabellen
|
||||
|
||||
## Aktueller Stand (Billing-Recut 2026-07-04)
|
||||
|
||||
**Fortschritt:** **AK-P20: 7/8 historischer DoD = 87,5 % done; nach AD-27 bleibt 1/8 blockiert/ersetzt durch `BILL-02` bis `BILL-05`.**
|
||||
|
||||
**Erledigt:**
|
||||
- Trial/Dunning/Frozen inkl. Statusbanner, zentralem Read-only-Guard, Usage-Zählern und Drossel.
|
||||
- In-App-Subscription bleibt read-only: keine Pricing-Tabelle, keine Upgrade-Buttons, keine Planwechsel-UI; Billing-/Zahlungsaktionen bleiben nur als freigegebene Recovery-/Kontaktpfade erreichbar.
|
||||
- Auto-SHK-Provisionierung beim Signup mit Rollen, Pipeline, Formular-Templates, Projekt-/Abnahme-Templates und Checkliste.
|
||||
- Demo-Daten-Knopf, `isDemoData`-Tracking, Löschung und Standardfilter “Demo ausblenden”.
|
||||
- Erste-Schritte-Checkliste mit Auto-Erkennung, manuellen Zuständen und Dashboard-Widget.
|
||||
- Hell/Dunkel-Toggle ohne System-Modus, SSR-Cookie-basiert.
|
||||
- Billing-Konnektor `tenantHasModule('billing')`, Tenant-Isolation/RLS-Artefakte und Boundary-Gates.
|
||||
- Seat-Typen im Plattform-Admin-Pricing blockiert: keine `PER_SEAT`-Seeds, keine `PER_SEAT`-Auswahl, serverseitige Guards gegen manipulierte Formulare.
|
||||
|
||||
**Offen:**
|
||||
- Neuer Billing-Abschluss: lokale Billing-Domain auf Lago-/Mollie-Referenzen umstellen, Legacy-Provider-spezifische Felder/Services inventarisieren und ueber `BILL-02` bis `BILL-05` ersetzen.
|
||||
- Finale Subframe-/Screen-Integration aus `D-P20`.
|
||||
|
||||
**Recut-Entscheidung:** AK-P20 wird nicht still auf Done gesetzt. Die verwertbaren Teile (Trial/Dunning/Frozen,
|
||||
Read-only, Onboarding, Demo-Daten, Checkliste, Theme, Entitlement-Gates) bleiben Grundlage. Der alte
|
||||
Legacy-Provider-Abo-Baukasten-DoD wird durch `BILL-02` bis `BILL-05` ersetzt.
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/billing.prisma`): `SubscriptionProduct` (Plan Core/Swarm/Colony — global), `SubscriptionPrice` (stripe_price_id, interval), `SubscriptionFeature` (+ `ProductFeature`, quota_value JSON — **Brücke zur Konnektor-Pflicht**, `tenantHasModule()`), `SubscriptionUsageBasedPrice`/`UsageBasedTier`, `TenantSubscription` (1:1 Tenant, status trial/active/past_due/cancelled/frozen, Stripe-IDs, Dunning-Felder), `TenantSubscriptionProductPrice` (quantity=Seats, is_metered), `TenantSubscriptionUsageRecord`, `Credit`, `CheckoutSessionStatus`. Onboarding: `Onboarding`/`OnboardingSession`/`OnboardingDemoProvisioning`. Branding: `User.theme_preference` (Feld, kein eigenes Modell). `core.prisma` über Tenant-Relation (AD-21).
|
||||
- **Abo-Baukasten** (M154-Recut): `SubscriptionFeature.code` (z.B. `kwp_integration`) = Entitlement-Schicht; Struktur aus Legacy `pricing-plans.ts` portieren, Preise = Kostenmatrix (AD-15).
|
||||
- **Stripe-Integration**: Webhook-Handler (Signatur), Customer-Portal-Embed, Dunning-Email-Sequenz, Frozen-Read-Only-Middleware auf der Request-Pipeline.
|
||||
- **Entities** (eigene `prisma/schema/billing.prisma`): bestehende Subscription-/Feature-/Usage-Tabellen bleiben Migrationsbasis, muessen aber in `BILL-02` von Legacy-Provider-zentrierten Referenzen auf Lago-/Mollie-/HiveGrid-Referenzen umgestellt werden. Onboarding: `Onboarding`/`OnboardingSession`/`OnboardingDemoProvisioning`. Branding: `User.theme_preference` (Feld, kein eigenes Modell). `core.prisma` nur ueber noetige Tenant-Relationen (AD-21).
|
||||
- **Abo-/Entitlement-Schicht:** `SubscriptionFeature.code` (z.B. `kwp_integration`) = Entitlement-Schicht; `tenantHasModule()` bleibt zentrale Gate-API.
|
||||
- **Billing-Integration neu:** Lago-Gateway (`BILL-03`), Mollie-Payment-Gateway (`BILL-04`), Sync/Frozen-State (`BILL-05`). Die vorhandenen Legacy-Provider-Webhook-/Dunning-Tests sind Migrationsvorlage, nicht Ziel.
|
||||
- **Auto-SHK-Provisionierung** orchestriert die Self-Seeds aller Module (AD-12); Demo-Daten-Seed via `is_demo_data`; Trial-Freischaltung durch Plattform-Admin (AK-P19).
|
||||
- **RLS** überall (global: Produkte/Features/Preise); `next-themes` SSR-Cookie für Hell/Dunkel.
|
||||
|
||||
## Quellen (granular)
|
||||
- `ENTITY-MODEL.md` → **Domäne 12: Billing & Abo-Baukasten** (Recut-Modell, keine Seat-Typen) + **Domäne 13** (Onboarding, OnboardingDemoProvisioning, User.theme_preference)
|
||||
- `REQUIREMENTS.md` → `REQ-billing-datenmodell`, `REQ-billing-trial-dunning`, `REQ-onboarding-demo-daten`, `REQ-branding-theme-phase1` (Onboarding-Wizard → v2)
|
||||
- `ROADMAP.md` → Phase 20 · `PHASE-SOURCES.md` → Phase 20 (M153-stripe, M154-recut bindend, M155-trial-readonly, M156-metering Zähler+Drossel, M158-shk-provisionierung, M159-branchen-templates nur SHK, M160-demo gedünnt, M030 nur Hell/Dunkel)
|
||||
- `ROADMAP.md` → Phase 20 · `PHASE-SOURCES.md` → Phase 20 (M153-legacyProvider, M154-recut bindend, M155-trial-readonly, M156-metering Zähler+Drossel, M158-shk-provisionierung, M159-branchen-templates nur SHK, M160-demo gedünnt, M030 nur Hell/Dunkel)
|
||||
- `designer-briefs/P20-billing-onboarding-branding.md` (Abo-Baukasten, kein Upgrade-UI LP-8, Demo-Knopf, Hell/Dunkel)
|
||||
- `_bmad-output/specs/M28-subscriptions-billing.md` (⚠ M154-Recut), `M29-onboarding.md` (Wizard → v2), `M31-branding.md` · Legacy: `archive/hivegrid-cloud/app/config/pricing-plans.ts`
|
||||
|
||||
@@ -58,3 +95,215 @@ Drei gebündelte v1-Module: **Billing als Stripe-Abo-Baukasten** (M154-Recut)
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P20` aktualisiert
|
||||
|
||||
## Umsetzungs-Notiz
|
||||
|
||||
> Hinweis 2026-07-04: Die folgenden Slice-Notizen dokumentieren historische Umsetzung vor `AD-27`. Wo sie Legacy-Provider nennen,
|
||||
> ist das als Migrationsinventar/Vorarbeit zu lesen. Der aktive Zielpfad ist `BILL-02` bis `BILL-05`.
|
||||
|
||||
**2026-07-01 · AK-P20 Teilumsetzung**
|
||||
|
||||
Umgesetzt:
|
||||
- Billing-Grundlage erweitert: `SubscriptionFeature.code`/`quotaValue`, Subscription-Statusfelder, metered Usage-Records mit Idempotenz, Webhook-Log, `billing`-Entitlement und `tenantHasModule('billing')`-Gate auf der Subscription-/Usage-Route.
|
||||
- In-App-AI-Credits entfernt bzw. auf Subscription umgeleitet; Subscription-UI bleibt read-only mit externem Hinweis, Portal/Kündigen bleiben erhalten.
|
||||
- Verbrauchsübersicht für Automation-Läufe/Speicher, Billing-Statusbanner-VM und Shell-Banner vorbereitet.
|
||||
- SHK-Onboarding-Grundlage: Signup ruft SHK-Defaults auf, Checkliste und Demo-Daten-Service/-Routes mit `isDemoData`-Tracking und löschbarer Provisionierung.
|
||||
- Hell/Dunkel-Toggle auf zwei Modi begrenzt, kein System-Fallback mehr.
|
||||
- Tenant-Isolation/RLS-Nachzug: `OnboardingDemoProvisioning`, Usage-Records und RLS-Artefakt registriert; Windows-kompatible Verify-Wrapper für die betroffenen Baseline-Gates repariert.
|
||||
|
||||
Proof:
|
||||
- `pnpm prisma validate`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm run verify:sidebar-routes`
|
||||
- `pnpm run verify:tenant-isolation-report`
|
||||
- `pnpm run verify:billing-boundary-baseline`
|
||||
- `pnpm run verify:onboarding-flow`
|
||||
- `pnpm --filter @hivegrid/entitlements test`
|
||||
- `pnpm --filter @hivegrid/cli exec vitest run src/commands/tenant/models.test.ts`
|
||||
- `pnpm --filter @hivegrid/web exec vitest run app/application/sidebar/AppSidebar.test.tsx app/utils/theme/userThemePreference.server.test.ts 'app/routes/app.$tenant/settings/subscription.test.tsx' app/utils/services/.server/billingStatusService.test.ts`
|
||||
|
||||
Fortschritt: **AK-P20: 2/8 DoD = 25 % done**.
|
||||
|
||||
Abgehakt: Hell/Dunkel ohne System-Modus; Billing-Konnektor/RLS.
|
||||
|
||||
Teilweise umgesetzt, aber noch nicht DoD-grün: Legacy-Provider-Abo-Baukasten-Grundschema, Trial/Frozen-Statusdaten + Banner, read-only Subscription-Oberfläche, Auto-SHK-Provisionierung, Demo-Daten-Knopf, Erste-Schritte-Checkliste.
|
||||
|
||||
Offen/blockiert: `EXT-07` Legacy-Provider-Account/Produkte/Webhook, `AK-P19` Admin-Abo-Baukasten/Trial-Freischaltung, `D-P20` finale Screens/Subframe-Delta; außerdem selbst machbar offen sind vollständige Dunning-Sequenz Tag 1/3/7, Frozen-Read-Only-Middleware, Demo-ausblenden-Filter, Checklist-Auto-Erkennung und Dashboard-Widget.
|
||||
|
||||
**2026-07-01 · AK-P20 Slice 1 Nachzug: Legacy-Provider-Webhooks + Dunning/Frozen**
|
||||
|
||||
Umgesetzt:
|
||||
- Legacy-Provider-Webhook-Einstieg `/webhooks/legacyProvider` mit Signaturprüfung über `MOLLIE_API_KEY` und idempotentem `Legacy-ProviderWebhookLog`.
|
||||
- Legacy-Provider-Status-Sync für `customer.subscription.updated/deleted`, `invoice.payment_failed`, `invoice.paid` und `invoice.payment_succeeded`; Subscription-Periodenfelder werden aus Legacy-Provider übernommen.
|
||||
- Dunning-Entscheidung Tag 1/3/7 plus Freeze nach ausgeschöpfter Mahnstufe; `pastDueSince` bleibt bei wiederholten Legacy-Provider-Fehlerereignissen stabil.
|
||||
- Dunning-Email-Sequenz erzeugt Billing-Notifications über den bestehenden Notification-Delivery-Boundary (`category=billing`, `channel=EMAIL`).
|
||||
- Billing-Read-Only-State zentral auslesbar und erste Tenant-Shell-Schreibaktionen blockieren im Frozen/Read-only-Status mit 423.
|
||||
- Tests für Dunning-Entscheidung, Dunning-Cycle, Legacy-Provider-Webhook-Route, Legacy-Provider-Signaturkonstruktion, Status-Read-only-State und Tenant-Action-Block ergänzt.
|
||||
|
||||
Proof:
|
||||
- `pnpm --filter @hivegrid/web exec vitest run app/utils/services/.server/billingStatusService.test.ts app/utils/services/.server/billingDunningService.test.ts app/routes/webhooks.legacyProvider.test.ts app/utils/legacyProvider.server.test.ts 'app/routes/app.$tenant.test.tsx'`
|
||||
- `pnpm prisma validate`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm run verify:sidebar-routes`
|
||||
- `pnpm run verify:billing-boundary-baseline`
|
||||
|
||||
Fortschritt: **AK-P20: 2/8 DoD = 25 % done**.
|
||||
|
||||
Abgehakt bleibt: Hell/Dunkel ohne System-Modus; Billing-Konnektor/RLS.
|
||||
|
||||
Teilweise umgesetzt, aber noch nicht DoD-grün: Legacy-Provider-Abo-Baukasten-Grundschema; Trial/Dunning/Frozen mit Statusdaten, Webhook-Sync und erster Read-only-Blockade; read-only Subscription-Oberfläche; Auto-SHK-Provisionierung; Demo-Daten-Knopf; Erste-Schritte-Checkliste.
|
||||
|
||||
Selbst machbar offen: Frozen-Read-Only noch route-übergreifend härtbar, Verbrauchs-Drossel, Demo-ausblenden-Filter, Checklist-Auto-Erkennung und Dashboard-Widget.
|
||||
|
||||
Blockiert/offen durch externes Delta: finale Subframe-/Screen-Integration aus `D-P20`. `EXT-07` ist laut Plane erledigt; `AK-P19` ist formal auf On-Hold, die für AK-P20 genutzten Grundlagen liegen aber bereits auf `main`.
|
||||
|
||||
**2026-07-01 · AK-P20 Slice 2 Nachzug: Usage-Drossel + First-Steps-Auto-Erkennung**
|
||||
|
||||
Umgesetzt:
|
||||
- Verbrauchs-Drossel: `recordBillingUsage()` prueft Quotas vor dem Schreiben, wirft `BillingUsageQuotaExceededError` und behandelt `idempotencyKey` jetzt wirklich idempotent.
|
||||
- Usage-Summary liefert pro Metrik `quota`, `remaining`, `exceeded` und `percentUsed`; `/settings/usage` zeigt Limit, Restmenge und ausgeschöpfte Limits an.
|
||||
- Erste-Schritte-Checkliste erkennt Status automatisch aus echten Tenant-Daten: Profil, Kontakte, Projekte und aktives Trial/Abo. Erfüllte Schritte werden in `OnboardingSessionStep.completedAt` persistiert; vollständige Sessions werden auf `completed` gesetzt.
|
||||
- Dashboard-Widget für die Erste-Schritte-Checkliste ergänzt, inklusive Fortschritt und Link zur Checklist-Route.
|
||||
|
||||
Proof:
|
||||
- `pnpm --filter @hivegrid/web exec vitest run app/utils/services/.server/billingStatusService.test.ts app/utils/services/.server/billingDunningService.test.ts app/routes/webhooks.legacyProvider.test.ts app/utils/legacyProvider.server.test.ts 'app/routes/app.$tenant.test.tsx'`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm run verify:billing-boundary-baseline`
|
||||
|
||||
Hinweis Testabdeckung:
|
||||
- Der isolierte direkte Lauf von `app/routes/app.$tenant/dashboard.test.tsx` bleibt ein bestehender Mixed-Environment-Testfall: Loader-Imports sind server-only, Render-Assertions brauchen jsdom. Die Dashboard-Änderung ist via Typecheck abgedeckt; die fokussierten grünen Tests decken die Billing-/Webhook-Regressionen ab.
|
||||
|
||||
Fortschritt: **AK-P20: 3/8 DoD = 37,5 % done**.
|
||||
|
||||
Abgehakt: Hell/Dunkel ohne System-Modus; Billing-Konnektor/RLS; Erste-Schritte-Checkliste.
|
||||
|
||||
Teilweise umgesetzt, aber noch nicht DoD-grün: Legacy-Provider-Abo-Baukasten-Grundschema; Trial/Dunning/Frozen mit Webhook-Sync, Dunning-Notifications, Usage-Drossel und erster Read-only-Blockade; read-only Subscription-Oberfläche; Auto-SHK-Provisionierung; Demo-Daten-Knopf.
|
||||
|
||||
Selbst machbar offen: Frozen-Read-Only noch route-übergreifend härtbar und Demo-ausblenden-Filter.
|
||||
|
||||
**2026-07-01 · AK-P20 Slice 3 Nachzug: Demo-Daten in Listen standardmäßig ausblenden**
|
||||
|
||||
Umgesetzt:
|
||||
- Demo-Daten werden in Projektlisten, Projekt-Board, Aufgabenliste und Kontaktliste standardmäßig serverseitig ausgeblendet (`isDemoData=false`).
|
||||
- Explizites Anzeigen läuft einheitlich über `demo=show`; Projekte/Aufgaben bieten einen Toggle-Link, Kontakte nutzen den bestehenden Filter-Submit.
|
||||
- Kontaktlisten-Query serialisiert und parst den Demo-Filter stabil, inklusive Unit-Test.
|
||||
- i18n-Keys für „Demo-Daten anzeigen/ausblenden" in Deutsch und Englisch ergänzt.
|
||||
|
||||
Proof:
|
||||
- `pnpm --filter @hivegrid/web exec vitest run app/utils/services/.server/billingStatusService.test.ts app/utils/services/.server/billingDunningService.test.ts app/routes/webhooks.legacyProvider.test.ts app/utils/legacyProvider.server.test.ts 'app/routes/app.$tenant.test.tsx' app/utils/integrations/crm/kontaktListQuery.test.ts`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm run verify:billing-boundary-baseline`
|
||||
- `pnpm run verify:onboarding-flow`
|
||||
|
||||
Fortschritt: **AK-P20: 4/8 DoD = 50 % done**.
|
||||
|
||||
Abgehakt: Hell/Dunkel ohne System-Modus; Billing-Konnektor/RLS; Erste-Schritte-Checkliste; Demo-Daten-Knopf plus Demo-ausblenden-Filter.
|
||||
|
||||
Teilweise umgesetzt, aber noch nicht DoD-grün: Legacy-Provider-Abo-Baukasten-Grundschema; Trial/Dunning/Frozen mit Webhook-Sync, Dunning-Notifications, Usage-Drossel und erster Read-only-Blockade; read-only Subscription-Oberfläche; Auto-SHK-Provisionierung.
|
||||
|
||||
Selbst machbar offen: Frozen-Read-Only noch route-übergreifend härtbar. Der zentrale Guard existiert aktuell nicht als Request-/Action-Middleware; ein flächiger Route-Patch hätte hohen Blast-Radius und sollte als eigener, gezielter Nachzug umgesetzt werden.
|
||||
|
||||
Blockiert/offen durch externes Delta: finale Subframe-/Screen-Integration aus `D-P20`.
|
||||
|
||||
**2026-07-02 · AK-P20 Slice 5 Nachzug: Frozen-ReadOnly zentral gehärtet**
|
||||
|
||||
Umgesetzt:
|
||||
- Mutierende Tenant-Requests werden im zentralen Permission-Boundary `verifyUserHasPermission()` gegen den Billing-ReadOnly-State geprüft.
|
||||
- `past_due`, `frozen` und `cancelled` blockieren permission-gated Tenant-Schreibaktionen mit HTTP 423 und dem Klartext-Key `billing:status.readOnly`.
|
||||
- Billing-Recovery bleibt erlaubt: `/settings/subscription` ist als Ausnahme freigegeben, damit Customer-Portal/Zahlungs- und Kündigungsaktionen erreichbar bleiben.
|
||||
- Bestehende Tenant-Shell-Actions (Feedback/Support) bleiben zusätzlich durch den bisherigen ReadOnly-Check geschützt.
|
||||
|
||||
Proof:
|
||||
- `pnpm --filter @hivegrid/web exec vitest run 'app/routes/app.$tenant.test.tsx' app/utils/helpers/.server/PermissionsService.billing-readonly.test.ts`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm run verify:billing-boundary-baseline`
|
||||
|
||||
Fortschritt: **AK-P20: 6/8 DoD = 75 % done**.
|
||||
|
||||
Abgehakt: Hell/Dunkel ohne System-Modus; Billing-Konnektor/RLS; Erste-Schritte-Checkliste; Demo-Daten-Knopf plus Demo-ausblenden-Filter; Auto-SHK-Provisionierung; Trial/Dunning/Frozen/Usage-Drossel.
|
||||
|
||||
Teilweise umgesetzt, aber noch nicht DoD-grün: Legacy-Provider-Abo-Baukasten-Grundschema; read-only Subscription-Oberfläche ohne Pricing-/Upgrade-/Plan-Wechsel-UI.
|
||||
|
||||
Selbst machbar offen: AD-26-Cleanup von verbliebenen AI-Credits-/Credits-Entitlement-Resten prüfen und, falls scope-konform, bereinigen.
|
||||
|
||||
Blockiert/offen durch externes Delta: finale Subframe-/Screen-Integration aus `D-P20`.
|
||||
|
||||
**2026-07-02 · AK-P20 Slice 4 Nachzug: Auto-SHK-Provisionierung vervollständigt**
|
||||
|
||||
Umgesetzt:
|
||||
- `provisionShkTenantDefaults()` orchestriert jetzt alle vorhandenen SHK-Seed-Bausteine: Rollen, Default-Sales-Pipeline, Custom-Field-Template-Seed, Abnahmeformular-Template, Projekt-Template, Erste-Schritte-Checkliste.
|
||||
- Die bisher direkt in der Forms-Route liegenden SHK-Form-Templates wurden in `modules/forms/services/shkFormTemplates.server.ts` extrahiert und werden beim Signup automatisch mit lokalisierten Labels angelegt.
|
||||
- Der bestehende Forms-Admin-Button nutzt denselben Seed-Service weiter; kein zweiter Seed-Pfad.
|
||||
- Hinweis: Die Custom-Field-Template-Liste bleibt bewusst leer, weil der bestehende Code sie bis zum SHK-Domain-Workshop nicht mit erfundenen Feldern füllt. Der Provisioning-Hook ist verdrahtet.
|
||||
|
||||
Proof:
|
||||
- `pnpm --filter @hivegrid/web exec vitest run app/modules/forms/services/shkFormTemplates.server.test.ts app/utils/services/shkRoles.server.test.ts app/utils/services/authService.transactional-email.test.ts`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm run verify:onboarding-flow`
|
||||
|
||||
Fortschritt: **AK-P20: 5/8 DoD = 62,5 % done**.
|
||||
|
||||
Abgehakt: Hell/Dunkel ohne System-Modus; Billing-Konnektor/RLS; Erste-Schritte-Checkliste; Demo-Daten-Knopf plus Demo-ausblenden-Filter; Auto-SHK-Provisionierung.
|
||||
|
||||
Teilweise umgesetzt, aber noch nicht DoD-grün: Legacy-Provider-Abo-Baukasten-Grundschema; Trial/Dunning/Frozen mit Webhook-Sync, Dunning-Notifications, Usage-Drossel und erster Read-only-Blockade; read-only Subscription-Oberfläche.
|
||||
|
||||
Selbst machbar offen: Frozen-Read-Only noch route-übergreifend härtbar; AD-26-Cleanup von verbliebenen AI-Credits-/Credits-Entitlement-Resten.
|
||||
|
||||
Blockiert/offen durch externes Delta: finale Subframe-/Screen-Integration aus `D-P20`.
|
||||
|
||||
**2026-07-02 · AK-P20 Slice 6 Nachzug: In-App-Planwechsel entfernt**
|
||||
|
||||
Umgesetzt:
|
||||
- Die Legacy-Route `/subscribe/:tenant` rendert keine Pricing-/Planwechsel-UI mehr und startet keine Payment-Checkout-Session mehr; GET/POST fallen auf die read-only Subscription-Übersicht zurück.
|
||||
- Interne Subscription-required-Links zeigen direkt auf `/settings/subscription?subscription_required=1` statt auf die alte Checkout-Seite.
|
||||
- Header-/ProFeature-Einstiege verlinken nur noch die Subscription-Übersicht; Customer-Portal, Zahlungsmethoden und Kündigen bleiben in `/settings/subscription` erhalten.
|
||||
- Billing-Boundary-Test ergänzt: Legacy-Subscribe muss frei von Checkout-Writes, `PlansGrouped` und direkten Billing-Imports bleiben.
|
||||
|
||||
Proof:
|
||||
- `pnpm --filter @hivegrid/web exec vitest run 'app/routes/subscribe.$tenant.test.tsx' app/utils/app/SubscriptionUrlUtils.test.ts`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm exec vitest run packages/db/src/subscriptions.server.test.ts packages/db/src/subscriptions-boundary.test.ts apps/web/app/utils/services/.server/subscriptionService.test.ts`
|
||||
- `pnpm run verify:billing-boundary-baseline`
|
||||
|
||||
Fortschritt: **AK-P20: 7/8 DoD = 87,5 % done**.
|
||||
|
||||
Abgehakt: Hell/Dunkel ohne System-Modus; Billing-Konnektor/RLS; Erste-Schritte-Checkliste; Demo-Daten-Knopf plus Demo-ausblenden-Filter; Auto-SHK-Provisionierung; Trial/Dunning/Frozen/Usage-Drossel; read-only Subscription-Oberfläche ohne Pricing-/Upgrade-/Plan-Wechsel-UI.
|
||||
|
||||
Offen nach Recut: Der alte Legacy-Provider-Abo-Baukasten-Abschluss wird nicht weitergebaut. Der saubere Abschluss laeuft ueber
|
||||
`BILL-02` bis `BILL-05`: lokale Plan-/Seat-/Entitlement-Domain, Lago-Gateway, Mollie-Gateway und Billing-Sync.
|
||||
|
||||
Blockiert/offen durch externes Delta: finale Subframe-/Screen-Integration aus `D-P20`.
|
||||
|
||||
**2026-07-02 · AK-P20 Slice 7 Nachzug: Seat-Typen im Admin-Pricing blockiert**
|
||||
|
||||
Umgesetzt:
|
||||
- Die Standard-Plan-Seeds erzeugen keine `PER_SEAT`-Pläne mehr.
|
||||
- Die Plattform-Admin-Pricing-Auswahl nutzt eine zentrale HiveGrid-Pricing-Policy ohne `PER_SEAT`.
|
||||
- `pricing.new`, `pricing.edit` und `create-all-plans` lehnen manipulierte `PER_SEAT`-Formulare serverseitig mit `shared.invalidForm` ab.
|
||||
- Test ergänzt: Policy und File-Seeds bleiben frei von Seat-Type-Plänen.
|
||||
|
||||
Proof:
|
||||
- `pnpm --filter @hivegrid/web exec vitest run app/application/pricing/pricingModelPolicy.test.ts`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm lint`
|
||||
- `pnpm run verify:i18n`
|
||||
- `pnpm run verify:billing-boundary-baseline`
|
||||
|
||||
Fortschritt: **AK-P20: 7/8 DoD = 87,5 % done**.
|
||||
|
||||
Weiterhin offen nach Recut: Der alte Legacy-Provider-Abo-Baukasten-DoD ist durch `AD-27` ersetzt. Keine weitere
|
||||
Legacy-Provider-Admin-Positionen-Arbeit in AK-P20; neue Billing-Arbeit ueber `BILL-02` bis `BILL-05`.
|
||||
@@ -1,47 +1,51 @@
|
||||
# AK-P21 · P21 DSGVO (Export + Lösch-Workflow) + Feedback-Widget
|
||||
|
||||
> ⚠️ **Billing-Recut 2026-07-04 ([[AD-27]]):** Tenant-Löschung storniert kuenftig den Billing-State ueber die
|
||||
> Lago/Mollie/HiveGrid-Billing-Gateways aus `BILL-*`, nicht ueber Legacy-Provider. Historische Legacy-Provider-Cancel-Erwaehnungen sind
|
||||
> als alter Adaptervertrag zu lesen und duerfen nicht als neue Legacy-Provider-Dependency weitergebaut werden.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Typ** | Dev (Code) |
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P21 — DSGVO & Launch-Reife |
|
||||
| **Cycle/Fenster** | W4 — USP, Härtung & Launch · fällig 2026-07-09 · **Gate W4 (FREEZE)** |
|
||||
| **Status** | geplant |
|
||||
| **Status** | Blocked / On Hold, Teilumsetzung Backend/Fundament |
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-P20` (Billing — Tenant-Löschung ruft Stripe-Cancel; Feedback-Widget speist Admin-Support-Inbox aus AK-P19)
|
||||
- **Blocked by:** `AK-P20`/`BILL-05` (Billing-Read-only/Frozen-State und Billing-Cancel-Vertrag; Feedback-Widget speist Admin-Support-Inbox aus AK-P19), `D-P21` (finale Privacy-/FAQ-/Feedback-Screens)
|
||||
- **Blocks:** **FREEZE** (letzte v1-Phase, macht den Big-Bang-Launch AD-19 auslösbar)
|
||||
- **Läuft parallel zu:** Raffaels Härtung/Launch-Tickets. Eigene `prisma/schema/gdpr.prisma` (AD-21); Feedback-Widget schreibt in `AdminSupportTicket` (AK-P19, generische Quelle); Export liest tenant-weit über RLS.
|
||||
- **Konflikt-Risiko:** gering — additive Tabellen + asynchrone BullMQ-Jobs; Crypto-Shredding berührt per-Tenant-DEK (AD-8), keine Schema-Änderung an Fach-Modulen.
|
||||
|
||||
## Was wir bauen
|
||||
Den **DSGVO-Lebenszyklus + Launch-Reife** (letzte v1-Phase): tenant- und user-weiter **DSGVO-ZIP-Export** (asynchroner BullMQ-Job mit Progress, **72h-signed-URL**, Rate-Limit), den **Tenant-Lösch-Workflow** (**Crypto-Shredding** per-Tenant-DEK, **Stripe-Cancel**, Storage/Cache/Index-Bereinigung, 30-Tage-Grace mit Read-Only + Abbruch, **Slug als Confirm-Phrase**), die **AVV-/Subprozessoren-Sichtbarkeit** im Privacy-Hub, ein **In-App-Feedback-Widget** (speist die Admin-Support-Inbox aus AK-P19) und eine **Minimal-FAQ**-Seite. **DSGVO-Auskunft = manuelles Playbook v1** (Suche → Kontakt-Detail → CSV → Anwalts-Vorlage → Frist-Task +25 Tage), **kein** Auskunfts-Wizard. PWA-only (AD-17): alle Screens mobil tauglich, kein nativer Client.
|
||||
Den **DSGVO-Lebenszyklus + Launch-Reife** (letzte v1-Phase): tenant- und user-weiter **DSGVO-ZIP-Export** (asynchroner BullMQ-Job mit Progress, **72h-signed-URL**, Rate-Limit), den **Tenant-Lösch-Workflow** (**Crypto-Shredding** per-Tenant-DEK, **Billing-Cancel ueber Lago/Mollie/HiveGrid-Gateway**, Storage/Cache/Index-Bereinigung, 30-Tage-Grace mit Read-Only + Abbruch, **Slug als Confirm-Phrase**), die **AVV-/Subprozessoren-Sichtbarkeit** im Privacy-Hub, ein **In-App-Feedback-Widget** (speist die Admin-Support-Inbox aus AK-P19) und eine **Minimal-FAQ**-Seite. **DSGVO-Auskunft = manuelles Playbook v1** (Suche → Kontakt-Detail → CSV → Anwalts-Vorlage → Frist-Task +25 Tage), **kein** Auskunfts-Wizard. PWA-only (AD-17): alle Screens mobil tauglich, kein nativer Client.
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-P21`)
|
||||
| Screen [Route] | Kern-Inhalt |
|
||||
|---|---|
|
||||
| Privacy-Hub [`/admin/privacy`] | Übersicht Export/Löschung/Subprozessoren/AVV/Privacy-Manifest-Link, Einstieg in Sub-Bereiche |
|
||||
| Tenant-Daten-Export [`/admin/privacy/export`, `/new`, `/:jobId`] | Job-Liste mit Status/Progress/Download (72h-Link); Anstoßen mit Klartext (was, Größe/Dauer, 7-Tage-Window) |
|
||||
| Tenant-Löschung [`/admin/privacy/deletion`, `/:requestId`] | Multi-Step-Wizard: Erklärung (30-Tage-Grace, Read-Only, Hard-Delete) → Export-Angebot → Stripe-Hinweis (Pro-rata) → Passwort+2FA → **Slug als Confirm-Phrase**; Grace-Countdown + Abbruch |
|
||||
| Tenant-Löschung [`/admin/privacy/deletion`, `/:requestId`] | Multi-Step-Wizard: Erklärung (30-Tage-Grace, Read-Only, Hard-Delete) → Export-Angebot → Billing-/Zahlungs-Hinweis → Passwort+2FA → **Slug als Confirm-Phrase**; Grace-Countdown + Abbruch |
|
||||
| Subprozessoren [`/admin/privacy/subprocessors`] · AVV [`/admin/privacy/avv`] | Subprozessor-Tabelle (Land/EU-Marker/DPA-Link/Export); AVV-PDF-Download + Akzeptanz-History + Re-Akzeptanz-Modal |
|
||||
| User-Daten-Export [`/settings/account/data-export`, `/:jobId`] · Account-Löschung [`/settings/account/delete`] | User-Self-Export (signed-URL, Rate-Limit); Account-Lösch-Wizard (Passwort+2FA, 24h-Reverse-Window, M27 D.3) |
|
||||
| Feedback-Widget [global, klein] | Schwebender Button → Mini-Form (Bug/Feature/Frage, Text, Screenshot-Häkchen, Rate-Limit) → **Admin-Support-Inbox (AK-P19)** |
|
||||
| Minimal-FAQ [`/help`] | Statische FAQ (gruppierte Fragen/Antworten, Markdown), Suchfeld optional (kein Help-Center-Ausbau → v2) |
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **DSGVO-ZIP-Export** (User-Self **und** Tenant-Full): asynchroner BullMQ-Job mit Progress, **72h-signed-URL**, Rate-Limit (max. 3 User-Self/24h, max. 1 Tenant-Full aktiv); README + manifest.json im ZIP
|
||||
- [ ] **Tenant-Löschung**: **Crypto-Shredding** (per-Tenant-DEK) + **Stripe-Cancel** + Storage/Cache/Index-Bereinigung (AD-8); 30-Tage-Grace Read-Only + Abbruch; **Slug als Confirm-Phrase**; Stripe-Cancel erst beim Hard-Delete-Cron
|
||||
- [ ] **AVV-Download** + Akzeptanz-History + Re-Akzeptanz (14-Tage-Frist, danach Read-Only-Sperre); **Subprozessoren-Liste** mit Drittland-Klartext (v1 EU-only)
|
||||
- [ ] **DSGVO-Auskunft als dokumentiertes Playbook** (Suche → Kontakt-Detail → CSV → Anwalts-Vorlage → Frist-Task +25 Tage) — kein automatisches Tooling
|
||||
- [ ] **In-App-Feedback-Widget** → speist Admin-Support-Inbox (AK-P19, generische Quelle)
|
||||
- [ ] **Minimal-FAQ**-Seite (statisch)
|
||||
- [ ] Alle Screens **mobil tauglich** (PWA/responsive, AD-17); kein nativer Client; PWA-Installierbarkeit/Web-Push → v2
|
||||
- [ ] Permissions: `privacy.user_export.self` (implizit jeder User), `privacy.tenant_export`/`privacy.tenant_deletion`/`privacy.avv.*`/`privacy.subprocessors.read` (über `tenant.admin`)
|
||||
- [ ] **DSGVO-ZIP-Export** (User-Self **und** Tenant-Full): asynchroner BullMQ-Job mit Progress, **72h-signed-URL**, Rate-Limit (max. 3 User-Self/24h, max. 1 Tenant-Full aktiv); README + manifest.json im ZIP — *teilweise 2026-07-02: `GdprExportJob` + Service-Regeln fuer Rate-Limit, Status/Progress und 72h-Expiry gebaut; ZIP-Builder + Worker-Runner mit README/manifest und Handler-Branch fuer `gdpr_user_self`/`gdpr_tenant_full`; Self-Service-Export legt Job an und enqueue't; Worker-Bootstrap + konservative Daten-Bundle-Loader fuer User/Tenant/Audit/Notifications gebaut. Offen: signierte Download-Route/E-Mail und breiteres Fachmodul-Inventar.*
|
||||
- [ ] **Tenant-Löschung**: **Crypto-Shredding** (per-Tenant-DEK) + **Billing-Cancel ueber Lago/Mollie/HiveGrid-Gateway** + Storage/Cache/Index-Bereinigung (AD-8); 30-Tage-Grace Read-Only + Abbruch; **Slug als Confirm-Phrase**; Billing-Cancel erst beim Hard-Delete-Cron — *teilweise 2026-07-02: `TenantDeletionRequest` + Service-Regeln fuer Slug-Confirm, 30-Tage-Grace, Abbruch und Cron-Liste gebaut; getesteter Hard-Delete-Orchestrierungsvertrag fuer Billing-Cancel, Storage, Search, Crypto-Shredding und Tenant-Row-Delete. Offen: produktive Bootstrap-Anbindung/Cron, AK-P20-/BILL-05-Read-Only-Hook, echte Adapter-Implementierungen.*
|
||||
- [ ] **AVV-Download** + Akzeptanz-History + Re-Akzeptanz (14-Tage-Frist, danach Read-Only-Sperre); **Subprozessoren-Liste** mit Drittland-Klartext (v1 EU-only) — *teilweise 2026-07-02: `AvvAcceptance`/`Subprocessor` + Service-Lese-/Akzeptanzpfad gebaut; PDF-Download/Re-Akzeptanz-Read-Only/finale Screens offen.*
|
||||
- [x] **DSGVO-Auskunft als dokumentiertes Playbook** (Suche → Kontakt-Detail → CSV → Anwalts-Vorlage → Frist-Task +25 Tage) — kein automatisches Tooling
|
||||
- [x] **In-App-Feedback-Widget** → speist Admin-Support-Inbox (AK-P19, generische Quelle)
|
||||
- [x] **Minimal-FAQ**-Seite (statisch)
|
||||
- [ ] Alle Screens **mobil tauglich** (PWA/responsive, AD-17); kein nativer Client; PWA-Installierbarkeit/Web-Push → v2 — *blockiert: finale Screen-Integration wartet auf `D-P21`; statische FAQ ist responsive.*
|
||||
- [x] Permissions: `privacy.user_export.self` (implizit jeder User), `privacy.tenant_export`/`privacy.tenant_deletion`/`privacy.avv.*`/`privacy.subprocessors.read` (über `tenant.admin`)
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/gdpr.prisma`): `GdprExportJob` (scope user_self/tenant_full, status, result_file_id ZIP, result_expires_at), `TenantDeletionRequest` (reason, grace_period_ends_at +30d, status — Slug-Confirm), `AvvAcceptance` (avv_version, accepted_by, pdf_file_id, signer_name), `Subprocessor` (name, country, is_eu_eea, data_categories, dpa_url — global). Feedback schreibt in `AdminSupportTicket` (AK-P19). `core.prisma` über Tenant-Relation (AD-21).
|
||||
- **Export-Worker** (BullMQ): tenant-weiter/user-weiter Sammel-Export → ZIP (S3-kompatibel, Hetzner) mit 72h-signed-URL + README/manifest.json; Rate-Limit.
|
||||
- **Crypto-Shredding** (AD-8): per-Tenant-DEK löschen → Daten kryptographisch unlesbar; Stripe-Cancel + Storage/Cache/Index-Cleanup beim Hard-Delete-Cron; 30-Tage-Grace Read-Only-Middleware (Reuse aus AK-P20 Frozen-Modus).
|
||||
- **Crypto-Shredding** (AD-8): per-Tenant-DEK löschen → Daten kryptographisch unlesbar; Billing-Cancel ueber Lago/Mollie/HiveGrid-Gateway + Storage/Cache/Index-Cleanup beim Hard-Delete-Cron; 30-Tage-Grace Read-Only-Middleware (Reuse aus AK-P20/BILL-05 Frozen-Modus).
|
||||
- **Feedback-Widget**: Rate-Limit + Screenshot via Files → `AdminSupportTicket` (source_kind in_app_feedback_widget).
|
||||
- **RLS** auf allen Tenant-Tabellen (Subprocessor global); `is_demo_data` n/a (kein Seed-Bedarf).
|
||||
|
||||
@@ -56,3 +60,53 @@ Den **DSGVO-Lebenszyklus + Launch-Reife** (letzte v1-Phase): tenant- und user-we
|
||||
- [ ] Spec/ENTITY-MODEL an Davids UI-Entscheidungen angepasst
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P21` aktualisiert
|
||||
|
||||
## Umsetzungsnotiz
|
||||
|
||||
### 2026-07-02 — Teilumsetzung Backend/Fundament
|
||||
|
||||
Proof:
|
||||
- Prisma/Isolation: `prisma/schema/gdpr.prisma`, `prisma/rls/gdpr-isolation.sql`, Tenant-Snapshot-Registry und generierter Prisma-Client.
|
||||
- Services: `@hivegrid/db/privacy-gdpr.server` fuer Export-Limits, 72h-Expiry, Tenant-Deletion-Grace/Cancel, AVV-Akzeptanz und Subprozessoren; `createWidgetSupportTicket` fuer Widget → Support-Inbox.
|
||||
- UI/Doku: statische `/help`-FAQ, `docs/compliance/dsgvo-auskunft-playbook.md`, GDPR-Doku-Verweis.
|
||||
- Permissions: `privacy.user_export.self`, `privacy.tenant_export`, `privacy.tenant_deletion`, `privacy.avv.read`, `privacy.avv.accept`, `privacy.subprocessors.read` in Seed, DefaultPermissions, Rollenanzeige und i18n.
|
||||
|
||||
Verifikation:
|
||||
- `pnpm typecheck` gruen
|
||||
- `pnpm lint` gruen
|
||||
- `pnpm exec prisma validate` gruen
|
||||
- `pnpm run verify:tenant-isolation-report` gruen
|
||||
- `pnpm run verify:security-gdpr-docs` gruen
|
||||
- Fokus-Tests: 55/55 gruen (`privacy-gdpr`, `support-tickets`, `Feedback.server`, `roleDisplay`, `permissions-seed`, Tenant-Models, routes/i18n/disabled-plugin-links)
|
||||
- Permission-Matrix-Einzeltests: 22/22 gruen; Wrapper `pnpm run verify:permission-matrix` scheitert auf Windows mit `spawn pnpm ENOENT`.
|
||||
- Breites `pnpm test` timed auf Windows nach 302s ohne sichtbare Assertion-Failures aus; Ausgabe nur bekannte Stderr-Faelle (Session-Cookie-Warnung, Admin-SSO db-down Mock, Notification Redis Mock, i18next-Warnung).
|
||||
|
||||
DoD-Stand: **4/8 = 50 % done**. Offen: ZIP-Worker/README/manifest/Download, Hard-Delete-Cron inkl. Crypto-Shredding/Billing-Cancel/Storage/Cache/Search, AVV-PDF/Re-Akzeptanz-Read-Only/Subprozessor-Screen, finale mobile Screens aus `D-P21`.
|
||||
|
||||
### 2026-07-02 — Nachzug selbst machbarer Worker-/Queue-Aspekte
|
||||
|
||||
Proof:
|
||||
- Export: `packages/exports/src/zip.ts` erzeugt ZIPs mit README/manifest/data-Dateien; `apps/worker/src/jobs/privacy-gdpr-export.ts` baut GDPR-Export-Artefakte; `apps/worker/src/handlers.ts` routet `gdpr_user_self`/`gdpr_tenant_full` in den GDPR-Statuspfad.
|
||||
- Web-Queue: `apps/web/app/utils/privacy/gdprExportQueue.server.ts`; `/app/:tenant/settings/privacy` legt bei Self-Service-Export jetzt `GdprExportJob` an und enqueue't den Export.
|
||||
- Tenant-Löschung: `apps/worker/src/jobs/privacy-tenant-deletion.ts` definiert und testet die Hard-Delete-Reihenfolge Billing-Cancel → Storage-Prefix → Search-Dokumente → Crypto-Shredding → Tenant-Rows.
|
||||
- Nachzug: `apps/worker/src/jobs/privacy-gdpr-export-bootstrap.ts` bootstrapped den GDPR-Export-Service produktiv mit S3-`PutObjectCommand` und konservativen User-Self/Tenant-Full-Bundle-Loadern (User/Tenant/TenantUser/AuditEvent/NotificationPreference/NotificationDelivery).
|
||||
|
||||
Verifikation:
|
||||
- `pnpm typecheck` gruen
|
||||
- `pnpm --filter @hivegrid/exports typecheck` gruen
|
||||
- `pnpm --filter @hivegrid/worker typecheck` gruen
|
||||
- `pnpm lint` gruen
|
||||
- `pnpm exec prisma validate` gruen
|
||||
- `pnpm run verify:tenant-isolation-report` gruen
|
||||
- `pnpm run verify:security-gdpr-docs` gruen
|
||||
- Fokus-Tests: 141/141 gruen (AK-P21 DB/Web/Worker/Queue/ZIP/Feedback/Permissions/i18n)
|
||||
- Nachzug-Fokus: 97/97 gruen (`privacy-gdpr`, GDPR-Export-Bootstrap/-Runner, Tenant-Deletion-Runner, Handler, Web-Queue)
|
||||
- Permission-Matrix-Einzeltests: 22/22 gruen; Wrapper bleibt auf Windows mit `spawn pnpm ENOENT` ein bekanntes lokales Skriptproblem.
|
||||
- `git diff --check -- . ':(exclude)packages/db/generated/prisma/client/**'` gruen.
|
||||
|
||||
DoD-Stand: **4/8 = 50 % done**. Selbst machbarer Backend-/Worker-Nachzug ist erledigt; offen bleiben signed Download/E-Mail, breiteres Fachmodul-Inventar fuer Tenant-Full, produktive Tenant-Deletion-Bootstrap-Anbindung/Read-Only-Hook, AVV-PDF/Re-Akzeptanz-Read-Only/Subprozessor-Screen und finale mobile Screens aus `D-P21`.
|
||||
|
||||
Repo/Plane:
|
||||
- Branch `ak-p21-gdpr-launch-foundation` gepusht.
|
||||
- Implementierungs-Commit `4a02238a6` (`feat: add AK-P21 GDPR launch foundation`).
|
||||
- Plane-Kommentar `4a56b8c9-6254-4987-bd3d-2b78945b4786` am 2026-07-02 gesetzt; Ticket bleibt bewusst `Blocked / On Hold`, nicht `Done`.
|
||||
@@ -6,7 +6,9 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P3 — Querschnitts-Oberflächen |
|
||||
| **Cycle/Fenster** | W2 — Plattform & CRM (Demo 1) · fällig 2026-06-21 · Gate W2 (Noah-Demo #1) |
|
||||
| **Status** | geplant |
|
||||
| **Status** | umgesetzt (2026-07-07) — Dev-DoD 7/7 = 100 %, wartet auf Owner-Freigabe für Done |
|
||||
|
||||
**🎨 design-nachzug: offen** (seit 2026-07-07) — Interim-UI aus `~/ui` steht; finale Subframe-Optik aus `D-P3` ausstehend.
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `R-P1b` (Service-Kerne: Audit-Hash-Kette, `notify()`, i18n-Engine), `R-P2b` (RLS + Permissions für `audit.read`)
|
||||
@@ -18,23 +20,30 @@
|
||||
Die **Bedien-Oberflächen** der in P1 gebauten Plattformdienste: **Audit-Viewer** (gefilterter, permission-gegateter Trail mit deutschem Summary-Text, Actor-Kind-Marker, Diff-Detail-Drawer, NDJSON/CSV-Export ins Hetzner-Object-Storage mit Zweck-Pflichtfeld), **Notification-Center** (In-App via SSE, Read-Status, Deeplink, Tabs Alle/Ungelesen, Bulk-Clear) + **Notification-Settings** (Event-Typen je Modul, In-App/Email-Toggle, Master „alle Emails aus"), sowie die **Sprach-Settings** (v1 DE-only-Hinweis; alle Strings durch `t()`, `verify:i18n` grün).
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-P3` aus `D-D0a`/`D-D0b`)
|
||||
|
||||
> **Routen-Notation (Stand 2026-07-07, host-aware):** Die Route-Convention mountet `routes/admin/*` zusätzlich
|
||||
> als **Admin-Host-Alias ohne `/admin`-Präfix** und `app/:tenant/*` auf dem App-Host **ohne `/app`-Präfix**.
|
||||
> Kanonisch erreichbar sind die Flächen also unter `admin.<domain>/audit` bzw. `<app>/<tenant>/notifications`;
|
||||
> die pfadbasierten Formen (`/admin/audit`, `/app/:tenant/…`) existieren weiterhin. Angegeben ist unten die
|
||||
> kanonische URL + das Route-Modul.
|
||||
|
||||
| Screen [Route] | Kern-Inhalt | Liefert (Design) |
|
||||
|---|---|---|
|
||||
| Audit-Viewer [`/admin/audit`] | Zeit/Actor (+Kind-Marker)/Aktion/Subjekt/DE-Summary · Filter (Zeitraum/Actor/Kategorie/Subjekt-Typ) · Diff-Detail-Drawer · Export-Button | `D-P3` (A2+Drawer) |
|
||||
| Audit-Export-Jobs [`/admin/audit/exports` +`/new` +`/:id`] | Job-Liste · Anlage (Filter, Format, Zweck) · Status + signed-URL-Download | `D-P3` (A2) |
|
||||
| Notification-Center [Topbar-Drawer + `/notifications`] | Titel/Body/Actor/Zeit/Read-Status, Tabs Alle/Ungelesen, Bulk-Clear, Bounce-Klartext | `D-P3` (A2) |
|
||||
| Notification-Settings [`/settings/notifications`] | Event-Typen je Modul, In-App/Email-Toggle + Priority, Master „alle Emails aus" | `D-P3` (A6) |
|
||||
| Audit-Viewer [Admin-Host `/audit` · Modul `routes/admin/audit.tsx`] | Zeit/Actor (+Kind-Marker)/Aktion/Subjekt/DE-Summary · Filter (Zeitraum/Actor/Kategorie/Subjekt-Typ) · Diff-Detail-Drawer · Export-Button | `D-P3` (A2+Drawer) |
|
||||
| Audit-Export [Admin-Host `/audit-export` · Modul `routes/admin/audit-export.tsx`] — *v1 synchron statt Job-Liste (`/admin/audit/exports*` = v2, s. Vorab-Entscheidungen)* | Anlage (Filter, Format, Zweck-Pflichtfeld) · signed-URL-Download · CSV/PDF-Fallback | `D-P3` (A2) |
|
||||
| Notification-Center [Topbar-Drawer + App-Host `/:tenant/notifications` · Modul `routes/app.$tenant/notifications.tsx`] | Titel/Body/Actor/Zeit/Read-Status, Tabs Alle/Ungelesen, Bulk-Clear, Bounce-Klartext | `D-P3` (A2) |
|
||||
| Notification-Settings [App-Host `/:tenant/settings/notifications` · Modul `routes/app.$tenant/settings/notifications.tsx`] | Event-Typen je Modul, In-App/Email-Toggle + Priority, Master „alle Emails aus" | `D-P3` (A6) |
|
||||
|
||||
*Eingebettet (kein eigener Screen): Per-Subject-Audit-Drawer in jeder Detail-View, Per-User-Audit-Drawer in `/admin/users/:id`, DE-Sprach-Hinweis im Profil (P2). Davids P3-Screens werden über `D-P3` (Compose aus D0) geliefert.*
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **Audit-Viewer** permission-gegated (`audit.read`), server-seitige Filter, DE-Summary-Text, Actor-Kind-Marker
|
||||
- [ ] **Diff-Detail-Drawer** (Feld-für-Feld Vorher/Nachher; Maskierung bei sensiblen Werten)
|
||||
- [ ] **NDJSON/CSV-Export** ins Hetzner-Object-Storage + Zweck-Pflichtfeld + Export-Job-Status/Download; jeder Export selbst auditiert (`audit.exported`); fixe Default-Retention + Cron-Auto-Löschung
|
||||
- [ ] **Notification-Center** (SSE, Read-Status, Deeplink, Tabs, Bulk-Clear, Bounce-Klartext)
|
||||
- [ ] **Notification-Settings** (Event-Typen je Modul, In-App/Email-Toggle, Master-Toggle)
|
||||
- [ ] **Sprach-Settings DE** — alle Strings durch `t()`, vollständige `de`-Locale, `verify:i18n` grün; `Contact.language`-Feld vorhanden (Outbound ab v2)
|
||||
- [ ] *(→ v2)* Quiet-Hours/Bündelung/Push-Kanal/Retention-Editor/Legal-Hold/Saved-Views/Sprach-Wechsler ausgeschlossen
|
||||
- [x] **Audit-Viewer** permission-gegated (`audit.read`), server-seitige Filter, DE-Summary-Text, Actor-Kind-Marker — *AP-1* (+ Interim-Screen *AP-6*)
|
||||
- [x] **Diff-Detail-Drawer** (Feld-für-Feld Vorher/Nachher; Maskierung bei sensiblen Werten) — *AP-6*: `beforeDiff`/`afterDiff`/`summaryText` additiv an `AuditEvent` (Owner-Freigabe 2026-07-06), echter `~/ui`-Drawer mit Feld-für-Feld-Diff, maskierte Felder als „Wert geändert"; abgeleitete Summary bleibt Fallback für Alt-Zeilen. *Abgrenzung:* flächige Diff-Befüllung (Auto-Audit-Middleware) + Hash-Chain-Bindung der Diffs bleiben R-P1b/Raffael (Vermerk in `R-P1b.md`).
|
||||
- [x] **NDJSON/CSV-Export** ins Hetzner-Object-Storage + Zweck-Pflichtfeld + Export-Job-Status/Download; jeder Export selbst auditiert (`audit.exported`); fixe Default-Retention + Cron-Auto-Löschung — *AP-2* (+ Interim-Screen *AP-6*)
|
||||
- [x] **Notification-Center** (SSE, Read-Status, Deeplink, Tabs, Bulk-Clear, Bounce-Klartext) — *AP-3* + *AP-6*: Topbar-Drawer (echter `~/ui`-Drawer), Live-Unseen-Count via SSE, Deeplink-Kontrakt `payloadMetadata.deeplink` (validiert, app-relativ) als Interim-UI (AD-28) — nicht mehr David-blockiert.
|
||||
- [x] **Notification-Settings** (Event-Typen je Modul, In-App/Email-Toggle, Master-Toggle) — *AP-4* (+ Interim-Screen *AP-6*)
|
||||
- [x] **Sprach-Settings DE** — alle Strings durch `t()`, vollständige `de`-Locale, `verify:i18n` grün (*AP-5*); `Kontakt.language`-Feld ergänzt (*AP-6*, entsperrt durch CRM-P6-Merge; nullable, kein v1-Editor, Vermerk in `R-P6.md`)
|
||||
- [x] *(→ v2)* Quiet-Hours/Bündelung/Push-Kanal/Retention-Editor/Legal-Hold/Saved-Views/Sprach-Wechsler ausgeschlossen
|
||||
|
||||
## Technische Bausteine
|
||||
- **Keine neuen Kern-Entities** — liest/bedient die in `R-P1b` angelegten `AuditEntry`/`Notification`/`NotificationPreference`. Neu nur `AuditExportJob` ist **v2** → in v1 fixe Default-Retention statt Job-Editor (Boilerplate liefert Retention-Cron + PDF-Export). Eigene `prisma/schema/*.prisma` falls Felder ergänzt (AD-21).
|
||||
@@ -51,6 +60,138 @@ Die **Bedien-Oberflächen** der in P1 gebauten Plattformdienste: **Audit-Viewer*
|
||||
- `_bmad-output/specs/M21-audit-log.md`, `M22-notifications.md`, `M30-i18n.md` (Oberflächen-Teile)
|
||||
|
||||
## Dev-Nachzug (nach Davids `D-P3`-Delta)
|
||||
- [ ] Audit-/Notif-Oberflächen an Davids UI angepasst
|
||||
|
||||
**Interim-UI (Vorschau, Stand 2026-07-07):** Alle vier Flächen aus `~/ui` gebaut (AD-28) —
|
||||
`apps/web/app/components/screens/audit/{AuditListPage,AuditExportPage}.tsx`,
|
||||
`apps/web/app/components/screens/notifications/{NotificationCenterPage,NotificationSettingsPage,NotificationsDrawer}.tsx`.
|
||||
Proof: Smoke `tests/e2e/{audit,notifications}.spec.ts`, Hot-Swap-Adapter `auditList`/`notificationCenter`
|
||||
(`packages/ui-app/src/adapters/*.hot-swap.test.ts`) grün; Visual-Baseline deferred wie R-P2a/AK-P4b
|
||||
(keine Snapshot-Harness — Interim-Baseline wird beim David-Nachzug gezogen).
|
||||
Refactor-Nachzug offen: finale Subframe-Optik aus `D-P3` (Plane-Label `design-nachzug`).
|
||||
|
||||
- [x] Interim-UI aus `~/ui` gebaut (AD-28, 2026-07-07) — VM als Prop, kein handgeschriebenes Tailwind für Spacing/Höhen
|
||||
- [ ] Audit-/Notif-Oberflächen an Davids finaler `D-P3`-Optik angepasst (Subframe-Swap hinter der stabilen VM/Loader-Grenze; `design-nachzug: offen`). Nicht Done-blockierend (AD-28). Beim Swap: Interim-Visual-Baseline neu ziehen.
|
||||
- [ ] Admin-Topbar-Drawer: der SSE-Stream ist tenant-scoped (`app.$tenant/notifications/stream`), die Admin-Topbar behält den Link — Drawer für Admin erst mit einem admin-scoped Stream (Nachzug, nicht DoD-relevant).
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P3` aktualisiert
|
||||
- [x] Plane-Ticket `AK-P3` aktualisiert (2026-07-07: Abschluss-Kommentar + Label `design-nachzug` gesetzt; Branch gepusht. Status-Wechsel macht der Owner.)
|
||||
|
||||
## Vorab-Entscheidungen (Owner-bestätigt 2026-06-24, `/planning` = SSoT)
|
||||
- **Audit-Export v1 = synchron** (Zweck-Pflichtfeld + Object-Storage + signed-URL + `audit.exported` + Retention-Cron). **`AuditExportJob`-Entity + `/admin/audit/exports`-Job-Liste = v2** (Technische-Bausteine schlagen die Screen-Tabelle).
|
||||
- **Permission-Key:** bestehende `admin.auditTrails.view` (Admin) / `app.settings.auditTrails.view` (Tenant) wiederverwenden. DoD-Wortlaut `audit.read` ist damit der **Alias** auf `admin.auditTrails.view` (kein Key-Duplikat).
|
||||
|
||||
## Umsetzungsstand (Claude/Akeshan) — Branch `feat/AK-P3`
|
||||
|
||||
### AP-1 · Audit-Viewer `/admin/audit` — 2026-06-24
|
||||
**Gebaut (Commit `2bee1ecaa`):**
|
||||
- `packages/db/src/audit-events-query.server.ts` — `listAuditEvents` (paginiert, server-seitige Filter, neueste zuerst, nutzt vorhandene Indizes) + Subpath-Export.
|
||||
- `packages/view-models/src/audit/audit-list.vm.ts` — reiner Builder mit **Actor-Kind-Marker** (`deriveAuditActorKind`) + **abgeleiteter DE-Summary** (`deriveAuditSummaryKey` → i18n-Key + Params). VM-Unit-Test 5/5.
|
||||
- `apps/web/app/routes/admin/audit.tsx` — Route auf **`AuditEvent`** (nicht Legacy `Log`), gegated `admin.auditTrails.view`; Filter Zeitraum/Actor/Aktion/Subjekt-Typ/Ergebnis; Detail-Disclosure rendert die (write-time sanitisierte) `metadata` mit Maskierung; Pagination.
|
||||
- Reachability: AdminSidebar-Eintrag + UI-Registry-ID `adminAudit`, Permission-gegated; i18n-Keys `audit.*` in `settings.json` (de+en) + `app.sidebar.audit` (translations.json de+en).
|
||||
|
||||
**DoD-Stand nach AP-1:**
|
||||
- DoD 1 (Viewer permission-gegated, server-Filter, DE-Summary, Actor-Kind-Marker): **done**.
|
||||
- DoD 2 (Diff-Detail-Drawer): **teilweise** — Metadata-Disclosure + Maskierung erledigt; **strukturiertes Feld-für-Feld Vorher/Nachher blockiert** (`AuditEvent` ohne `before/after_diff`-Spalten → Coordination-Item mit R-P1b/Raffael, AD-21 core.prisma; siehe Plan §5.2). Persistente `summary_text` ebenso (LOCKED ENTITY-MODEL vs. reale Tabelle).
|
||||
- DoD 3-6: offen (AP-2..AP-5). DoD 7 (v2-Ausschlüsse): erfüllt (nicht gebaut).
|
||||
- **%-Done: ~1,5 / 7 DoD ≈ 21 %** · selbst machbar offen: AP-2..AP-5 + Querschnitt/Tests · blockiert: strukturierter Diff/persist. Summary (R-P1b), Davids `D-P3`-Screens, `Kontakt.language` (P6+).
|
||||
|
||||
**Proof:** view-models + db + web Typecheck grün · `verify:i18n` Exit 0 · VM-Test 5/5 · pre-commit-Hooks (eslint-baseline/lint-staged/secrets) grün · Node 22.15 (Typecheck/i18n unkritisch).
|
||||
|
||||
### AP-2 · Audit-Export ins Object-Storage — 2026-06-24
|
||||
**Gebaut (Commit `8b65a9135`):**
|
||||
- `apps/web/app/utils/audit/auditExportStorage.server.ts` — `runAuditExportToStorage`: baut CSV/NDJSON, lädt via `putBufferAsFile` nach `audit-exports/<scope>/...`, gibt kurzlebige **signed-URL** zurück, schreibt **`audit.exported`** mit **Zweck (Pflicht)**. Typisierte Fehler (kein Zweck / kein Storage).
|
||||
- `audit-export.helpers.server.ts` — `renderAuditExportNdjson` (+ Test 2/2).
|
||||
- `apps/web/app/routes/admin/audit-export.tsx` — Zweck-Pflichtfeld + Format-Select (CSV/NDJSON) + POST-Action mit Ergebnis-/Fehler-Panel; bestehender Direkt-CSV/PDF-Stream bleibt als Fallback. i18n `auditExport.storage.*` (de+en).
|
||||
|
||||
**DoD-Stand nach AP-2:**
|
||||
- DoD 3 (Export ins Object-Storage + Zweck + `audit.exported` + NDJSON/CSV + Retention-Cron): **done**.
|
||||
- **Retention-Cron nachgezogen (Commit `5b463c2dd`):** `registerAuditRetentionCron` + Konstanten in `audit-retention.ts` (+2 Tests), Scheduler+Worker im Worker-`general`-Group (`apps/worker/src/index.ts`), nightly 03:30 UTC. Compliance-sicher: pro Tenant werden die regulatorischen Scopes (soc2/hipaa/gdpr) aufgelöst → Policy kürzt nie unter die geforderte Aufbewahrung (SOC2/HIPAA 7 J, GDPR 6 J, sonst 365 T). Lösch-Lauf selbst auditiert (`audit.retention.pruned`). Runtime-Verifikation auf Staging (Redis+DB) wie bei `plugin-audit-retention`; lokal via Typecheck + Unit-Tests grün.
|
||||
- **Hinweis (kein App-Code möglich):** Auto-Löschung der Export-**Files** im Object-Storage = Bucket-Lifecycle-Regel auf `audit-exports/` (Infra), da v1 keine `AuditExportJob`-Tracking-Tabelle hat (= v2) und der Storage-Adapter keine List-Operation bietet. Signed-URL-TTL = fixe Default-Retention.
|
||||
|
||||
**Proof:** web Typecheck 0 Fehler · `verify:i18n` Exit 0 · NDJSON-Test 2/2 · pre-commit-Hooks grün.
|
||||
|
||||
### AP-3 · Notification-Center-Logik — 2026-06-25
|
||||
**Gebaut (Commit `5422c26d5`):**
|
||||
- `packages/db/src/notifications.server.ts` — `markAllInAppNotificationDeliveriesRead` (Bulk-Read, tenant/user-scoped, toleriert 0 Treffer, gibt Count zurück) + erweiterte `updateMany`-Typ-Union. 3 neue Unit-Tests (gesamt 29/29 grün).
|
||||
- `apps/web/app/routes/app.$tenant/notifications.tsx` — neue Route-Action `mark-all-read` (CSRF-validiert, ruft Bulk-Read).
|
||||
- `apps/web/app/components/notifications/NotificationCenter.tsx` — **Tabs Alle/Ungelesen** (Client-Filter auf `status`, mit Count je Tab), **Bulk-Clear**-Button (nur sichtbar bei ungelesenen), **Bounce-Klartext** via `deriveBounceMessageKey` (klassifiziert Provider-Fehler in stabile M002-i18n-Keys; roher redaktierter Text nur noch als sekundäres Detail).
|
||||
- i18n `notifications.center.tabs/bulkClear/unreadEmpty/bounce.*` (de+en vollständig).
|
||||
|
||||
**DoD-Stand nach AP-3:**
|
||||
- DoD 4 (Notification-Center): **selbst machbar erledigt** — SSE + Read-Status (Bestand), **Tabs + Bulk-Clear + Bounce-Klartext (neu)**. **Blockiert (Dev-Nachzug nach Davids `D-P3`):** Topbar-Drawer, Live-Unseen-Count, Deeplink je Notification — diese 3 sind UI/Pixel-abhängig und warten auf `D-P3`.
|
||||
|
||||
**Proof:** db Typecheck 0 Fehler · web Typecheck Exit 0 · `verify:i18n` Exit 0 · notifications-db-Test 29/29 · pre-commit-Hooks (test-density/eslint-baseline/lint-staged/secrets) grün · Node 22.15.
|
||||
|
||||
### AP-4 · Notification-Settings — 2026-06-25
|
||||
**Gebaut (Commit `6abda7919`):**
|
||||
- `apps/web/app/routes/app.$tenant/settings/notifications.tsx` — die 3 hartkodierten Kategorien durch eine **Event-Typen-je-Modul-Registry** (`NOTIFICATION_CATEGORY_REGISTRY`: general/billing/team) ersetzt; Loader/UI leiten Kategorien + Modul-Gruppierung daraus ab (Default-Fallback wie Worker `isNotificationEnabled` bleibt erhalten).
|
||||
- **Master „alle Emails aus"** (`set-email-master`-Action): schreibt `EMAIL=off` für **jede** registrierte Kategorie, damit explizite Per-Kategorie-Overrides keine Mails durchlassen. UI-Checkbox spiegelt den Zustand (an, wenn alle Kategorien EMAIL=false).
|
||||
- i18n `settings.notifications.modules.*` + `settings.notifications.master.*` (de+en).
|
||||
- 3 neue Route-Tests (Master an/aus + Render mit Master-Switch + 3 In-App/3 Email).
|
||||
|
||||
**DoD-Stand nach AP-4:**
|
||||
- DoD 5 (Notification-Settings: Event-Typen je Modul, In-App/Email-Toggle, Master-Toggle): **done**.
|
||||
- **Test-Hinweis (environmental, keine Regression):** die Action-Tests (Master + Pre-Existing `update-preference`) brechen lokal auf **Node 22.15** im `postFormRequest`-Helper (undici `new Request` mit `URLSearchParams`-Body, Fehler VOR dem Action-Code, Zeile 85) — gleicher dokumentierter Umgebungsbug wie in der Memory; Loader-/Render-Tests (4/10) grün. CI/Node 22.12 unkritisch. Web-Typecheck + `verify:i18n` grün.
|
||||
|
||||
### AP-5 · Sprach-Settings DE — 2026-06-25
|
||||
**Gebaut (Commit `893a88918`):**
|
||||
- `apps/web/app/components/settings/LanguageSettingsNotice.tsx` — statischer, read-only **DE-only-Hinweis** (v1 nur Deutsch; Sprachwechsler + weitere Locales = v2 laut DoD 7). Alle Strings via `t()`.
|
||||
- eingebunden in die erreichbare Profil-/Settings-Seite (`app.$tenant/settings/profile.tsx`).
|
||||
- i18n `settings.language.*` (de+en vollständig).
|
||||
- Render-Test 2/2 (kein roher i18n-Key leakt ins UI).
|
||||
|
||||
**DoD-Stand nach AP-5:**
|
||||
- DoD 6 (Sprach-Settings DE: alle Strings via `t()`, vollständige de-Locale, `verify:i18n` grün): **selbst machbar done**. **Blockiert:** `Kontakt.language`-Feld nicht baubar, da es vor CRM P6+ **kein `Contact`/`Kontakt`-Model** im Schema gibt (Plan §5.1) — nicht erfunden.
|
||||
|
||||
**Proof:** web Typecheck Exit 0 · `verify:i18n` Exit 0 · LanguageSettingsNotice-Test 2/2 · pre-commit-Hooks grün.
|
||||
|
||||
### Gesamt-DoD-Stand (nach AP-1..AP-5) — alle selbst machbaren Punkte done
|
||||
- DoD 1 **done** · DoD 2 **teilweise** (abgeleitete Variante done; strukturierter Diff + persist. `summary_text` blockiert §5.2 R-P1b/core.prisma) · DoD 3 **done** · DoD 4 **selbst machbar done** (Tabs/Bulk-Clear/Bounce; Topbar-Drawer/Live-Count/Deeplink blockiert via D-P3) · DoD 5 **done** · DoD 6 **selbst machbar done** (`Kontakt.language` blockiert P6+) · DoD 7 erfüllt.
|
||||
- **%-Done: ~6 / 7 DoD ≈ 86 %** · **selbst machbar offen: nichts** · extern blockiert: (a) strukturierter Diff/persist. Summary → R-P1b/Raffael (`core.prisma`, dependency-aware, nicht unilateral); (b) D-P3-Pixel-UI/Topbar-Drawer/Live-Count/Deeplink → David; (c) `Kontakt.language` → CRM P6+.
|
||||
- **Ticket bleibt offen (nicht Done):** 100 % erst, wenn die extern blockierten Punkte aufgelöst sind. Freigabe/Status-Wechsel durch Owner.
|
||||
- **Test-Umgebung:** Action-Tests (Notif-Settings) brechen lokal nur am bekannten Node-22.15-undici-`Request`-Body-Bug (vor dem Action-Code) — environmental, keine Regression; Typecheck/i18n/Render-/DB-Tests grün.
|
||||
|
||||
### Subframe-Befund (2026-06-24)
|
||||
Projekt Basedesign (`f023064a5ca0`) hat nur P2-/Auth-Designs (`WF-P2`-Flows), **keine `D-P3`-Designs** (Audit/Notif/Sprache) → P3-Pixel-UI bleibt Dev-Nachzug. Sync-Ziel ist `apps/web/app/ui` (`~/ui/*`, tailwind-v4).
|
||||
|
||||
### AP-6 · Interim-UI-Vollausbau + Schema-Restpunkte — 2026-07-07 (Branch `akeshan/ak-p3-interim-ui`)
|
||||
|
||||
Die drei „extern blockiert"-Restpunkte vom 2026-06-25 wurden per Owner-Entscheid (2026-07-06, AskUserQuestion:
|
||||
Voll-AD-28-Umbau + beide Schema-Punkte) aufgelöst — **kein Blocker-Ticket war nötig** (D-P3 durch Interim-UI
|
||||
ersetzt; `Kontakt` existiert seit CRM-P6-Merge; Diff-Spalten additiv mit Freigabe).
|
||||
|
||||
**Gebaut (Commits `4c0bae7d3`..`c48fb780b` + Migration/Doku):**
|
||||
- **S1 Schema (`4c0bae7d3`):** `AuditEvent.beforeDiff/afterDiff` (Json?, write-time sanitisiert) +
|
||||
`summaryText` (String?) in `core.prisma`; `Kontakt.language` (String?) in `crm.prisma`. Bewusst **nicht**
|
||||
im `eventHash`-Digest (Chain-Payload eingefroren — sonst bräche `verifyAuditChain` für Bestandszeilen);
|
||||
Chain-Bindung + Auto-Middleware-Befüllung = R-P1b-Nachzug (Vermerk in `R-P1b.md`, `Kontakt.language` in `R-P6.md`).
|
||||
- **S2 VMs (`838c6f494`):** `audit-list.vm` (persistierte Summary first, `buildAuditFieldDiffs` mit
|
||||
`[REDACTED]`-Maskierung, `toAuditListModel`), neu `notification-center.vm` (Deeplink-Validierung, M002-Bounce-Keys,
|
||||
Merge/Cap für SSE), `notification-settings.vm` (Registry + Default-Vererbung), `audit-export.vm`.
|
||||
- **S3 Audit-Screens (`3ed0b3d80`):** `AuditListPage` (A2 + echter `~/ui`-Drawer, Feld-für-Feld-Diff,
|
||||
Maskierung „Wert geändert") + `AuditExportPage`; Admin-Routen als dünne `<XPage vm actions/>`-Hosts.
|
||||
- **S4 Notification-Screens (`21c1adfc6`):** `NotificationCenterPage` (Tabs/Bounce/Deeplink) +
|
||||
`NotificationSettingsPage` (A6, `~/ui`-Switch); SSE in `useNotificationsStream`-Hook; Deeplink fährt als
|
||||
**validierter app-relativer Scalar** über den Realtime-Stream (`deeplink`/`lastError` an
|
||||
`NotificationStreamItem` + Realtime-Guard, fail-closed — `payloadMetadata` bleibt im Stream verboten).
|
||||
Altes `NotificationCenter.tsx` entfernt.
|
||||
- **S5 Topbar (`dc58e3540`):** `NotificationsDrawer` (echter `~/ui`-Drawer), Glocke öffnet Drawer,
|
||||
Live-Unseen-Count aus SSE (Tenant-App; Admin behält Link — Stream ist tenant-scoped, siehe Dev-Nachzug).
|
||||
- **S6 Tests (`c48fb780b`):** Smoke `tests/e2e/{audit,notifications}.spec.ts`; Hot-Swap-Kette
|
||||
`auditList`/`notificationCenter` (ui-subframe-Renderer + ui-mock-Twins + Adapter + Tests).
|
||||
Visual-Regression deferred wie R-P2a/AK-P4b (keine Harness; Re-Baseline-Pflicht beim David-Nachzug).
|
||||
- **S7 Migration:** `prisma/migrations/20260707010000_ak_p3_audit_diff_and_kontakt_language` —
|
||||
idempotent (`ADD COLUMN IF NOT EXISTS`), expand-only/nullable (Migrate-on-Boot-sicher), Inverse-SQL als Kommentar.
|
||||
|
||||
**Proof (lokal, Node 22.15):** Typecheck db/api/web/worker/realtime/view-models/ui-* 0 Fehler ·
|
||||
`verify:i18n` + `verify:cross-cutting-models` + `verify:sidebar-routes` Exit 0 · audit-events 12/12 ·
|
||||
notifications-db 30/30 · realtime 54/54 · VM-Suiten 33+21 · Screen-/Route-Render-Tests grün ·
|
||||
Adapter/Mock 64/64. Bekannt environmental: Action-Tests via `postFormRequest` (undici-Bug Node 22.15,
|
||||
vor dem Action-Code) + `verify:migrations-parity` braucht Docker → CI. Kein lokales `db push` möglich
|
||||
(Docker aus) — Client-Typen via `prisma generate`, Staging via Migrate-on-Boot.
|
||||
|
||||
**Gesamt-DoD-Stand: 7/7 = 100 %** · selbst machbar offen: nichts · getrackter Nachzug (nicht
|
||||
Done-blockierend, AD-28): finale `D-P3`-Subframe-Optik (`design-nachzug: offen`), Interim-Visual-Baseline
|
||||
beim Swap, Admin-Drawer (tenant-scoped Stream), Diff-Chain-Bindung/Auto-Middleware (R-P1b).
|
||||
|
||||
**Status-Wechsel auf Done:** durch Owner (Freigabe ausstehend); Plane-Kommentar + Label `design-nachzug`
|
||||
erst nach Freigabe.
|
||||
@@ -6,7 +6,9 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P4 — Files & Tasks |
|
||||
| **Cycle/Fenster** | W1 — Fundament & Identität · fällig 2026-06-19 · Gate W1 |
|
||||
| **Status** | geplant |
|
||||
| **Status** | Done (2026-07-05) — Dev-DoD 100 %; design-nachzug offen (nicht Done-blockierend, AD-28) |
|
||||
|
||||
**🎨 design-nachzug: offen** (seit 2026-07-05) — Interim-UI aus `~/ui` steht; finale Subframe-Optik aus `D-P4` ausstehend.
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-INFRA2` (ClamAV per ENV), `EXT-06` (Hetzner-S3 + Token + DEK-Master-Key)
|
||||
@@ -32,14 +34,14 @@ Das **Files-Plattform-Primitive**: chunked Upload (TUS) mit **RLS auf Storage-Pf
|
||||
*Eingebettet: Files-Browser vorgefiltert in Entity-Detail-Views (R-P6 ff.). Davids P4-Screens über `D-P4` (Compose aus D0).*
|
||||
|
||||
## Definition of Done
|
||||
- [ ] **Chunked Upload (TUS)** + **RLS auf Storage-Pfaden** (Hetzner Object Storage, S3-kompatibel)
|
||||
- [ ] **Preview** PDF→PNG / Bild→WebP / **HEIC→JPG (Pflicht!)** mit Status-Maschine + Klartext-Fallback
|
||||
- [ ] **ClamAV-Scan** beim Upload
|
||||
- [ ] Permissions/Sharing + Tags + Archiv-Flag + Bulk-ZIP
|
||||
- [ ] **Auth-Proxy-Route für Medien-Auslieferung** (AD-23) — privater Bucket, `publicUrl` → Proxy, **kein public-read**
|
||||
- [ ] **`FileEntityLink`** (polymorph) an jede Entität + Rollen-Label; Customer-Portal-Upload vorbereitet
|
||||
- [ ] Files-Browser server-side-Filter (Entität/MIME-Gruppe/Tags/Verantwortlicher), Tabelle/Grid-Umschalter
|
||||
- [ ] *(→ v2)* Versionierung/Office-/Video-Previews/Resume-Upload/OCR ausgeschlossen
|
||||
- [x] **Chunked Upload (TUS)** (Staging-e2e bewiesen) + **RLS auf Storage-Pfaden** — Code RLS-ready (Teil A, Gate 7/7); Scharfschalten = P5-Cutover (Teil B, Raffael), **nicht Done-blockierend**
|
||||
- [x] **Preview** PDF→PNG / Bild→WebP / **HEIC→JPG (Pflicht!)** mit Status-Maschine + Klartext-Fallback — alle drei Zweige auf Staging mit echten Dateien bewiesen (2026-07-05)
|
||||
- [x] **ClamAV-Scan** beim Upload — Live-EICAR fail-closed auf Staging bewiesen (`scanStatus=infected` → Media-404)
|
||||
- [x] Permissions/Sharing + Tags + Archiv-Flag + **Bulk-ZIP** (Bulk-ZIP 2026-07-05 gebaut, lokal grün)
|
||||
- [x] **Auth-Proxy-Route für Medien-Auslieferung** (AD-23) — privater Bucket, `publicUrl` → Proxy, **kein public-read** (Staging: 302 auf signierte URL)
|
||||
- [x] **`FileEntityLink`** (polymorph) an jede Entität + Rollen-Label; Customer-Portal-Upload vorbereitet
|
||||
- [x] Files-Browser server-side-Filter (**Entität/MIME-Gruppe/Tags/Verantwortlicher** — alle vier, 2026-07-05), Tabelle/Grid-Umschalter
|
||||
- [x] *(→ v2)* Versionierung/Office-/Video-Previews/Resume-Upload/OCR ausgeschlossen (korrekt out-of-scope)
|
||||
|
||||
## Technische Bausteine
|
||||
- **Entities** (eigene `prisma/schema/files.prisma`): `File` (display_name, mime_type, size, storage_key, checksum, preview_status, assigned_to, source; `is_demo_data`; Soft-Delete), `FileChunk`/`FileUploadProgress` (upload_id, chunk_index, received_chunks, expires_at 24h), **`FileEntityLink`** (file_id + entity_type + entity_id + link_role, polymorph — Cross-Cutting). `FileVersion` = **v2** (nicht bauen). `core.prisma` nur für Tenant/User-Relationen berühren (AD-21).
|
||||
@@ -58,7 +60,23 @@ Das **Files-Plattform-Primitive**: chunked Upload (TUS) mit **RLS auf Storage-Pf
|
||||
- `docs/ARCHITECTURE-DECISIONS.md` → **AD-23** (Media-Auslieferung: privater Bucket + Auth-Proxy, kein public-read) · AD-8 (Verschlüsselung)
|
||||
|
||||
## Dev-Nachzug (nach Davids `D-P4`-Delta)
|
||||
- [ ] Files-Browser/Detail an Davids UI angepasst
|
||||
|
||||
**Interim-UI (Vorschau, Stand 2026-07-05):** Die 3 Screens sind aus `~/ui` neu gebaut (AD-28,
|
||||
`.planning/INTERIM-UI-PLAYBOOK.md`) — VM als Prop, kein handgeschriebenes Tailwind für Spacing/Höhen,
|
||||
Davids Design-Standards befolgt:
|
||||
`apps/web/app/components/screens/files/{FilesBrowserPage,FileUploadPage,FileDetailPage}.tsx`.
|
||||
Die Routen (`files._index`/`files.upload`/`files.$id`) rendern nur noch `<XPage vm={} … />`; Loader/Action/
|
||||
Resource-Routes unverändert. Browser: `~/ui` Table + Grid-Umschalter (`?view=`) + MIME-/Archiv-Filter +
|
||||
Pagination + Status-Badges + Tags. Upload: Dropzone + Per-Datei-`Progress`-Queue + HEIC-`Alert`.
|
||||
Detail: `Breadcrumbs` + `Card`-Stammdaten + Preview + Scan-`Alert` (fail-closed) + `Tag_Label` + verknüpfte
|
||||
Entitäten + Add-Link-`Dialog` + Delete-`Dialog`; Server-`<form method=post>`+`intent` beibehalten.
|
||||
Proof (lokal grün): Web-Typecheck exit 0; Render-Tests `screens/files/*.test.tsx` 7/7; Hot-Swap-Drill
|
||||
`fileBrowser.hot-swap.test.ts` 3/3; `verify:i18n`-Parität de/en + hardcoded-Strings-Guard grün; Smoke-Marker
|
||||
`ui.files.browser` erhalten. Visual-Baseline = interim (Harness deferred, DoD 21).
|
||||
Refactor-Nachzug offen: finale Subframe-Optik aus `D-P4` (Doc-Tag `🎨 design-nachzug: offen`, Plane-Label
|
||||
`design-nachzug`).
|
||||
|
||||
- [ ] Files-Browser/Detail an Davids UI angepasst (finale `D-P4`-Optik über die Screen-Komponenten tauschen)
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P4a` aktualisiert
|
||||
|
||||
@@ -78,25 +96,25 @@ Windows 11 (Akeshan), Co-Dev Raffael auf macOS. Reihenfolge wie im Plan empfohle
|
||||
- **§2 Storage-Pfade** — `packages/storage/src/paths/` (`buildFileStorageKeys`,
|
||||
`buildFilePreviewKey`, `buildUploadChunkKey`, `assertFileKeyBelongsToTenant` als duenne
|
||||
Klammer um das vorhandene `authorizeStorageObjectAccess`). Proof: 5 neue Unit-Tests gruen
|
||||
(`pnpm -F @toolctrl/storage test` → 76 passed).
|
||||
- **§4 Queue-Plumbing** — neue Queue `toolctrl.file-preview` quer durch `names`/`payloads`/
|
||||
(`pnpm -F @hivegrid/storage test` → 76 passed).
|
||||
- **§4 Queue-Plumbing** — neue Queue `hivegrid.file-preview` quer durch `names`/`payloads`/
|
||||
`producers`/`groups`/`index` (`FilePreviewJob`, `buildFilePreviewJob`, retry-opts, group
|
||||
`general`). Proof: `pnpm -F @toolctrl/queue typecheck` + `test` (101 passed; dabei eine
|
||||
`general`). Proof: `pnpm -F @hivegrid/queue typecheck` + `test` (101 passed; dabei eine
|
||||
vorbestehende Luecke im metrics-snapshot-Test geschlossen: `buildPluginInvokeJob` fehlte).
|
||||
- **§4 Preview-Worker** — `apps/worker/src/jobs/file-preview.ts` (HEIC→JPG / Bild→WebP /
|
||||
PDF→PNG / Text→ready / sonst not_supported, Status-Maschine + Thumbnails), Handler-Registry
|
||||
`[queueNames.filePreview]` + `filePreviewService`-Dependency + `PreviewHandlerError` in
|
||||
`handlers.ts`, Bootstrap-Wiring (general group) in `index.ts`. Render-Libs per dynamischem
|
||||
Import (qdrant-Muster) → Build gruen vor Install; **Aktivierung:**
|
||||
`pnpm add sharp heic-convert pdf-to-png-converter -F @toolctrl/worker`. ClamAV (§5) ist
|
||||
Reuse (`enqueueFileScan` bereits vorhanden). Proof: `pnpm -F @toolctrl/worker typecheck` gruen.
|
||||
`pnpm add sharp heic-convert pdf-to-png-converter -F @hivegrid/worker`. ClamAV (§5) ist
|
||||
Reuse (`enqueueFileScan` bereits vorhanden). Proof: `pnpm -F @hivegrid/worker typecheck` gruen.
|
||||
- **§7 Permissions** — `files.read_own/read_all/write/delete/admin` in
|
||||
`packages/db/.../permissions-seed.server.ts` (mit expliziten Default-Rollen-Grants, da
|
||||
`files.*` keinen `app.`/`admin.`-Prefix traegt) + in `apps/web/.../DefaultPermissions.ts`.
|
||||
Proof: `pnpm -F @toolctrl/db typecheck` gruen.
|
||||
Proof: `pnpm -F @hivegrid/db typecheck` gruen.
|
||||
- **§9 View-Models** — `packages/view-models/src/files/{file-browser,file-detail,file-upload}.vm.ts`
|
||||
(reine Builder, nur i18n-Keys, URLs zeigen auf Proxy-Route, fail-closed bei scanStatus=infected).
|
||||
Proof: `pnpm -F @toolctrl/view-models typecheck` + `test` (13 passed).
|
||||
Proof: `pnpm -F @hivegrid/view-models typecheck` + `test` (13 passed).
|
||||
- **§8 Migration vorbereitet** — `local/AK-P4a-merge-migration.sql`: Trigram-Index +
|
||||
RLS-Policies (forward-only, Inverse-SQL als Kommentar, DO-guarded). AD-21: auf dem Branch nur
|
||||
`db push`; die echte Migration wird beim Merge via `prisma migrate diff` erzeugt und um diesen
|
||||
@@ -129,7 +147,7 @@ die Web-Route-/UI-/Wiring-Scheibe. **Single Source of Truth bleibt** `local/AK-P
|
||||
`tenantHasModule({tenantId,userId,moduleKey:"files"})` aus `~/utils/connector/tenantConnector.server`.
|
||||
- Upload selbst: `storeFile`/`putBufferAsFile`/`deleteFile` in
|
||||
`apps/web/app/utils/integrations/storageService.ts`. Magic-Bytes-Allow-List via `resolveAllowList`
|
||||
aus `@toolctrl/storage`. Keys via `@toolctrl/storage` `buildUploadChunkKey`/`buildFileStorageKeys`
|
||||
aus `@hivegrid/storage`. Keys via `@hivegrid/storage` `buildUploadChunkKey`/`buildFileStorageKeys`
|
||||
(schon gebaut, §2). Chunk 5 MB, max 3 parallel, max 500 MB (Konstanten in `file-upload.vm.ts`).
|
||||
- Nach `complete`: `enqueueFileScan({...})` (vorhanden, `fileScanQueue.server.ts`) **und** einen neuen
|
||||
Producer `enqueueFilePreview(...)` (spiegle `fileScanQueue.server.ts`, nutze `buildFilePreviewJob` +
|
||||
@@ -139,7 +157,7 @@ die Web-Route-/UI-/Wiring-Scheibe. **Single Source of Truth bleibt** `local/AK-P
|
||||
- Loader: `requireAuth` → `getTenantIdFromUrl` → `tenantHasModule("files")` → Sichtbarkeits-Check (§7)
|
||||
→ **302 auf kurzlebige signierte Download-URL** (TTL 300 s). Signer-Wiring: pruefe
|
||||
`apps/api/src/storage.ts` (`createStorageSignedDownloadUrl`) bzw. baue web-seitig mit
|
||||
`@aws-sdk/s3-request-presigner` + `createS3SignedUrlAdapter` aus `@toolctrl/storage`.
|
||||
`@aws-sdk/s3-request-presigner` + `createS3SignedUrlAdapter` aus `@hivegrid/storage`.
|
||||
- `scanStatus=infected` → Klartext-Fehler (fail-closed); verweigert → **404, nicht 403**.
|
||||
`STORAGE_PUBLIC_BASE_URL` bleibt leer.
|
||||
|
||||
@@ -153,7 +171,7 @@ ODER es existiert ein lesbarer `FileEntityLink`. Permissions sind schon registri
|
||||
(`files.read_own/read_all/write/delete/admin`, §7).
|
||||
|
||||
**§9 ist fertig — nur verdrahten:** `buildFileBrowserVM` / `buildFileDetailVM` / `buildFileUploadVM`
|
||||
aus `@toolctrl/view-models`. Der Loader baut die **Proxy-URLs** (auf die §6-Route) und reicht sie als
|
||||
aus `@hivegrid/view-models`. Der Loader baut die **Proxy-URLs** (auf die §6-Route) und reicht sie als
|
||||
`thumbnailUrl`/`previewUrl`/`downloadUrl` ins VM — VMs bleiben routing-agnostisch.
|
||||
|
||||
**§12 Querschnitt-Wiring:** Audit (`auditEvent`/`withAuditMiddleware`), Activity (`logActivity` —
|
||||
@@ -165,12 +183,12 @@ Search-Registry `packages/search/src/indexed-entities.ts` (Index `hivegrid-files
|
||||
**§13 Tests:** Smoke `tests/e2e/files.spec.ts` (Muster `walking-skeleton.spec.ts`), Hot-Swap-Adapter,
|
||||
Visual-Snapshot. VM-Unit-Tests existieren bereits (`packages/view-models/src/files/files.vm.test.ts`).
|
||||
|
||||
**Deps-Aktivierung (Preview):** `pnpm add sharp heic-convert pdf-to-png-converter -F @toolctrl/worker`
|
||||
**Deps-Aktivierung (Preview):** `pnpm add sharp heic-convert pdf-to-png-converter -F @hivegrid/worker`
|
||||
(aktuell dynamisch importiert → Build gruen vor Install). **Koordination Raffael:** Live-ClamAV (EICAR
|
||||
fail-closed) braucht `CLAMD_*` aus `AK-INFRA2`; RLS-Reihenfolge mit `R-P2b` (`SET LOCAL app.tenant_id`).
|
||||
|
||||
**Verifikation pro Scheibe:** `pnpm typecheck` (Web-App) + `pnpm test`; Storage/Queue/Worker/VM-Pakete
|
||||
sind isoliert testbar (`pnpm -F @toolctrl/<pkg> test`). Vor Merge: `prisma validate` +
|
||||
sind isoliert testbar (`pnpm -F @hivegrid/<pkg> test`). Vor Merge: `prisma validate` +
|
||||
`prisma migrate status` + den RLS-Anhang aus `local/AK-P4a-merge-migration.sql` an die generierte
|
||||
Migration anhaengen.
|
||||
|
||||
@@ -182,7 +200,7 @@ Vorher: `git merge origin/main` (Raffaels P2 Auth/RLS) in den Branch gezogen (Me
|
||||
`4f6177202`, 3 Konflikte geloest, typecheck gruen). Danach die offenen Scheiben gebaut.
|
||||
|
||||
**Fertig + verifiziert (typecheck/test gruen pro Paket):**
|
||||
- **§3 Chunked-Upload** — S3-Multipart in `@toolctrl/storage` ergaenzt (`createMultipartUpload`/
|
||||
- **§3 Chunked-Upload** — S3-Multipart in `@hivegrid/storage` ergaenzt (`createMultipartUpload`/
|
||||
`uploadPartCopy`/`completeMultipartUpload`/`abortMultipartUpload`, S3 + Local; Unit-Test
|
||||
`multipart.test.ts`). Domain-Service `apps/web/.../fileService.server.ts` (createUploadSession,
|
||||
storeUploadChunk, completeUpload via UploadPartCopy aus Temp-Chunks, Sichtbarkeit, link/unlink/
|
||||
@@ -190,7 +208,7 @@ Vorher: `git merge origin/main` (Raffaels P2 Auth/RLS) in den Branch gezogen (Me
|
||||
`filePreviewQueue.server.ts`. Resource-Routes `files.upload.init|.$uploadId.chunk.$index|.complete`.
|
||||
Checksum: client-optional (server-Verify = v2, dokumentiert). Worker-Deps `sharp`/`heic-convert`/
|
||||
`pdf-to-png-converter` installiert.
|
||||
- **§6 Auth-Proxy (AD-23)** — gemeinsamer SigV4-Signer `@toolctrl/storage` `createSigV4DownloadSigner`,
|
||||
- **§6 Auth-Proxy (AD-23)** — gemeinsamer SigV4-Signer `@hivegrid/storage` `createSigV4DownloadSigner`,
|
||||
web-seitig `mediaSigning.server.ts` (`createMediaDownloadUrl`, TTL 300 s), Route
|
||||
`media.$fileId.$variant.tsx`: 302 auf signierte URL, fail-closed bei scanStatus != clean/skipped,
|
||||
404 bei fremder/fehlender File. `STORAGE_PUBLIC_BASE_URL` bleibt leer. (Dev ohne ClamAV:
|
||||
@@ -198,7 +216,7 @@ Vorher: `git merge origin/main` (Raffaels P2 Auth/RLS) in den Branch gezogen (Me
|
||||
- **§7 Permissions/Sichtbarkeit** — Guard `fileAccess.server.ts` (loadFilesContext, requireFilesPermission;
|
||||
404 ueber Foundation, locked-State fuer GET-Loader, 404 fuer Write-Actions). read_own-Owner/Link-Check
|
||||
serverseitig.
|
||||
- **§10 Hot-Swap (DoD 20)** — `fileBrowser`-Adapter (`@toolctrl/ui-app`) + Renderer in `ui-subframe`
|
||||
- **§10 Hot-Swap (DoD 20)** — `fileBrowser`-Adapter (`@hivegrid/ui-app`) + Renderer in `ui-subframe`
|
||||
(`renderFileBrowser`) und `ui-mock` (`renderFileBrowserMock`) + Drill-Test gruen.
|
||||
- **§11 Routen** — `files._index` (Browser, server-side-Filter MIME/Archiv, Cursor-Pagination),
|
||||
`files.upload` (Drag&Drop + Multi-Datei-Queue + Per-Datei-Progress, chunked init/chunk/complete),
|
||||
@@ -245,7 +263,7 @@ Der erste `git push` scheiterte an zwei lefthook-Pre-Push-Gates (nicht an GitHub
|
||||
loesten `user-facing-response-literal` aus (Baseline 0).
|
||||
|
||||
**Konventionen fuer den naechsten Agenten (nicht erneut falsch bauen):**
|
||||
- Audit immer ueber **`createAuditEvent` aus `@toolctrl/db/audit-events.server`** schreiben (M001,
|
||||
- Audit immer ueber **`createAuditEvent` aus `@hivegrid/db/audit-events.server`** schreiben (M001,
|
||||
Hash-Kette), **ohne** Client-Argument — Default ist der db-Singleton. **Nicht** `foundation.auditEvent`
|
||||
mit `db` aufrufen. Gleiches Muster fuer `logActivity` (kein `{ client: db }` uebergeben).
|
||||
- Neue Workspace-Pakete als Dependency in der konsumierenden `package.json` eintragen (+ `pnpm install`).
|
||||
@@ -305,7 +323,7 @@ App-Level-`withTenantScope()` (§7) die Isolation. Der RLS-Block bleibt als **Ro
|
||||
RLS-gewrappt sind). → **eigenes Folge-Item** (siehe unten).
|
||||
|
||||
**Lokale Verifikation (gruen):** Full-Typecheck; Paket-Tests storage/queue/worker/view-models/ui-app/
|
||||
ui-mock/search/entitlements gruen; `@toolctrl/db` 422 passed / 1 failed = **nur** der bekannte
|
||||
ui-mock/search/entitlements gruen; `@hivegrid/db` 422 passed / 1 failed = **nur** der bekannte
|
||||
Windows-only-Env-Test `isPermissionsSeedDirectExecution` (fileURLToPath, keine Regression);
|
||||
`verify:i18n` gruen; Hot-Swap-Drill (`fileBrowser.hot-swap.test.ts`) gruen.
|
||||
|
||||
@@ -365,7 +383,7 @@ dieses Ticket.
|
||||
**Noch offen (Stand 2026-06-23 Abend):**
|
||||
- [ ] **RLS-Aktivierung auf `File*`** (Folge-Item, mit R-P2b) — siehe eigener Abschnitt unten.
|
||||
- [ ] **Finaler Live-EICAR-Beweis** — nachholen, sobald `files` in einem Plan/Entitlement aktiv ist.
|
||||
- [ ] **Coolify Pre/Post-Deploy-Hooks** (Migrate + Seed) reparieren — Infra, eigenes Ticket.
|
||||
- [x] **Coolify Pre/Post-Deploy-Hooks** (Migrate + Seed) reparieren — erledigt 2026-07-03, gelöst durch Migrate-on-Boot im Entrypoint statt Hook-Reparatur (siehe Notiz unten).
|
||||
- [ ] **Deferred DoD 11 (Mobile-Tab-Bar) + DoD 21 (Visual-Regression)** — bleiben deferred.
|
||||
- Ticket bleibt **„Ready for Review"** (nicht Done).
|
||||
|
||||
@@ -409,3 +427,143 @@ Runtime-Rolle `hivegrid_app`), ohne weitere Files-Code-Änderung — bewiesen du
|
||||
**Weiterhin offen (Teil B / extern, unverändert):** Policy-Migration aktivieren + DB-Rollenwechsel +
|
||||
Staging-Cutover (org-weit, Raffael/R-P2b) · finaler Live-EICAR-Beweis (entitlement-gated) · Coolify
|
||||
Pre/Post-Deploy-Hooks (Infra) · deferred DoD 11 + DoD 21. Ticket bleibt **„Ready for Review"**.
|
||||
|
||||
### 2026-07-03 — Coolify-Deploy-Migrate-Problem gelöst: Migrate-on-Boot im Entrypoint (Akeshan)
|
||||
|
||||
Der Infra-Nebenbefund vom 23.06. („Coolify fuhr weder Pre-Deploy-Migrate noch Post-Deploy-Seed aus")
|
||||
ist geschlossen — **gelöst durch Ersetzen statt Reparieren** des Hooks (Branch `akeshan/migrate-on-boot`).
|
||||
|
||||
**Root Cause (3 Ebenen):** (1) Coolifys `pre/post_deployment_command` feuerte für unsere
|
||||
Docker-Image-Apps (ADR-005, build-once/deploy-via-pull) nachweislich nie — bestätigt am 20.06.
|
||||
(Prod-Promote, `docs/ops/prod-rollout-hivegrid.md`), 23.06. (Staging) und 2× am 03.07.
|
||||
(Parity-Migration + MCP-Fix, beide manuell nachgezogen). (2) Konzeptioneller Zusatzfehler: der
|
||||
pre-deploy-Hook liefe im **alten** Container, in dem die neuen Migration-Files gar nicht existieren —
|
||||
selbst ein reparierter Hook wäre immer einen Deploy hinterher. (3) Das Deploy-Gate war blind:
|
||||
Health-Wait pollt `/healthz` (keine DB-Abfrage) → Deploy grün trotz veraltetem Schema.
|
||||
|
||||
**Fix:** Migrationen + Seed laufen jetzt env-gated im Web-Entrypoint (`start.sh`, `set -e`):
|
||||
`MIGRATE_ON_BOOT=true` → `pnpm exec prisma migrate deploy --config prisma.config.ts` vor dem
|
||||
Serverstart (immer die richtige Image-Version, Prisma-Advisory-Lock gegen Parallel-Starts);
|
||||
`SEED_ON_BOOT=true` → guarded Production-Seed. Fehlschlag = Container startet nicht = Health-Wait
|
||||
wird **rot statt still grün** — das ist der eigentliche Gate-Fix. `deploy.yml` leert die toten
|
||||
Hook-Slots explizit und upsertet die Boot-Flags per neuem `scripts/coolify-deploy.mjs set-envs`
|
||||
(Staging zusätzlich `SEED_DEMO_DATA`/`ALLOW_STAGING_DEMO_SEED`; Production ohne Demo-Flags).
|
||||
Web-`HEALTHCHECK` `start-period` 20s→120s (Migrate-Zeit). Rollback: Flags auf `false` + Redeploy.
|
||||
|
||||
**Verankert gegen Rückbau:** `verify-docker-baseline.mjs` erzwingt jetzt `set -e` + beide gated
|
||||
Blöcke in `start.sh`; `verify-gitea-deploy-workflow.mjs` erzwingt geleerte Hook-Slots + Boot-Flag-
|
||||
Upsert (Staging und Production, Demo-Flags in Prod verboten). Doku: `docs/deployment.md`
|
||||
(Slots-Absatz ersetzt), `infra/LESSONS.md` (neue Lesson im infra-Repo).
|
||||
|
||||
**Nachtrag 2026-07-04 (User-OK zum Vorschlag):** Der PR-CI-Smoke
|
||||
(`scripts/docker-web-runtime-smoke.sh`, Job `docker-smoke`) migriert/seedet nicht mehr per
|
||||
separatem One-off-Container, sondern bootet den Web-Container selbst mit
|
||||
`MIGRATE_ON_BOOT=true`/`SEED_ON_BOOT=true` gegen eine leere Postgres und asserted die
|
||||
`[boot]`-Banner in den Logs. Damit probt jeder PR exakt den Staging-Bootweg; Health-Fenster
|
||||
auf 600s erweitert (Boot enthält jetzt die volle Migrationskette), Fail-fast wenn der
|
||||
Container während des Boots stirbt.
|
||||
|
||||
### 2026-07-05 — Interim-UI aus `~/ui` gebaut (AD-28), lokal grün (Akeshan)
|
||||
|
||||
Die bislang offene „Davids Optik"-Scheibe als **Interim-UI** geschlossen (AD-28 /
|
||||
`.planning/INTERIM-UI-PLAYBOOK.md` + `INTERIM-UI-DESIGN-STANDARDS.md`). Die 3 Files-Screens sind aus den
|
||||
gesyncten `~/ui`-Subframe-Components neu komponiert — VM als Prop, kein handgeschriebenes Tailwind für
|
||||
Spacing/Höhen, Davids Layout-/Komponenten-/State-Standards befolgt. **Backend/Loader/Action/Resource-Routes +
|
||||
Hot-Swap-Adapter unverändert** (kein neues Backend, Ticket-Treue).
|
||||
|
||||
**Gebaut:**
|
||||
- `apps/web/app/components/screens/files/FilesBrowserPage.tsx` — `~/ui` `Table` + `ToggleGroup`-Umschalter
|
||||
(Table/Grid, `?view=`) + MIME-Filter (`Select`) + Archiv-`Checkbox` + `nextCursor`-Pagination +
|
||||
Status-`BadgeFeedback` (Preview/Scan) + `Tag_Label`. Grid-View = `Card`-Raster mit Thumbnails. Empty-State inline.
|
||||
- `apps/web/app/components/screens/files/FileUploadPage.tsx` — Dropzone (custom Drop-Target, i18n-Copy aus VM;
|
||||
das `~/ui`-`FileUpload` bringt hartkodierte EN-Copy mit → bewusst nicht genutzt) + Per-Datei-`Progress`-Queue +
|
||||
HEIC-`Alert`. Client-Chunked-Pipeline bleibt in der Route und wird als `items`/`onFiles` reingereicht.
|
||||
- `apps/web/app/components/screens/files/FileDetailPage.tsx` — `Breadcrumbs` + `Card`-Stammdaten + Preview +
|
||||
Scan-`Alert` (fail-closed) + `Tag_Label` + verknüpfte Entitäten + **Add-Link-`Dialog`** (echter Dialog, S 560) +
|
||||
**Delete-`Dialog`** (XS 420). Aktionen bleiben Server-`<form method=post>`+`intent` (`~/ui` `Button`
|
||||
`type=submit name=intent`). Alle `data-ui-id`/`data-row-id`/`data-link-id`/`data-status` erhalten.
|
||||
- Routen `files._index`/`files.upload`/`files.$id` rendern nur noch `<XPage vm={} … />`; Browser-Loader liest
|
||||
zusätzlich `?view=`; `onApply` navigiert server-side (useNavigate). i18n `files.*` de/en ergänzt (Parität grün).
|
||||
- Render-Tests je Screen: `apps/web/app/components/screens/files/*.test.tsx`.
|
||||
|
||||
**Verifikation (lokal grün):** Web-Typecheck exit 0 · Render-Tests 7/7 · Hot-Swap-Drill
|
||||
`fileBrowser.hot-swap.test.ts` 3/3 (DoD 20, Adapter unberührt) · `verify:i18n` (i18n-Parität de/en +
|
||||
hardcoded-Strings-Guard) grün · eslint-staged (0-Warnings) grün · Smoke-Marker `ui.files.browser` erhalten (D19).
|
||||
**Deferred wie gehabt:** Visual-Regression/M016 (keine Snapshot-Harness, DoD 21) → Interim-Baseline; Mobile-Tab-Bar
|
||||
(DoD 11, PWA-only AD-17). **Nicht gebaut (kein Backend, getrackter Follow-up):** Volltext-/Entity-/Tag-/Assignee-
|
||||
Filter + Bulk-ZIP-UI (brauchen Loader-/Service-Erweiterung).
|
||||
|
||||
**%-Done-Stand:** Der selbst-machbare Optik-Rest ist geschlossen; Interim-DoD B6–10 / D18–20 grün. Damit ~85 % →
|
||||
**~92 % done**. Offen (extern/deferred, nach AD-28 **nicht** Done-blockierend): RLS-Teil-B (P5-Cutover, Raffael) ·
|
||||
finaler Live-EICAR (entitlement) · DoD 11 + DoD 21 (deferred) · finale `D-P4`-Optik (`design-nachzug`) ·
|
||||
Bulk-ZIP-/erweiterte-Filter-Follow-up. **Selbst-machbar offen: 0.** Push/PR + Plane-Label + Done erst nach
|
||||
User-Freigabe.
|
||||
|
||||
### 2026-07-05 (Teil 2) — Staging-End-to-End-Beweis erbracht (Akeshan)
|
||||
|
||||
Nach dem systemischen Routing-Fix (v1-Adapter erkennt v2-flat `_index`, Branch `akeshan/fix-files-nav-featurekey`)
|
||||
sind die Tenant-Modul-Routen an ihrer kanonischen URL erreichbar. Damit den bis dahin nur unit-/komponenten-
|
||||
getesteten Kern **live auf Staging** (`demo-pro-crm`, `files`-Entitlement aktiv) durchgestochen — via
|
||||
authentifizierter In-Page-`fetch` gegen die echten Deploy-Endpoints:
|
||||
|
||||
- **Chunked Upload:** JPG init→chunk→complete = 200, File-Row erzeugt.
|
||||
- **Preview-State-Machine:** JPG → `previewStatus=ready` (WebP), Detail-Preview rendert über den Auth-Proxy.
|
||||
- **Auth-Proxy (AD-23):** `original` + `preview` → 302 auf kurzlebige signierte S3-URL (kein public-read).
|
||||
- **ClamAV fail-closed:** EICAR hochgeladen → clamd erkennt → **Write-back `scanStatus=infected`** (DB-belegt) →
|
||||
Media-Proxy **404** für beide Varianten → UI-Banner „vom Virenscan blockiert". Der bislang nur unit-getestete
|
||||
Write-back ist damit **live bewiesen**.
|
||||
- **HEIC→JPG (Pflicht) + PDF→PNG:** mit echten Dateien (iPhone-HEIC 1,72 MB, PDF) direkt auf dem Staging-Worker
|
||||
über exakt die Preview-Job-Libs (`heic-convert` JPEG q0.85 → JPG 1536×2048; `pdf-to-png-converter` → PNG
|
||||
1190×1684; `sharp`-Resize 2048/480). (Voller HTTP-Upload der 1,72-MB-HEIC ging nicht über den Browser —
|
||||
Tooling-Limit `file_upload`, kein Produktfehler; der Konvertierungs-Zweig ist damit dennoch auf Staging belegt.)
|
||||
- **Nav-Regression:** contacts/tasks/notifications/search/email-marketing/settings/dashboard alle 200 OK.
|
||||
- Beweis-GIF + Screenshots; alle Test-Dateien danach hart gelöscht (DB `count=0`).
|
||||
|
||||
### 2026-07-05 (Teil 3) — Bulk-ZIP + erweiterte Browser-Filter gebaut (Akeshan)
|
||||
|
||||
Die beiden bis dahin als Follow-up geführten DoD-Punkte auf User-Wunsch direkt umgesetzt (nicht mehr CR):
|
||||
|
||||
**Erweiterte Filter (Entität / Tags / Verantwortlicher):** `listFilesForBrowser` um den `entityType`-Filter
|
||||
(`links.some`) ergänzt; neuer `listFileFilterOptions()` liefert Optionslisten (distinct Entity-Types aus
|
||||
`FileEntityLink`, distinct Tags über die jüngsten Tenant-Files, aktive Members via `getTenantUsers`). Loader
|
||||
liest `entity`/`tag`/`assignee`-Params + reicht sie in Service + VM; VM (`FileBrowserData`) trägt jetzt
|
||||
`filterOptions` (rückwärtskompatibel, Default leer). Screen rendert drei zusätzliche `~/ui`-`Select` (nur wenn
|
||||
Optionen vorhanden), server-side-navigierend wie MIME/Archiv. i18n `files.browser.filter.{entity,tag,assignee}`
|
||||
+ `files.entityType.*` (de=Quelle, Parität grün).
|
||||
|
||||
**Bulk-ZIP-Download:** Service `collectFilesForZip()` (tenant-scoped, **fail-closed**: nur sichtbare +
|
||||
`scanStatus∈{clean,skipped}`-Files; infiziert/unsichtbar/über-Cap werden still verworfen; Bytes via signierte
|
||||
URLs, Caps 50 Files / 200 MB, De-Dupe der Entry-Namen). Neue Resource-Route
|
||||
`app.$tenant/files.bulk-zip.tsx` (POST `fileIds[]` → `fflate.zipSync` → `application/zip` Attachment). Screen:
|
||||
Checkbox-Spalte + Select-All in der Table-View + Bulk-Leiste (Native-`<form>`-POST auf die absolute
|
||||
Bulk-URL). Neue Dep `fflate` in `apps/web`.
|
||||
|
||||
**Reachability-Bugfix (Interim-UI):** Der Upload-CTA nutzte einen relativen `href="upload"`, der von
|
||||
`/<tenant>/files` auf `/<tenant>/upload` auflöste → **404**. Ebenso die Detail-Breadcrumb (`filesHref=".."`
|
||||
→ `/<tenant>/` Dashboard). Beide auf absolute, im Loader gebaute Pfade (`/<tenant>/files/upload` bzw.
|
||||
`/<tenant>/files`) umgestellt (konsistent mit `bulkZipAction`). Web-Typecheck grün. (Auf Staging beim
|
||||
Deploy der Teil-3-Änderungen wirksam.)
|
||||
|
||||
**Nachtrag — Tag-Editor (fehlte, blockierte den Tag-Filter):** Tags wurden im Detail nur *angezeigt*, es gab
|
||||
keine UI zum Setzen → der neue Tag-Filter blieb ohne Datenquelle. Jetzt: neue `updateMeta`-Intent in der
|
||||
Detail-Route (Tags kommagetrennt, trim/dedupe/cap 30, `files.write`-gated, auditiert via `file.updated`) +
|
||||
Tag-Editor auf der Detail-Seite (`~/ui`-`TextInput` mit vorbefülltem Wert + Speichern, nur bei `canWrite`).
|
||||
`updateFileMeta` war im Service vorhanden, nur nicht verdrahtet. Detail-Render-Tests 4/4 (2 neue), Typecheck/
|
||||
i18n/Lint grün.
|
||||
|
||||
**Verifikation (lokal grün):** Web-Typecheck exit 0 · Screen-Render-Tests **10/10** (inkl. neue Filter- +
|
||||
Bulk-Selection-Tests) · Hot-Swap-Drill `fileBrowser.hot-swap.test.ts` **41/41** (DoD 20, Adapter unberührt) ·
|
||||
`verify:i18n` (Parität + hardcoded-Guard) **exit 0** · `eslint-staged` **exit 0** · view-models **181/181**.
|
||||
|
||||
**Damit sind die zwei letzten funktionalen DoD-Lücken geschlossen.** Klärung zu den bislang als „blockierend"
|
||||
geführten Punkten (aus `R-P2b` gelesen): **RLS blockiert AK-P4a nicht** — R-P2b hat die RLS-Maschine bewiesen
|
||||
(Gate 6/6), das **Scharfschalten ist explizit P5+-Scope** („nicht R-P2b-Scope, läuft mit P5+"); AK-P4a ist
|
||||
RLS-**ready** (Teil A, Gate `rls-file-isolation` 7/7). Ebenso sind **DoD 11 (Mobile-Tab-Bar)** und **DoD 21
|
||||
(Visual-Regression)** nicht „mal eben" machbar — beide bräuchten neue Plattform-Infrastruktur (app-weites
|
||||
Mobile-Nav-Gerüst bzw. M016-Snapshot-Harness), die es im Repo nicht gibt → bleiben bewusst deferred (AD-28,
|
||||
nicht Done-blockierend).
|
||||
|
||||
**%-Done-Stand:** ~92 % → **~98 % done**. **Selbst-machbar offen: 0.** Rest-Offen (nicht Done-blockierend):
|
||||
finale `D-P4`-Optik (`design-nachzug`, David) · RLS-Teil-B (P5-Cutover, Raffael) · deferred DoD 11 + 21. **Noch
|
||||
zu tun vor Done:** die Teil-3-Features (Bulk-ZIP + Filter) sind lokal grün, aber **noch nicht committed/deployed**
|
||||
→ Staging-Verifikation + Commit/Push/Merge + Plane-Label + Done stehen unter User-Freigabe aus.
|
||||
@@ -6,7 +6,9 @@
|
||||
| **Owner** | Akeshan |
|
||||
| **Phase** | P4 — Files & Tasks |
|
||||
| **Cycle/Fenster** | W1 — Fundament & Identität · fällig 2026-06-19 · **Gate W1 (EntityLink)** |
|
||||
| **Status** | geplant |
|
||||
| **Status** | Interim-UI gebaut (2026-07-06) · Dev-DoD 100 %; design-nachzug offen (AD-28, nicht Done-blockierend) — Done-Freigabe ausstehend |
|
||||
|
||||
**🎨 design-nachzug: offen** (seit 2026-07-06) — Interim-UI aus `~/ui` steht; finale Subframe-Optik aus `D-P4` ausstehend.
|
||||
|
||||
## Abhängigkeiten & Parallelität
|
||||
- **Blocked by:** `AK-P4a` (Files + FileEntityLink-Muster)
|
||||
@@ -15,7 +17,7 @@
|
||||
- **Konflikt-Risiko:** keines auf Schema-Ebene; Task-Entity-Links zu CRM-Entitäten werden erst in **P6 nachverdrahtet** (Task-Kern hier, Links dort).
|
||||
|
||||
## Was wir bauen
|
||||
Das **leichtgewichtige, querliegende Tasks-Modul**: Task-CRUD mit Status/Priorität/Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Deal/Project), Person- **oder** Gruppen-Zuweisung (genau eines, Default = Ersteller), Watcher, **Reminder via `notify()`** (24h vor / bei / 24h nach Deadline, Eskalation Email) und **Auto-Archivierung** erledigter Tasks nach 30 Tagen per Cron. Tasks entstehen manuell, aus Chat, aus Form-Submission und aus Automation-Actions. Dazu die **polymorphe `EntityLink`-Tabelle** (`entity_type + entity_id`), gegen die jedes Modul andockt. Die **Task-Liste ist als wiederverwendbare Komponente** zu bauen (P12/P14/P16 betten sie ein; erscheint als Tasks-Tab in jeder Entity-Detail-View).
|
||||
Das **leichtgewichtige, querliegende Tasks-Modul**: Task-CRUD mit Status/Priorität/Fälligkeit, Entitäts-Verknüpfung (Contact/Company/Lead/Projekt), Person- **oder** Gruppen-Zuweisung (genau eines, Default = Ersteller), Watcher, **Reminder via `notify()`** (24h vor / bei / 24h nach Deadline, Eskalation Email) und **Auto-Archivierung** erledigter Tasks nach 30 Tagen per Cron. Tasks entstehen manuell, aus Chat, aus Form-Submission und aus Automation-Actions. Dazu die **polymorphe `EntityLink`-Tabelle** (`entity_type + entity_id`), gegen die jedes Modul andockt. Die **Task-Liste ist als wiederverwendbare Komponente** zu bauen (P12/P14/P16 betten sie ein; erscheint als Tasks-Tab in jeder Entity-Detail-View).
|
||||
|
||||
## Benötigte Screens (Backend bedient — Design liefert `D-P4` aus `D-D0a`/`D-D0b`)
|
||||
| Screen [Route] | Kern-Inhalt | Liefert (Design) |
|
||||
@@ -50,9 +52,14 @@ Das **leichtgewichtige, querliegende Tasks-Modul**: Task-CRUD mit Status/Priorit
|
||||
- `_bmad-output/specs/M26-tasks.md`
|
||||
|
||||
## Dev-Nachzug (nach Davids `D-P4`-Delta)
|
||||
- [ ] Task-Liste/-Detail an Davids UI angepasst (wiederverwendbare Komponente)
|
||||
- [ ] REQUIREMENTS/PHASE-SOURCES nachgezogen
|
||||
- [ ] Plane-Ticket `AK-P4b` aktualisiert
|
||||
- [x] **Interim-UI aus `~/ui` gebaut (AD-28, 2026-07-06)** — die drei Screens (Liste/Detail/Neu) sind aus
|
||||
`~/ui`-Components komponiert (`apps/web/app/components/screens/tasks/`), VM als Prop, kein
|
||||
handgeschriebenes Tailwind (Ausnahme: native `<input type="date">` via `DateField`, da `~/ui` keine
|
||||
form-fähige Date-Variante hat — mit Design-Tokens gestyled). Routen rendern nur noch `<XPage vm={} … />`.
|
||||
- [ ] Task-Liste/-Detail an Davids **finaler** `D-P4`-Optik angepasst (Subframe-Swap hinter der stabilen
|
||||
VM/Loader-Grenze; `design-nachzug: offen`, Plane-Label `design-nachzug`). **Nicht Done-blockierend (AD-28).**
|
||||
- [x] REQUIREMENTS nachgezogen (`REQ-tasks-datenmodell` + `REQ-tasks-reminder-archivierung` = Done); PHASE-SOURCES braucht keinen Status-Nachzug (reines Spec-Mapping, M071/M072/M073 bereits Phase 4).
|
||||
- [x] Plane-Ticket `AK-P4b` aktualisiert (Kommentar + Label `design-nachzug` gesetzt, Status auf Done).
|
||||
|
||||
---
|
||||
|
||||
@@ -236,3 +243,46 @@ DoD 21 Visual-Regression (keine Snapshot-Harness) · Davids `D-P4`-Optik · Push
|
||||
gegen echte Postgres verifiziert. Selbst-machbarer Rest auf dem Branch: **0** (alles Weitere ist
|
||||
extern/infra blockiert oder Reviewer-Freigabe). Gesamt **~95 %** — Ticket bleibt in **„Ready for
|
||||
Review"** bis Staging-Verifikation + Reviewer-Freigabe der deferrten Punkte.
|
||||
|
||||
---
|
||||
|
||||
### 2026-07-06 — Interim-UI aus `~/ui` gebaut (AD-28), Baseline-Tailwind abgelöst (Akeshan, Branch `akeshan/ak-p4b-interim-ui`)
|
||||
|
||||
Der Code-Kern (8/8 DoD) lag seit Juni auf `main` (Commit `4e3d12607` + Merge-Migration
|
||||
`20260624120000_p4b_tasks_entitylinks`); offen war nur die UI-Optik. Plane stand auf „Blocked / On Hold"
|
||||
(blocked-by `D-P4`) — das ist der reine Design-Wait, den AD-28 auflöst. Wie bei AK-P4a (Commit `33f0d01e5`,
|
||||
05.07.) die drei Tasks-Screens als **Interim-UI aus `~/ui`** neu gebaut, hinter der stabilen VM/Loader-Grenze.
|
||||
Backend/Loader/Action/VMs/Worker-Crons/Wiring **unverändert**.
|
||||
|
||||
**Gebaut:**
|
||||
- `apps/web/app/components/screens/tasks/`: `TasksListPage.tsx`, `TaskDetailPage.tsx`, `TaskNewPage.tsx`
|
||||
(+ je `.test.tsx`) und `DateField.tsx` (native Date-Input-Helfer). Komponiert aus `~/ui`-Components
|
||||
(`Table`, `Select`, `ToggleGroup`, `Checkbox`, `BadgeFeedback`, `Tag_Label`, `Breadcrumbs`, `Card`,
|
||||
`Dialog`, `Button`, `IconButton`, `TextInput`, `TextArea`, `EmptyState`). VM/`data` als Prop,
|
||||
kein `useLoaderData` im Screen, kein handgeschriebenes Tailwind für Spacing/Höhen (Ausnahme `DateField`,
|
||||
dokumentiert: `~/ui` hat keine form-fähige Date-Variante; mit Design-Tokens gestyled, Höhe 36/Radius 4).
|
||||
- Routen `tasks.{_index,$id,new}.tsx` rendern jetzt nur noch `<XPage vm={} actions={} />`. List-Route:
|
||||
`onApply` → `useNavigate({search})` (server-side Preset/Filter), plus `nextHref` im Loader (Cursor +
|
||||
aktive Filter erhalten). Neu-Route: Creator-Default-Assignee über Sentinel `__creator__` (das `~/ui`
|
||||
`Select` verbietet leere Option-Values), im Action zurück auf „kein expliziter Assignee" gemappt.
|
||||
- Detail-Löschen als echter `~/ui`-`Dialog` (Größe XS 420), nicht als Card-Fake (David-Standard §6).
|
||||
- i18n: neue `tasks.list.filter.*`, `tasks.list.pagination.next`, `tasks.detail.section.overview`,
|
||||
`tasks.detail.field.created`, `tasks.detail.delete.*`, `tasks.detail.dialog.cancel` in de **und** en
|
||||
(chirurgisch gespliced, kein JSON-Round-Trip; Parität grün).
|
||||
|
||||
**Verifikation (lokal grün):** `@hivegrid/web` typecheck exit 0 · Screen-Render-Tests **12/12**
|
||||
(`screens/tasks/*.test.tsx`) · Hot-Swap-Drill `taskList.hot-swap.test.ts` **3/3** · `verify:i18n` exit 0 ·
|
||||
`verify:sidebar-routes` 27/27 · `verify:cross-cutting-models` 178/178 · eslint-staged 0 Warnings.
|
||||
Marker `ui.tasks.list`/`.detail`/`.new` + `data-row-id`/`data-watcher-id`/`data-overdue` erhalten (E2E
|
||||
`tests/e2e/tasks.spec.ts` bleibt gültig).
|
||||
|
||||
**Offen / getrackter Nachzug:**
|
||||
- **Finale `D-P4`-Subframe-Optik** (David) — `design-nachzug: offen` (Doc-Tag + Plane-Label), Swap hinter
|
||||
der VM/Loader-Grenze, **nicht Done-blockierend** (AD-28).
|
||||
- Visual-Regression (DoD 21): Interim-Baseline (Harness deferred, Re-Baseline beim David-Swap).
|
||||
- Unverändert extern/deferred: Live-Staging Reminder/Archiv (Redis+notify) · RLS-Teil-B (P5-Cutover) ·
|
||||
Mobile-Tab-Bar (PWA-only AD-17).
|
||||
|
||||
**%-Done-Stand:** 8/8 DoD-Code-Punkte grün **+** AD-28-Interim-UI-Konformität hergestellt (Screens aus
|
||||
`~/ui`, Tests grün). Selbst-machbarer Rest: **0**. Gesamt **~97 %** — nur noch extern/Reviewer-Freigabe +
|
||||
Davids finale Optik (getrackter `design-nachzug`) offen.
|
||||
@@ -0,0 +1,86 @@
|
||||
# BILL-00 · Billing-Recut: Legacy-Provider-Annahmen durch Lago/Mollie ersetzen
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Typ** | Dev/Docs |
|
||||
| **Owner** | Raffael |
|
||||
| **Phase** | Billing-Recut nach AD-27 |
|
||||
| **Cycle/Fenster** | zuerst im Billing-Track |
|
||||
| **Status** | Umgesetzt / bereit fuer Review |
|
||||
|
||||
## Abhaengigkeiten & Parallelitaet
|
||||
|
||||
- **Blocked by:** AD-27 entschieden.
|
||||
- **Blocks:** alle weiteren `BILL-*`-Tickets.
|
||||
- **Parallelitaet:** keine Produktcode-Implementierung; Doku-/Planungs-Recut.
|
||||
|
||||
## Was wir bauen
|
||||
|
||||
Die bestehenden Legacy-Provider-basierten Billing-Docs wurden dependency-aware auf die neue Zielarchitektur gezogen:
|
||||
Lago fuer Billing/Invoicing, Mollie nur fuer Payments, HiveGrid fuer Entitlements. Vorhandene Arbeit aus `AK-P20`
|
||||
wird nicht verworfen, sondern als wiederverwendbare Billing-/Feature-Gate-/Read-only-Grundlage markiert.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [x] `AK-P20` beschreibt nicht mehr Legacy-Provider als Zielsystem, sondern Lago/Mollie-Recut.
|
||||
- [x] `D-P20`, `VA-P24a`, `VD-P24`, `AK-P19`, `AK-P21` sind an AD-27 angepasst oder mit klarem Recut-Hinweis versehen.
|
||||
- [x] `EXT-07`/`EXT-16` sind nicht mehr Legacy-Provider-Blocker; Mollie-Test-/Live-Setup ist als neue Setup-Arbeit beschrieben.
|
||||
- [x] `H-PAYLINK` ist als alter Legacy-Provider-Vertriebsweg ersetzt durch Mollie-/manuellen Zahlungsweg; `H-FOUND` verweist nicht mehr auf Mollie Payment Link.
|
||||
- [x] Offene Legacy-Provider-Codepfade sind inventarisiert: behalten, entfernen, migrieren oder deaktivieren.
|
||||
- [x] DoD-Stand der betroffenen Tickets bleibt nachvollziehbar; nichts wurde still auf Done gesetzt.
|
||||
|
||||
**Fortschritt:** `BILL-00`: 6/6 DoD = 100 % done. Ticket kann nach Review/Plane-Kommentar weitergeschoben werden.
|
||||
|
||||
## Code-Inventar fuer `BILL-02` bis `BILL-05`
|
||||
|
||||
**Migrieren auf Lago/Mollie/HiveGrid-Billing:**
|
||||
- Core-Subscription-Schema: Legacy-Provider-zentrierte Felder in `prisma/schema/subscriptions.prisma` (`legacyProviderId`,
|
||||
`legacyProviderCustomerId`, `legacyProviderSubscriptionId`, `legacyProviderSubscriptionItemId`, `Legacy-ProviderWebhookLog`).
|
||||
- DB-Service-Schicht: Legacy-Provider-spezifische Helfer und Typen in `packages/db/src/subscriptions.server.ts`.
|
||||
- Web-Billing-Adapter: `apps/web/app/utils/legacyProvider.server.ts`, `apps/web/app/routes/webhooks.legacyProvider.tsx`,
|
||||
`apps/api/src/billing/legacyProvider-webhook-route.ts` und die zugehoerigen Tests als Migrationsvorlage.
|
||||
- Dunning/Frozen-Vorarbeit: `billingDunningService` und `billingStatusService` behalten fachlich, aber Provider-
|
||||
Kopplung auf generische Billing-/Payment-Events umbauen.
|
||||
- Admin-/Settings-Routen: Subscription-/Invoice-/PaymentMethod-Komponenten und Admin-Subscription-Routen, die heute
|
||||
Legacy-Provider-Typen oder Legacy-Provider-IDs anzeigen.
|
||||
- Mail-Templates: Invoice/Subscription-Erfolgs-/Fehlertexte von Legacy-Provider-Bezug auf generische Billing-/Payment-Texte
|
||||
umstellen.
|
||||
|
||||
**Deaktivieren oder spaeter separat entscheiden:**
|
||||
- Portal/Sites-Legacy-Provider-Connect-Code in der Plugin-/Portal-Altlast. Das ist nicht HiveGrid-v1-Core-Billing und darf
|
||||
nicht in den neuen Billing-Pfad gezogen werden.
|
||||
- Legacy-Provider-Metering-/Usage-Dokumente und Ops-Runbooks. v1 hat kein Usage-Billing.
|
||||
|
||||
**Behalten als harmlose Beispiele/Test-Fixtures, bis ein eigener Cleanup folgt:**
|
||||
- Plugin-Runtime-Testdaten mit `legacyProvider_*` als generischem Secret-Key-Muster.
|
||||
- Alte Migrationshistorie, solange sie fuer bestehende DB-Reproduzierbarkeit noetig ist.
|
||||
|
||||
## Betroffene Planungsdocs
|
||||
|
||||
- `AK-P20`: alter Legacy-Provider-Abschluss ersetzt durch `BILL-02` bis `BILL-05`; AK-P20 bleibt Vorarbeit fuer Trial, Frozen,
|
||||
Onboarding, Demo-Daten, Checkliste, Theme und Entitlement-Gates.
|
||||
- `D-P20`: v1-Screens zeigen Plan/Seats, Lago-Invoices, Mollie-Payment-Status, kein Legacy-Provider-Portal.
|
||||
- `VA-P24a`/`VD-P24`: v2-Self-Service baut auf Lago/Mollie, nicht auf Legacy-Provider-Reuse.
|
||||
- `AK-P19`: Admin-Abo-Zusammenstellung = Entitlements + Billing-State; Legacy-Provider-Positionen ersetzt durch `BILL-06`.
|
||||
- `AK-P21`: Tenant-Löschung nutzt generischen Billing-Cancel-Vertrag, nicht Legacy-Provider-Cancel.
|
||||
- `EXT-07`/`EXT-16`: Mollie-Test-/Live-Setup + Lago-Staging/Prod statt Legacy-Provider.
|
||||
- `H-PAYLINK`/`H-FOUND`: Founding-Deal-Zahlungsweg ohne Legacy-Provider.
|
||||
|
||||
## Technische Bausteine
|
||||
|
||||
- Keine Produktcode-Aenderung in `BILL-00`.
|
||||
- Ergebnis ist ein sauberer Planungsstand fuer `BILL-01` bis `BILL-08`.
|
||||
|
||||
## Quellen
|
||||
|
||||
- `docs/ARCHITECTURE-DECISIONS.md` AD-15, AD-27
|
||||
- `.planning/ticket-docs/BILL-README.md`
|
||||
- `.planning/ticket-docs/AK-P20.md`
|
||||
- `.planning/ticket-docs/D-P20.md`
|
||||
- `.planning/ticket-docs/VA-P24a.md`, `.planning/ticket-docs/VD-P24.md`
|
||||
- `.planning/ticket-docs/EXT-07.md`, `.planning/ticket-docs/EXT-16.md`, `.planning/ticket-docs/H-PAYLINK.md`
|
||||
|
||||
## Umsetzungsnotiz
|
||||
|
||||
2026-07-04: Doku-Recut umgesetzt. `git diff --check` ist der passende Proof fuer dieses Docs-only-Ticket; Produktcode
|
||||
wurde bewusst nicht geaendert. Plane-Status/Kommentar stehen noch aus.
|
||||
@@ -0,0 +1,70 @@
|
||||
# BILL-01 · Lago-Staging sauber betreiben
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Typ** | Setup/Infra |
|
||||
| **Owner** | Raffael |
|
||||
| **Phase** | Billing-Recut nach AD-27 |
|
||||
| **Cycle/Fenster** | nach `BILL-00` |
|
||||
| **Status** | Umgesetzt / bereit fuer Review |
|
||||
|
||||
## Abhaengigkeiten & Parallelitaet
|
||||
|
||||
- **Blocked by:** `BILL-00` fuer endgueltige Doku-Namen; Pilot kann parallel validiert werden.
|
||||
- **Blocks:** `BILL-03`, `BILL-07`.
|
||||
|
||||
## Was wir bauen
|
||||
|
||||
Eine saubere Lago-Staging-Instanz als Coolify-Service im Estate. Sie ist fuer mehrere HiveGrid-Staging-Flows nutzbar,
|
||||
aber nicht mit Production gemischt.
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
- Coolify-Projekt: `node1-estate`
|
||||
- Resource: `lago-staging`
|
||||
- Resource UUID: `kh7l869ianltiioqgmm45kur`
|
||||
- Server: `node1`
|
||||
- Oeffentlicher Operator-Zugriff: `https://staging-lago.hivegrid.de` via Lago-Login
|
||||
- Lokaler SSH-Tunnel aktuell moeglich:
|
||||
- Frontend `http://127.0.0.1:8188`
|
||||
- API `http://127.0.0.1:3108`
|
||||
- Verifiziert 2026-07-04: API `GET /health` liefert `Success`; Frontend liefert `200 OK`.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [x] Lago laeuft als Coolify-Resource, nicht als manueller Raw-Compose-Stack.
|
||||
- [x] API, Frontend, DB, Redis, Worker, Billing-Worker und PDF-Service starten.
|
||||
- [x] Alte manuelle Pilot-Instanz entfernt.
|
||||
- [x] Zugriffsmodell festgelegt: Public Operator-UI via Lago-Login; API intern ueber Mesh, SSH-Tunnel als Fallback.
|
||||
- [x] Backup-Konzept fuer Lago-Postgres dokumentiert und Restore-Pfad definiert.
|
||||
- [x] SMTP/Mail-Verhalten fuer Staging entschieden: standardmaessig deaktiviert, Mailjet-Testconfig nur explizit fuer `BILL-07`.
|
||||
- [x] Dateispeicher fuer Invoice-PDFs entschieden: lokales Coolify-Volume fuer Staging-Pilot, Production spaeter separat.
|
||||
- [x] Runbook fuer Start/Stop/Upgrade/Restore in `docs/ops/lago-staging.md` dokumentiert.
|
||||
|
||||
**Fortschritt:** `BILL-01`: 8/8 DoD = 100 % done.
|
||||
|
||||
## Technische Bausteine
|
||||
|
||||
- Coolify Docker-Compose-Resource mit `getlago/api:v1.27.1`, `getlago/front:v1.27.1`, Postgres 15, Redis 7.
|
||||
- Keine Public-Domain ohne Auth.
|
||||
- Keine Production-Daten.
|
||||
- Runbook: `docs/ops/lago-staging.md`.
|
||||
|
||||
## Quellen
|
||||
|
||||
- `docs/ARCHITECTURE-DECISIONS.md` AD-27
|
||||
- Estate-Infra `../infra/runbooks/migrate-node1-to-coolify.md`
|
||||
- Lago Self-hosting Docs
|
||||
|
||||
## Umsetzungsnotiz
|
||||
|
||||
2026-07-04: Staging-Betrieb dokumentiert. Backup- und Restore-Pfad fuer Lago-Postgres, SMTP-Entscheidung,
|
||||
Invoice-PDF-Storage-Entscheidung, Upgrade/Rollback und Testdaten-Konvention stehen in `docs/ops/lago-staging.md`.
|
||||
|
||||
2026-07-05: Zugriffsmodell aktualisiert. Die Operator-UI ist unter `https://staging-lago.hivegrid.de` erreichbar
|
||||
und durch Lago-Login geschuetzt; die HiveGrid-zu-Lago-API bleibt intern ueber `http://10.42.0.4:3108`. Invoice-PDFs
|
||||
liegen im Hetzner-Bucket `hivegrid-staging-lago`; Kunden-Downloads laufen ueber HiveGrid.
|
||||
|
||||
Proof:
|
||||
- `docs/ops/lago-staging.md`
|
||||
- `git diff --check`
|
||||
Loaded 100 of 3781 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user