#!/usr/bin/env python3 """Phase-9: webhooks, scheduled-reports, 2fa-stub, billing-stub, integrations.""" from __future__ import annotations import asyncio import datetime import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from phase2_features import Feature, FileGen, ROOT, log, log_section # noqa: E402 from phase3_features import run_feature_v2 # noqa: E402 PHASE9_STATE = ROOT / ".phase9-state.json" FEATURES: list[Feature] = [ Feature( name="webhooks-config", description="Outgoing-Webhooks Tabelle + CRUD + UI", files=[ FileGen( path="apps/api/src/db/schema.ts", purpose=( "ERWEITERT — füge `webhooks` (pgTable 'webhooks'): " "id (uuid pk default random), url (text notnull), event (text notnull, e.g. 'time_entry.created'), " "active (boolean default true), createdAt (timestamp default now), createdBy (uuid references users id)." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/api/src/routes/webhooks.ts", purpose=( "Fastify-Plugin /api/webhooks. Admin-only (preHandler checks role). " "CRUD: GET / (list), POST /, PATCH /:id, DELETE /:id, POST /:id/test (sendet test-payload)." ), refs=["apps/api/src/routes/users.ts"], ), FileGen( path="apps/web/src/pages/Webhooks.tsx", purpose=( "Webhooks-Page (admin-only). Liste + Create-Form (url, event-select). " "Test-Button pro Webhook. Verwende api.listWebhooks, api.createWebhook, api.deleteWebhook, api.testWebhook." ), refs=["apps/web/src/pages/Customers.tsx"], ), ], ), Feature( name="two-factor-auth-stub", description="2FA-Setup-Page (TOTP-Stub, kein realer verify yet)", files=[ FileGen( path="apps/web/src/pages/TwoFactorAuth.tsx", purpose=( "2FA-Setup-Page. Zeigt fake QR-Code-Placeholder (SVG-Box mit 'QR-Code here'), " "Secret-String (random base32, useState), Input für 6-stelligen Code. " "Submit: stub, zeigt Toast 'TOTP-Setup-MVP — Verifikation kommt in v2'. " "Tailwind, max-w-md." ), refs=["apps/web/src/pages/Profile.tsx"], ), ], ), Feature( name="billing-stub", description="Plans-Page mit Pricing-Tiers (UI only, kein Stripe)", files=[ FileGen( path="apps/web/src/pages/Billing.tsx", purpose=( "Billing/Plans-Page. 3 Pricing-Cards: 'Free' (0€, 1 User, 100 entries/Monat), " "'Team' (12€/User/Monat, unbegrenzte entries, Customers + Projects), " "'Enterprise' (custom, SSO, audit-log, priority support). " "Aktueller Plan: Free badge oben. Upgrade-Button → Toast 'Stripe-Integration kommt in v2'. " "Tailwind: grid-cols-3, hover-scale auf Cards." ), refs=["apps/web/src/pages/AdminUsers.tsx"], ), ], ), Feature( name="integrations-page", description="Integrations-Page mit Slack/Discord/Webhook-Cards", files=[ FileGen( path="apps/web/src/pages/Integrations.tsx", purpose=( "Integrations-Page. Grid mit Karten für: Slack, Discord, Webhooks, Calendar (Google), Email. " "Pro Karte: Icon (lucide-react), Name, kurze Beschreibung, 'Configure'-Button. " "Slack/Discord/Calendar: 'Coming Soon' Badge + disabled Button. " "Webhooks: links zu /webhooks. Email: links zu /settings. " "Tailwind cards, hover-shadow." ), ), ], ), Feature( name="api-client-phase9", description="API um Webhooks endpoints erweitert", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "ERWEITERT — behalte ALLES. Füge: " "listWebhooks(), createWebhook({url, event}), updateWebhook(id, data), deleteWebhook(id), testWebhook(id)." ), refs=["apps/web/src/lib/api.ts"], ), ], ), Feature( name="router-phase9", description="App + routes/index für phase9 Routes", files=[ FileGen( path="apps/api/src/routes/index.ts", purpose="ERWEITERT — füge webhookRoutes ('/api/webhooks'). Behalte alle bestehenden.", refs=["apps/api/src/routes/index.ts"], ), FileGen( path="apps/web/src/App.tsx", purpose=( "ERWEITERT — füge /webhooks (admin), /2fa, /billing, /integrations Routes. Behalte alles." ), refs=["apps/web/src/App.tsx"], ), FileGen( path="apps/web/src/components/Nav.tsx", purpose=( "ERWEITERT — füge Integrations-Link (alle Users) + Webhooks-Link (admin) + Billing-Link (alle). " "2FA-Link nur in Profile-Page (separate Section). Behalte alle bestehenden Links." ), refs=["apps/web/src/components/Nav.tsx"], ), ], ), ] def load_state() -> dict: if PHASE9_STATE.exists(): return json.loads(PHASE9_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state: dict) -> None: PHASE9_STATE.write_text(json.dumps(state, indent=2)) async def main() -> int: log_section("🚀 Phase-9 Codegen-Run gestartet") state = load_state() for feature in FEATURES: if feature.name in state.get("completed_features", []): continue state["current_feature"] = feature.name; save_state(state) try: success = await run_feature_v2(feature) if success: state.setdefault("completed_features", []).append(feature.name) else: state.setdefault("attempted_features", []).append(feature.name) save_state(state) except Exception as e: log(f"❌ {feature.name} crashed: {e}", level="ERROR") state.setdefault("attempted_features", []).append(feature.name); save_state(state) log_section("Phase-9 Run beendet") log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}") import subprocess log("Running db:generate + db:migrate…") r = subprocess.run(["pnpm", "--filter", "api", "db:generate"], cwd=ROOT, capture_output=True, text=True, timeout=60) log(f" db:generate rc={r.returncode}: {r.stdout[-200:]}") r = subprocess.run(["pnpm", "--filter", "api", "db:migrate"], cwd=ROOT, capture_output=True, text=True, timeout=60) log(f" db:migrate rc={r.returncode}: {r.stdout[-200:]}") return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))