EmberClone/scripts/phase10_features.py

212 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""Phase-10: markdown-notes, customer-tags, project-templates, language-toggle, keyboard-help."""
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
PHASE10_STATE = ROOT / ".phase10-state.json"
FEATURES: list[Feature] = [
Feature(
name="markdown-notes-time-entry",
description="Markdown-Notes-Feld pro Time-Entry + Render in Liste",
files=[
FileGen(
path="apps/api/src/db/schema.ts",
purpose=(
"ERWEITERT — füge `notes: text('notes')` Spalte (nullable) zu time_entries Tabelle. "
"Behalte alle bestehenden Spalten + Tabellen."
),
refs=["apps/api/src/db/schema.ts"],
),
FileGen(
path="apps/web/src/pages/TimeEntries.tsx",
purpose=(
"ERWEITERT — behalte alles. Füge `notes` Textarea (optional, expandable) zum Create-Form. "
"In der Liste: kleines Expand-Icon pro row, beim Klick zeigt notes als Markdown gerendert "
"(simple: nur \\n→<br/>, **bold** → <strong>, *italic* → <em>; oder benutze 'marked' npm dep wenn installiert)."
),
refs=["apps/web/src/pages/TimeEntries.tsx"],
),
],
),
Feature(
name="customer-tags",
description="Tags-Feld bei Customers + Filter-by-Tag",
files=[
FileGen(
path="apps/api/src/db/schema.ts",
purpose=(
"ERWEITERT — füge `tags: text('tags').array().notNull().default([])` Spalte zu customers. "
"Behalte alles."
),
refs=["apps/api/src/db/schema.ts"],
),
FileGen(
path="apps/web/src/pages/Customers.tsx",
purpose=(
"ERWEITERT — füge Tags-Input (kommagetrennt) zum Create-Form. "
"Zeige Tags als Chips in der Tabelle. Filter oben: 'Filter by tag…' input, "
"filtert client-side die Liste auf Customers wo tag.includes(filter)."
),
refs=["apps/web/src/pages/Customers.tsx"],
),
],
),
Feature(
name="project-templates",
description="Wiederverwendbare Project-Templates (admin)",
files=[
FileGen(
path="apps/api/src/db/schema.ts",
purpose=(
"ERWEITERT — füge `projectTemplates` Tabelle: id (uuid pk), name (text), defaultBillable (boolean), "
"estimatedHours (integer nullable), createdAt (timestamp). Behalte alles."
),
refs=["apps/api/src/db/schema.ts"],
),
FileGen(
path="apps/api/src/routes/project-templates.ts",
purpose=(
"Fastify-Plugin /api/project-templates. CRUD, admin-only via preHandler+role-check. "
"GET/POST/PATCH/DELETE."
),
refs=["apps/api/src/routes/customers.ts"],
),
FileGen(
path="apps/web/src/pages/ProjectTemplates.tsx",
purpose=(
"ProjectTemplates-Page (admin-only). Liste + Create-Form (name, defaultBillable checkbox, estimatedHours number). "
"Beim Project-Create (in Projects.tsx) optional: 'aus Template'-Dropdown — wenn gewählt, pre-fillen."
),
refs=["apps/web/src/pages/Customers.tsx"],
),
],
),
Feature(
name="language-toggle",
description="i18n-Stub mit DE/EN-Toggle (localStorage)",
files=[
FileGen(
path="apps/web/src/lib/i18n.tsx",
purpose=(
"Mini-i18n. useTranslation()-Hook: returns {t, setLang, lang}. "
"Dictionary inline mit ~20 wichtigsten Keys ('login', 'dashboard', 'customers', 'projects', "
"'time_entries', 'logout', 'save', 'delete', etc.) in DE+EN. "
"Persist in localStorage 'lang'."
),
),
FileGen(
path="apps/web/src/components/Nav.tsx",
purpose=(
"ERWEITERT — füge Sprach-Toggle (DE/EN Button) rechts neben Theme-Toggle. "
"Behalte alle bestehenden Links."
),
refs=["apps/web/src/components/Nav.tsx"],
),
],
),
Feature(
name="keyboard-help-modal",
description="Help-Modal mit Keyboard-Shortcuts (?-Hotkey)",
files=[
FileGen(
path="apps/web/src/components/KeyboardHelp.tsx",
purpose=(
"Help-Modal. Triggered by '?'-Taste (window-keydown). Liste der Shortcuts: "
"Cmd+K = Command Palette, ? = Diese Hilfe, T = Theme-Toggle, G then D = Dashboard, "
"G then C = Customers, G then P = Projects, G then T = TimeEntries. "
"Escape schließt. Tailwind centered modal mit kbd-styled keys."
),
),
FileGen(
path="apps/web/src/App.tsx",
purpose="ERWEITERT — mounte <KeyboardHelp /> global im Root-Route. Behalte alles.",
refs=["apps/web/src/App.tsx"],
),
],
),
Feature(
name="api-client-phase10",
description="API um project-templates erweitern",
files=[
FileGen(
path="apps/web/src/lib/api.ts",
purpose=(
"ERWEITERT — behalte ALLES. Füge: listProjectTemplates(), createProjectTemplate(data), "
"updateProjectTemplate(id, data), deleteProjectTemplate(id)."
),
refs=["apps/web/src/lib/api.ts"],
),
],
),
Feature(
name="router-phase10",
description="App + routes/index für /project-templates",
files=[
FileGen(
path="apps/api/src/routes/index.ts",
purpose="ERWEITERT — füge projectTemplateRoutes ('/api/project-templates').",
refs=["apps/api/src/routes/index.ts"],
),
FileGen(
path="apps/web/src/App.tsx",
purpose="ERWEITERT — füge /project-templates (admin-only). Behalte alles.",
refs=["apps/web/src/App.tsx"],
),
],
),
]
def load_state() -> dict:
if PHASE10_STATE.exists():
return json.loads(PHASE10_STATE.read_text())
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
def save_state(state: dict) -> None:
PHASE10_STATE.write_text(json.dumps(state, indent=2))
async def main() -> int:
log_section("🚀 Phase-10 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-10 Run beendet")
log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}")
import subprocess
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()))