#!/usr/bin/env python3 """Phase-4: admin-page, CSV export, mobile-fix, error-boundary, dashboard-charts.""" from __future__ import annotations import asyncio import datetime import json import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from phase2_features import ( # noqa: E402 Feature, FileGen, ROOT, log, log_section, git, gemma, strip_codefence, load_existing, tsc_check, MAX_RETRIES, ) from phase3_features import HARDENING_HINTS, generate_with_hints, run_feature_v2 # noqa: E402 PHASE4_STATE = ROOT / ".phase4-state.json" FEATURES: list[Feature] = [ Feature( name="admin-user-management", description="Admin-only CRUD /api/users + Settings-Page für User-Verwaltung", files=[ FileGen( path="apps/api/src/routes/users.ts", purpose=( "ERWEITERTE users.ts — behalte GET /me + PATCH /me. " "Füge hinzu (alle nur für role='admin'): " "GET / (list all users), POST / (create with email+name+role+password — argon2-hash), " "PATCH /:id (update name, role), DELETE /:id (cascade time-entries). " "Verwende `request.user as { sub: string, role: string }` für payload-access. " "403 wenn role !== 'admin'." ), refs=["apps/api/src/routes/users.ts", "apps/api/src/db/schema.ts"], ), FileGen( path="apps/web/src/pages/AdminUsers.tsx", purpose=( "Admin-User-Management Page. Liste aller User (TanStack Query). " "Inline-Form zum Anlegen (email, name, role-select, password). " "Edit-Button pro Row → Modal mit name/role-Update. Delete-Button mit confirm. " "Verwende api.listUsers(), api.createUser(), api.updateUser(), api.deleteUser(). " "Wenn current user role !== 'admin': zeige Forbidden-Message statt Inhalt." ), refs=["apps/web/src/pages/Customers.tsx", "apps/web/src/lib/api.ts"], ), ], ), Feature( name="csv-export-time-entries", description="CSV-Export-Endpoint + Button in TimeEntries-Page", files=[ FileGen( path="apps/api/src/routes/time-entries.ts", purpose=( "ERWEITERTE time-entries.ts — behalte alle bestehenden Routes (CRUD + running/start/stop). " "Füge hinzu: GET /export.csv?from=...&to=... → returnt text/csv content mit Spalten " "id,description,projectId,startTime,endTime,durationMinutes. " "Header content-type: text/csv, content-disposition: attachment; filename=time-entries.csv. " "Auth erforderlich, user sieht nur eigene (außer admin)." ), refs=["apps/api/src/routes/time-entries.ts"], ), FileGen( path="apps/web/src/pages/TimeEntries.tsx", purpose=( "ERWEITERT — behalte Form + Filter + Liste. Füge Export-Button im Filter-Bar: " "ruft `window.location.href = \\'/api/time-entries/export.csv?from=...&to=...\\'` " "mit aktuellen Filter-Werten. Stil: outline-Button rechts neben To-Date." ), refs=["apps/web/src/pages/TimeEntries.tsx"], ), ], ), Feature( name="error-boundary", description="React ErrorBoundary + global wrapping in App.tsx", files=[ FileGen( path="apps/web/src/components/ErrorBoundary.tsx", purpose=( "React-ErrorBoundary class-component. Fängt unkaufgefangene Render-Errors. " "Zeigt schöne Fehlerseite mit message + reload-button. " "Verwende componentDidCatch + getDerivedStateFromError. " "Tailwind, centered, max-w-md." ), ), FileGen( path="apps/web/src/App.tsx", purpose=( "ERWEITERT — wrap RouterProvider in . Behalte ToastProvider + alle Routes. " "Importiere ErrorBoundary von ./components/ErrorBoundary." ), refs=["apps/web/src/App.tsx"], ), ], ), Feature( name="dashboard-charts", description="Dashboard mit Stunden-Chart (recharts)", files=[ FileGen( path="apps/web/src/pages/Dashboard.tsx", purpose=( "ÜBERARBEITETER Dashboard. Behalte die 3 Karten (Heute/Woche/Aktive Projekte). " "Füge unten ein BarChart hinzu (recharts BarChart + Bar) mit den letzten 7 Tagen — " "x-axis: dayName, y-axis: hours. Daten aus listTimeEntries letzte 7 Tage, " "group-by-day per useMemo. Card-Wrapper mit Tailwind." ), refs=["apps/web/src/pages/Dashboard.tsx"], ), ], ), Feature( name="api-client-phase4", description="API-Client um Admin-User + Export-URL ergänzt", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "FINAL+ - behalte ALLES aus vorher. Füge hinzu: " "listUsers(), createUser({email,name,role,password}), updateUser(id, {name,role}), deleteUser(id), " "Alle Admin-Endpoints → /api/users." ), refs=["apps/web/src/lib/api.ts"], ), ], ), Feature( name="router-with-admin", description="App.tsx +/admin route + Nav admin-link bei admin-role", files=[ FileGen( path="apps/web/src/App.tsx", purpose=( "ERWEITERT — füge Route /admin (AdminUsers component) hinzu. Auth-Check: zusätzlich role='admin' (sonst redirect /). " "Behalte ErrorBoundary + ToastProvider + alle Routes." ), refs=["apps/web/src/App.tsx", "apps/web/src/pages/AdminUsers.tsx"], ), FileGen( path="apps/web/src/components/Nav.tsx", purpose=( "ERWEITERT — Nav zeigt Admin-Link nur wenn current user role='admin'. " "useQuery(['me'], api.getMe) → wenn data.role==='admin', render Admin. " "Behalte alle anderen Links + Logout." ), refs=["apps/web/src/components/Nav.tsx"], ), ], ), ] def load_state() -> dict: if PHASE4_STATE.exists(): return json.loads(PHASE4_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state: dict) -> None: PHASE4_STATE.write_text(json.dumps(state, indent=2)) async def main() -> int: log_section(f"🚀 Phase-4 Codegen-Run gestartet") log(f"Features: {len(FEATURES)}") # ensure recharts is installed for dashboard-charts log("Ensuring recharts dep…") import subprocess r = subprocess.run(["pnpm", "--filter", "web", "add", "recharts"], cwd=ROOT, capture_output=True, text=True, timeout=120) log(f" recharts install rc={r.returncode}") state = load_state() for feature in FEATURES: if feature.name in state.get("completed_features", []): log(f"⏭ Skip {feature.name}") 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 {feature.name} crashed: {e}", level="ERROR") state.setdefault("attempted_features", []).append(feature.name) save_state(state) log_section("Phase-4 Run beendet") log(f"OK: {len(state.get('completed_features', []))}, " f"Attempted: {len(state.get('attempted_features', []))}, " f"Total: {len(FEATURES)}") return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))