191 lines
7.7 KiB
Python
191 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Phase-8: recent-activity, project-stats, bulk-actions, csv-import, account-deletion."""
|
|
|
|
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
|
|
|
|
PHASE8_STATE = ROOT / ".phase8-state.json"
|
|
|
|
FEATURES: list[Feature] = [
|
|
Feature(
|
|
name="recent-activity-widget",
|
|
description="Recent-Activity-Feed im Dashboard (letzte 10 Audit-Log-Einträge)",
|
|
files=[
|
|
FileGen(
|
|
path="apps/web/src/components/ActivityFeed.tsx",
|
|
purpose=(
|
|
"ActivityFeed-Component. useQuery api.listAuditLog() (oder /api/audit-log/recent). "
|
|
"Zeigt letzte 10 Einträge: when (relative time wie '5 min ago' via date-fns formatDistance), "
|
|
"user.name, action, resource. Compact-Layout, max-h-96 overflow-y-auto. "
|
|
"Wenn current user nicht admin: zeige nur eigene Aktionen oder hide-widget."
|
|
),
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/pages/Dashboard.tsx",
|
|
purpose=(
|
|
"ERWEITERT — behalte bestehende Cards + Chart. Füge <ActivityFeed /> als 4. Section unten rechts. "
|
|
"Grid: 3 cols für Stats, 2 cols (chart links + activity rechts)."
|
|
),
|
|
refs=["apps/web/src/pages/Dashboard.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="time-entry-bulk-actions",
|
|
description="Bulk-Select + bulk-delete in TimeEntries-Page",
|
|
files=[
|
|
FileGen(
|
|
path="apps/api/src/routes/time-entries.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte alles. Füge POST /bulk-delete (body: {ids: string[]}) → "
|
|
"löscht alle IDs des Users (admin: alle), return {deleted: count}. Auth required."
|
|
),
|
|
refs=["apps/api/src/routes/time-entries.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/pages/TimeEntries.tsx",
|
|
purpose=(
|
|
"ERWEITERT — behalte bestehendes UI. Füge Checkbox-Spalte links in Table. "
|
|
"Wenn min 1 selektiert: zeige Action-Bar oben mit 'Delete (n)' Button. "
|
|
"Bulk-Delete-Mutation ruft api.bulkDeleteTimeEntries(ids)."
|
|
),
|
|
refs=["apps/web/src/pages/TimeEntries.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="customer-csv-import",
|
|
description="CSV-Import für Customers (admin) + Upload-UI",
|
|
files=[
|
|
FileGen(
|
|
path="apps/api/src/routes/customers.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte alles. Füge POST /import (multipart-Upload CSV mit Header 'name,active'): "
|
|
"parse jede Zeile, insert customers. Return {imported: count, errors: []}. Admin-only."
|
|
),
|
|
refs=["apps/api/src/routes/customers.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/pages/Customers.tsx",
|
|
purpose=(
|
|
"ERWEITERT — behalte bestehendes UI. Füge 'CSV importieren' Button rechts (nur admin), "
|
|
"öffnet file-input. Bei Upload: api.importCustomersCsv(file), Toast mit Resultat, refetch."
|
|
),
|
|
refs=["apps/web/src/pages/Customers.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="account-deletion",
|
|
description="User kann eigenes Account löschen (Profile-Page)",
|
|
files=[
|
|
FileGen(
|
|
path="apps/api/src/routes/users.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte alles. Füge DELETE /me (body: {password}): verify password mit argon2, "
|
|
"dann delete user (cascade time-entries). Logout + 200. Wenn password falsch: 401."
|
|
),
|
|
refs=["apps/api/src/routes/users.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/pages/Profile.tsx",
|
|
purpose=(
|
|
"ERWEITERT — behalte Name + Password Cards. Füge dritte Card 'Gefahrenzone' (red border): "
|
|
"'Account löschen'-Button → öffnet Confirm-Modal mit Passwort-Input. "
|
|
"Nach Bestätigung: api.deleteAccount(password), redirect /login."
|
|
),
|
|
refs=["apps/web/src/pages/Profile.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="project-stats-page",
|
|
description="Project-Stats: Stunden total, monthly chart, top contributors",
|
|
files=[
|
|
FileGen(
|
|
path="apps/api/src/routes/projects.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte alles. Füge GET /:id/stats: "
|
|
"{totalHours, entryCount, byUser: [{userId, name, hours}], byMonth: [{month, hours}]} "
|
|
"aus joining time_entries + users."
|
|
),
|
|
refs=["apps/api/src/routes/projects.ts"],
|
|
),
|
|
FileGen(
|
|
path="apps/web/src/pages/ProjectDetail.tsx",
|
|
purpose=(
|
|
"ERWEITERT — behalte bestehendes (header, time-entries). Füge Stats-Card oben: "
|
|
"Total-Hours-Badge + monthly BarChart (recharts) + 'Top Contributors' List."
|
|
),
|
|
refs=["apps/web/src/pages/ProjectDetail.tsx"],
|
|
),
|
|
],
|
|
),
|
|
Feature(
|
|
name="api-client-phase8",
|
|
description="API um Phase-8 endpoints",
|
|
files=[
|
|
FileGen(
|
|
path="apps/web/src/lib/api.ts",
|
|
purpose=(
|
|
"ERWEITERT — behalte ALLES. Füge: "
|
|
"bulkDeleteTimeEntries(ids: string[]), importCustomersCsv(file: File), "
|
|
"deleteAccount(password: string), getProjectStats(id)."
|
|
),
|
|
refs=["apps/web/src/lib/api.ts"],
|
|
),
|
|
],
|
|
),
|
|
]
|
|
|
|
|
|
def load_state() -> dict:
|
|
if PHASE8_STATE.exists():
|
|
return json.loads(PHASE8_STATE.read_text())
|
|
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
|
|
|
|
|
|
def save_state(state: dict) -> None:
|
|
PHASE8_STATE.write_text(json.dumps(state, indent=2))
|
|
|
|
|
|
async def main() -> int:
|
|
log_section("🚀 Phase-8 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-8 Run beendet")
|
|
log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}")
|
|
|
|
import subprocess
|
|
log("Running db:migrate (no schema changes expected, but just in case)…")
|
|
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()))
|