#!/usr/bin/env python3 """Phase-23: workspace-logo, custom-themes, command-actions, drag-widgets, animations.""" from __future__ import annotations import asyncio, datetime, json, sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from phase2_features import Feature, FileGen, ROOT, log, log_section from phase3_features import run_feature_v2 PHASE_STATE = ROOT / ".phase23-state.json" FEATURES: list[Feature] = [ Feature( name="workspace-logo", description="Logo-Feld auf appSettings + Anzeige in Nav", files=[FileGen( path="apps/api/src/db/schema.ts", purpose=( "WICHTIG: BEHALTE ALLE existierenden Tabellen/Spalten. " "Füge nur Spalte `logoUrl: text('logo_url')` (nullable) zu appSettings. " "BEHALTE: users, customers, projects (mit icon, budgetHours, pinnedAt), projectTemplates, timeEntries (notes, externalLink), " "timeEntryAttachments, timeEntryComments, appSettings (workspaceName, defaultBillable, weekStart, roundingMinutes), " "auditLog, documents, webhooks, savedViews, apiKeys, passwordResetTokens, invitations." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/web/src/components/Nav.tsx", purpose=( "ERWEITERT — wenn appSettings.logoUrl set, zeige Logo links statt 'EmberClone'-Text. " "Behalte alle bestehenden Nav-Links." ), refs=["apps/web/src/components/Nav.tsx"], )], ), Feature( name="custom-themes", description="3 Color-Themes wählbar (Ember/Ocean/Forest)", files=[FileGen( path="apps/web/src/lib/theme.tsx", purpose=( "ERWEITERT — behalte light/dark toggle. Füge zusätzlich color-theme: 'ember' | 'ocean' | 'forest'. " "Persist in localStorage 'colorTheme'. Setzt document.documentElement.dataset.colorTheme. " "useTheme() returns auch {colorTheme, setColorTheme}." ), refs=["apps/web/src/lib/theme.tsx"], ), FileGen( path="apps/web/src/index.css", purpose=( "ERWEITERT — behalte alles. Füge CSS-vars für color-themes: " "html[data-color-theme='ember'] { --primary: #f97316 } " "html[data-color-theme='ocean'] { --primary: #0ea5e9 } " "html[data-color-theme='forest'] { --primary: #10b981 }" ), refs=["apps/web/src/index.css"], )], ), Feature( name="command-bar-actions", description="CommandPalette mit Aktionen (z.B. 'New TimeEntry', 'Toggle Dark')", files=[FileGen( path="apps/web/src/components/CommandPalette.tsx", purpose=( "ERWEITERT — behalte bestehende Navigation-Items. Füge actions section: " "'Neuer Time-Entry' (öffnet QuickAdd), 'Dark/Light umschalten', 'Theme wechseln', 'Logout'. " "Fuzzy-Filter auch über actions." ), refs=["apps/web/src/components/CommandPalette.tsx"], )], ), Feature( name="animated-transitions", description="Page-Transitions mit fade-in beim Route-Change", files=[FileGen( path="apps/web/src/index.css", purpose=( "ERWEITERT — füge fade-in animation utility: " "@keyframes fade-in { from {opacity:0; transform:translateY(4px)} to {opacity:1; transform:translateY(0)} } " ".page-enter { animation: fade-in 200ms ease-out }. Behalte alles." ), refs=["apps/web/src/index.css"], ), FileGen( path="apps/web/src/App.tsx", purpose=( "ERWEITERT — wrap in
. " "Force-rerender bei Route-Change. Behalte alles." ), refs=["apps/web/src/App.tsx"], )], ), Feature( name="drag-resize-widgets", description="Dashboard-Widgets: resizable via drag-Handle (CSS-resize)", files=[FileGen( path="apps/web/src/pages/Dashboard.tsx", purpose=( "ERWEITERT — behalte alles. Jede Widget-Card bekommt `resize: both; overflow: auto; min-h-64` Style. " "User kann unten-rechts ziehen." ), refs=["apps/web/src/pages/Dashboard.tsx"], )], ), ] def load_state(): if PHASE_STATE.exists(): return json.loads(PHASE_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state): PHASE_STATE.write_text(json.dumps(state, indent=2)) async def main(): log_section("🚀 Phase-23 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) state.setdefault("completed_features" if success else "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-23 Run beendet") log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}") return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))