212 lines
8.5 KiB
Python
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()))
|