209 lines
8.5 KiB
Python
209 lines
8.5 KiB
Python
#!/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 <ErrorBoundary>. 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 <Link to='/admin'>Admin</Link>. "
|
|
"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()))
|