184 lines
7.2 KiB
Python
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()))
|