#!/usr/bin/env python3 """Phase-6: file-upload, password-change, audit-log, keyboard-shortcuts, calendar-week.""" 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 PHASE6_STATE = ROOT / ".phase6-state.json" FEATURES: list[Feature] = [ Feature( name="password-change", description="Change-Password Endpoint + Form in Profile", files=[ FileGen( path="apps/api/src/routes/users.ts", purpose=( "ERWEITERT — behalte alles. Füge POST /me/password (body: {oldPassword, newPassword}). " "Argon2.verify alten, dann argon2.hash neuen + db.update. 401 wenn alt nicht stimmt." ), refs=["apps/api/src/routes/users.ts"], ), FileGen( path="apps/web/src/pages/Profile.tsx", purpose=( "ERWEITERT — behalte bestehendes Form (Name update). Füge zweite Card 'Passwort ändern': " "Inputs altes Passwort + neues Passwort + Bestätigung. Submit → api.changePassword(). Toast feedback." ), refs=["apps/web/src/pages/Profile.tsx"], ), ], ), Feature( name="audit-log", description="Audit-Log Tabelle + Page (admin-only)", files=[ FileGen( path="apps/api/src/db/schema.ts", purpose=( "ERWEITERT — behalte alle Tabellen. Füge `auditLog` (pgTable 'audit_log'): " "id (uuid pk default random), userId (uuid references users id), " "action (text notnull, e.g. 'create:customer'), resourceType (text), resourceId (text nullable), " "metadata (text nullable JSON), createdAt (timestamp default now)." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/api/src/routes/audit-log.ts", purpose=( "Fastify-Plugin /api/audit-log. GET / (admin only, returns last 100 entries desc by createdAt). " "Auth via fastify.addHook preHandler." ), refs=["apps/api/src/routes/users.ts"], ), FileGen( path="apps/web/src/pages/AuditLog.tsx", purpose=( "AuditLog-Page (admin-only). Tabelle: When / User / Action / Resource. " "useQuery api.listAuditLog(). Empty/Loading states." ), refs=["apps/web/src/pages/AdminUsers.tsx"], ), ], ), Feature( name="calendar-week-view", description="Wochen-Kalender für Time-Entries", files=[ FileGen( path="apps/web/src/pages/Calendar.tsx", purpose=( "Calendar-Page mit Week-View. 7-Spalten-Grid (Mon-Sun mit aktueller Woche). " "Vor/zurück-Buttons für Wochen-Navigation. " "useQuery api.listTimeEntries({from: weekStart, to: weekEnd}). " "Pro Tag: Liste der Einträge mit Zeit + Description + Gesamt-Stunden des Tages oben. " "Tailwind grid-cols-7." ), refs=["apps/web/src/lib/api.ts"], ), ], ), Feature( name="keyboard-shortcuts", description="Cmd/Ctrl-K Command-Palette für Navigation", files=[ FileGen( path="apps/web/src/components/CommandPalette.tsx", purpose=( "Command-Palette Modal. Trigger: Cmd/Ctrl+K via window-keydown. " "Zeigt Liste navigierbarer Items (Dashboard, TimeEntries, Customers, Projects, Calendar, Settings, Profile). " "Fuzzy-Filter per Search-Input. Enter navigiert. Escape schließt. " "Tailwind: fixed inset-0 bg-black/50 + centered card." ), ), ], ), Feature( name="api-client-phase6", description="API um password + audit-log erweitert", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "ERWEITERT — behalte ALLES. Füge: changePassword({oldPassword, newPassword}), listAuditLog()." ), refs=["apps/web/src/lib/api.ts"], ), ], ), Feature( name="router-phase6", description="App.tsx + Nav + routes/index für phase6", files=[ FileGen( path="apps/api/src/routes/index.ts", purpose=( "ERWEITERT — behalte alle registrations. Füge auditLogRoutes mit prefix '/api/audit-log'." ), refs=["apps/api/src/routes/index.ts"], ), FileGen( path="apps/web/src/App.tsx", purpose=( "ERWEITERT — füge /calendar (Calendar), /audit-log (AuditLog admin-only) hinzu. " "Wrap CommandPalette global (mount once at root). Behalte alles bestehende." ), refs=["apps/web/src/App.tsx"], ), FileGen( path="apps/web/src/components/Nav.tsx", purpose=( "ERWEITERT — Calendar-Link, Audit-Log-Link bei admin, kleines '⌘K' Hint rechts vom Logo." ), refs=["apps/web/src/components/Nav.tsx"], ), ], ), ] def load_state() -> dict: if PHASE6_STATE.exists(): return json.loads(PHASE6_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state: dict) -> None: PHASE6_STATE.write_text(json.dumps(state, indent=2)) async def main() -> int: log_section("🚀 Phase-6 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-6 Run beendet") log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}") # auto-migrate for audit_log table 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()))