208 lines
8.6 KiB
Python
208 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Phase-5: dark-mode, settings, calendar, customer/project details."""
|
|
|
|
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
|
|
|
|
|
|
PHASE5_STATE = ROOT / ".phase5-state.json"
|
|
|
|
|
|
FEATURES: list[Feature] = [
|
|
Feature(
|
|
name="dark-mode-toggle",
|
|
description="Dark-Mode mit System-Preference + localStorage + toggle",
|
|
files=[
|
|
FileGen(
|
|
path="apps/web/src/lib/theme.ts",
|
|
purpose=(
|
|
"Theme-Hook + Util. useTheme() returns {theme:'light'|'dark', setTheme, toggle}. "
|
|
"Persist in localStorage 'theme'. Initial: System-pref via matchMedia. "
|
|
"Side-effect: setzt document.documentElement.classList toggle('dark')."
|
|
),
|
|
),
|
|
FileGen(
|
|
path="apps/web/tailwind.config.ts",
|
|
purpose=(
|
|
"ERWEITERT — füge `darkMode: 'class'` hinzu. Behalte content + theme.extend.colors.ember."
|
|
),
|
|
refs=["apps/web/tailwind.config.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/components/Nav.tsx",
|
|
purpose=(
|
|
"ERWEITERT — füge Theme-Toggle-Button rechts (Sun/Moon Icon von lucide-react, useTheme hook). "
|
|
"Behalte alle bestehenden Links + Logout + Profile + Admin (bei admin-role)."
|
|
),
|
|
refs=["apps/web/src/components/Nav.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="customer-detail-page",
|
|
description="Customer-Detail: zeigt Projekte + letzte Time-Entries des Kunden",
|
|
files=[
|
|
FileGen(
|
|
path="apps/api/src/routes/customers.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte CRUD. Füge GET /:id/projects (alle Projekte zum Kunden) und "
|
|
"GET /:id/time-entries (alle TimeEntries deren projectId zu einem Projekt dieses Kunden gehört, "
|
|
"letzte 50 sortiert by startTime desc)."
|
|
),
|
|
refs=["apps/api/src/routes/customers.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/pages/CustomerDetail.tsx",
|
|
purpose=(
|
|
"CustomerDetail-Page. Liest customerId aus URL-Param. Zeigt: Customer-Header (name, status). "
|
|
"Section: Projekte (Liste, Klick → /projects). Section: Letzte 20 TimeEntries (kompakte Tabelle). "
|
|
"Verwende api.getCustomerProjects(id), api.getCustomerTimeEntries(id)."
|
|
),
|
|
refs=["apps/web/src/pages/Customers.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="project-detail-page",
|
|
description="Project-Detail: zeigt Customer + alle TimeEntries des Projekts",
|
|
files=[
|
|
FileGen(
|
|
path="apps/web/src/pages/ProjectDetail.tsx",
|
|
purpose=(
|
|
"ProjectDetail-Page. Header: name + linked customer. Section: TimeEntries für dieses Projekt "
|
|
"(alle, mit Gesamt-Stunden-Summary). Verwende api.getProject(id), api.listTimeEntries({projectId:id})."
|
|
),
|
|
refs=["apps/web/src/pages/Projects.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="settings-page",
|
|
description="App-Settings (workspace name, default-billable, etc.)",
|
|
files=[
|
|
FileGen(
|
|
path="apps/api/src/db/schema.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte alle bestehenden Tabellen. Füge neue Tabelle `appSettings` (pgTable 'app_settings'): "
|
|
"id (singleton uuid, primary), workspaceName (text default 'EmberClone'), defaultBillable (boolean default true), "
|
|
"weekStart (integer default 1 für Monday), updatedAt (timestamp.notNull().defaultNow())."
|
|
),
|
|
refs=["apps/api/src/db/schema.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/api/src/routes/settings.ts",
|
|
purpose=(
|
|
"Fastify-Plugin /api/settings. GET / (current settings, lazy-init if none exist). "
|
|
"PATCH / (admin-only update). Auth required, role-check für PATCH."
|
|
),
|
|
refs=["apps/api/src/routes/users.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/pages/Settings.tsx",
|
|
purpose=(
|
|
"Settings-Page. Form mit workspaceName, defaultBillable (checkbox), weekStart (select Mon/Sun). "
|
|
"Admin-only sichtbar (sonst Forbidden). Submit → api.updateSettings()."
|
|
),
|
|
refs=["apps/web/src/pages/Profile.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="api-client-phase5",
|
|
description="API um customer-detail, project-detail, settings, theme erweitern",
|
|
files=[
|
|
FileGen(
|
|
path="apps/web/src/lib/api.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte ALLES. Füge: getCustomerProjects(id), getCustomerTimeEntries(id), "
|
|
"getProject(id), getSettings(), updateSettings({workspaceName?, defaultBillable?, weekStart?})."
|
|
),
|
|
refs=["apps/web/src/lib/api.ts"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="router-phase5",
|
|
description="App.tsx + Nav um neue Routen erweitern + db-migrate nicht vergessen",
|
|
files=[
|
|
FileGen(
|
|
path="apps/api/src/routes/index.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte alle bestehenden registrations. Füge `settingsRoutes` mit prefix '/api/settings' hinzu."
|
|
),
|
|
refs=["apps/api/src/routes/index.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/App.tsx",
|
|
purpose=(
|
|
"ERWEITERT — füge Routes /customers/$id (CustomerDetail), /projects/$id (ProjectDetail), "
|
|
"/settings (Settings, admin-only). Behalte ErrorBoundary + ToastProvider + alle bestehenden Routes."
|
|
),
|
|
refs=["apps/web/src/App.tsx"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/components/Nav.tsx",
|
|
purpose=(
|
|
"ERWEITERT — füge Settings-Link bei admin-role + Theme-Toggle. Behalte alle bestehenden Links."
|
|
),
|
|
refs=["apps/web/src/components/Nav.tsx"],
|
|
),
|
|
],
|
|
),
|
|
]
|
|
|
|
|
|
def load_state() -> dict:
|
|
if PHASE5_STATE.exists():
|
|
return json.loads(PHASE5_STATE.read_text())
|
|
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
|
|
|
|
|
|
def save_state(state: dict) -> None:
|
|
PHASE5_STATE.write_text(json.dumps(state, indent=2))
|
|
|
|
|
|
async def main() -> int:
|
|
log_section(f"🚀 Phase-5 Codegen-Run gestartet")
|
|
log(f"Features: {len(FEATURES)}")
|
|
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-5 Run beendet")
|
|
log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}")
|
|
|
|
# auto-run db:generate + db:migrate for schema-changes (settings table)
|
|
import subprocess
|
|
log("Running db:generate + db:migrate for schema changes…")
|
|
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[-300:]}")
|
|
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[-300:]}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(asyncio.run(main()))
|