#!/usr/bin/env python3 """Phase-10: markdown-notes, customer-tags, project-templates, language-toggle, keyboard-help.""" 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 PHASE10_STATE = ROOT / ".phase10-state.json" FEATURES: list[Feature] = [ Feature( name="markdown-notes-time-entry", description="Markdown-Notes-Feld pro Time-Entry + Render in Liste", files=[ FileGen( path="apps/api/src/db/schema.ts", purpose=( "ERWEITERT — füge `notes: text('notes')` Spalte (nullable) zu time_entries Tabelle. " "Behalte alle bestehenden Spalten + Tabellen." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/web/src/pages/TimeEntries.tsx", purpose=( "ERWEITERT — behalte alles. Füge `notes` Textarea (optional, expandable) zum Create-Form. " "In der Liste: kleines Expand-Icon pro row, beim Klick zeigt notes als Markdown gerendert " "(simple: nur \\n→
, **bold** → , *italic* → ; oder benutze 'marked' npm dep wenn installiert)." ), refs=["apps/web/src/pages/TimeEntries.tsx"], ), ], ), Feature( name="customer-tags", description="Tags-Feld bei Customers + Filter-by-Tag", files=[ FileGen( path="apps/api/src/db/schema.ts", purpose=( "ERWEITERT — füge `tags: text('tags').array().notNull().default([])` Spalte zu customers. " "Behalte alles." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/web/src/pages/Customers.tsx", purpose=( "ERWEITERT — füge Tags-Input (kommagetrennt) zum Create-Form. " "Zeige Tags als Chips in der Tabelle. Filter oben: 'Filter by tag…' input, " "filtert client-side die Liste auf Customers wo tag.includes(filter)." ), refs=["apps/web/src/pages/Customers.tsx"], ), ], ), Feature( name="project-templates", description="Wiederverwendbare Project-Templates (admin)", files=[ FileGen( path="apps/api/src/db/schema.ts", purpose=( "ERWEITERT — füge `projectTemplates` Tabelle: id (uuid pk), name (text), defaultBillable (boolean), " "estimatedHours (integer nullable), createdAt (timestamp). Behalte alles." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/api/src/routes/project-templates.ts", purpose=( "Fastify-Plugin /api/project-templates. CRUD, admin-only via preHandler+role-check. " "GET/POST/PATCH/DELETE." ), refs=["apps/api/src/routes/customers.ts"], ), FileGen( path="apps/web/src/pages/ProjectTemplates.tsx", purpose=( "ProjectTemplates-Page (admin-only). Liste + Create-Form (name, defaultBillable checkbox, estimatedHours number). " "Beim Project-Create (in Projects.tsx) optional: 'aus Template'-Dropdown — wenn gewählt, pre-fillen." ), refs=["apps/web/src/pages/Customers.tsx"], ), ], ), Feature( name="language-toggle", description="i18n-Stub mit DE/EN-Toggle (localStorage)", files=[ FileGen( path="apps/web/src/lib/i18n.tsx", purpose=( "Mini-i18n. useTranslation()-Hook: returns {t, setLang, lang}. " "Dictionary inline mit ~20 wichtigsten Keys ('login', 'dashboard', 'customers', 'projects', " "'time_entries', 'logout', 'save', 'delete', etc.) in DE+EN. " "Persist in localStorage 'lang'." ), ), FileGen( path="apps/web/src/components/Nav.tsx", purpose=( "ERWEITERT — füge Sprach-Toggle (DE/EN Button) rechts neben Theme-Toggle. " "Behalte alle bestehenden Links." ), refs=["apps/web/src/components/Nav.tsx"], ), ], ), Feature( name="keyboard-help-modal", description="Help-Modal mit Keyboard-Shortcuts (?-Hotkey)", files=[ FileGen( path="apps/web/src/components/KeyboardHelp.tsx", purpose=( "Help-Modal. Triggered by '?'-Taste (window-keydown). Liste der Shortcuts: " "Cmd+K = Command Palette, ? = Diese Hilfe, T = Theme-Toggle, G then D = Dashboard, " "G then C = Customers, G then P = Projects, G then T = TimeEntries. " "Escape schließt. Tailwind centered modal mit kbd-styled keys." ), ), FileGen( path="apps/web/src/App.tsx", purpose="ERWEITERT — mounte global im Root-Route. Behalte alles.", refs=["apps/web/src/App.tsx"], ), ], ), Feature( name="api-client-phase10", description="API um project-templates erweitern", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "ERWEITERT — behalte ALLES. Füge: listProjectTemplates(), createProjectTemplate(data), " "updateProjectTemplate(id, data), deleteProjectTemplate(id)." ), refs=["apps/web/src/lib/api.ts"], ), ], ), Feature( name="router-phase10", description="App + routes/index für /project-templates", files=[ FileGen( path="apps/api/src/routes/index.ts", purpose="ERWEITERT — füge projectTemplateRoutes ('/api/project-templates').", refs=["apps/api/src/routes/index.ts"], ), FileGen( path="apps/web/src/App.tsx", purpose="ERWEITERT — füge /project-templates (admin-only). Behalte alles.", refs=["apps/web/src/App.tsx"], ), ], ), ] def load_state() -> dict: if PHASE10_STATE.exists(): return json.loads(PHASE10_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state: dict) -> None: PHASE10_STATE.write_text(json.dumps(state, indent=2)) async def main() -> int: log_section("🚀 Phase-10 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-10 Run beendet") log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}") import subprocess 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()))