EmberClone/scripts/phase18_features.py

184 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Phase-18: invoice-pdf-real, api-key-management, audit-log-filters, idle-detection, time-entry-comments."""
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
PHASE18_STATE = ROOT / ".phase18-state.json"
FEATURES: list[Feature] = [
Feature(
name="api-key-management",
description="API-Keys für users (für REST-Programmzugriff)",
files=[
FileGen(
path="apps/api/src/db/schema.ts",
purpose=(
"WICHTIG: behalte alle bestehenden Tabellen — füge nur `apiKeys` neu hinzu. "
"Behalte vor allem `passwordResetTokens`. "
"Neue Tabelle: apiKeys (id, userId references users, name, keyHash, createdAt, lastUsedAt nullable, revokedAt nullable)."
),
refs=["apps/api/src/db/schema.ts"],
),
FileGen(
path="apps/api/src/routes/api-keys.ts",
purpose=(
"Fastify-Plugin /api/api-keys. Auth required. "
"GET / (list user's keys, ohne keyHash), POST / (generate random key, return plaintext einmalig, store hash), "
"DELETE /:id (revoke = set revokedAt). Use FastifyInstance."
),
refs=["apps/api/src/routes/users.ts"],
),
FileGen(
path="apps/web/src/pages/ApiKeys.tsx",
purpose=(
"ApiKeys-Page. Liste der eigenen Keys (name, lastUsed, status). "
"Create-Form mit name. Bei create: Modal zeigt key PLAINTEXT einmal (Copy-Button)."
),
refs=["apps/web/src/pages/Customers.tsx"],
),
],
),
Feature(
name="audit-log-filters",
description="Audit-Log mit Filter (user, action, date-range)",
files=[
FileGen(
path="apps/api/src/routes/audit-log.ts",
purpose=(
"ERWEITERT — behalte GET /. Füge Query-Params ?userId=, ?action=, ?from=, ?to=. "
"WHERE clauses mit drizzle and(). Behalte admin-only-Logik."
),
refs=["apps/api/src/routes/audit-log.ts"],
),
FileGen(
path="apps/web/src/pages/AuditLog.tsx",
purpose=(
"ERWEITERT — behalte Tabelle. Füge Filter-Bar: User-Select, Action-Search, Date-Range. "
"Refetch bei Filter-Change."
),
refs=["apps/web/src/pages/AuditLog.tsx"],
),
],
),
Feature(
name="idle-detection",
description="Idle-Detection: nach 5min Inaktivität Active-Timer pausieren-prompt",
files=[
FileGen(
path="apps/web/src/components/IdleDetector.tsx",
purpose=(
"IdleDetector-Component. Listens auf mousemove, keypress. Resettet Timer. "
"Nach 5min ohne Activity UND wenn ActiveTimer läuft: zeigt Modal 'Du warst 5min inaktiv. "
"Timer pausieren oder weiterlaufen lassen?'. Bei pause: api.stopTimeEntry."
),
refs=["apps/web/src/components/ActiveTimer.tsx"],
),
FileGen(
path="apps/web/src/App.tsx",
purpose="ERWEITERT — mount <IdleDetector /> global im Root-Route. Behalte alles.",
refs=["apps/web/src/App.tsx"],
),
],
),
Feature(
name="time-entry-comments",
description="Kommentare/Notes pro TimeEntry als Thread",
files=[
FileGen(
path="apps/api/src/db/schema.ts",
purpose=(
"WICHTIG: behalte ALLE bestehenden Tabellen (vor allem passwordResetTokens, apiKeys). "
"Füge `timeEntryComments` Tabelle: id, entryId references timeEntries, userId references users, "
"body text, createdAt."
),
refs=["apps/api/src/db/schema.ts"],
),
FileGen(
path="apps/api/src/routes/time-entry-comments.ts",
purpose=(
"Fastify-Plugin /api/time-entry-comments. Auth required. "
"GET /entries/:entryId/comments (list), POST /entries/:entryId/comments (body: {body}), DELETE /:id. "
"User kann nur eigene löschen (außer admin)."
),
),
],
),
Feature(
name="api-client-phase18",
description="API um apikeys + audit-filters + comments erweitern",
files=[
FileGen(
path="apps/web/src/lib/api.ts",
purpose=(
"ERWEITERT — behalte ALLES. Füge: listApiKeys, createApiKey(name), revokeApiKey(id), "
"listAuditLog(filters), listEntryComments(entryId), createEntryComment(entryId, body), deleteEntryComment(id)."
),
refs=["apps/web/src/lib/api.ts"],
),
],
),
Feature(
name="router-phase18",
description="Mount + UI-Routen für api-keys, comments",
files=[
FileGen(
path="apps/api/src/routes/index.ts",
purpose="ERWEITERT — füge apiKeyRoutes ('/api/api-keys') + timeEntryCommentRoutes ('/api/time-entry-comments').",
refs=["apps/api/src/routes/index.ts"],
),
FileGen(
path="apps/web/src/App.tsx",
purpose="ERWEITERT — füge /api-keys Route. Behalte alles.",
refs=["apps/web/src/App.tsx"],
),
],
),
]
def load_state() -> dict:
if PHASE18_STATE.exists():
return json.loads(PHASE18_STATE.read_text())
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
def save_state(state: dict) -> None:
PHASE18_STATE.write_text(json.dumps(state, indent=2))
async def main() -> int:
log_section("🚀 Phase-18 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-18 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()))