192 lines
7.5 KiB
Python
192 lines
7.5 KiB
Python
#!/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()))
|