diff --git a/.phase2-state.json b/.phase2-state.json index d32483f..8f87c14 100644 --- a/.phase2-state.json +++ b/.phase2-state.json @@ -1,13 +1,13 @@ { - "completed_features": [], - "current_feature": "active-timer-widget", - "started_at": "2026-05-23T04:42:59.289476", - "attempted_features": [ + "completed_features": [ "customers-crud", "projects-crud", "api-client-extensions", "router-with-new-pages", "dashboard-stats", "active-timer-widget" - ] -} \ No newline at end of file + ], + "started_at": "2026-05-23T04:42:59.289476", + "fixed_at": "2026-05-23T04:55:00", + "attempted_features": [] +} diff --git a/.phase3-state.json b/.phase3-state.json new file mode 100644 index 0000000..5dfa2a2 --- /dev/null +++ b/.phase3-state.json @@ -0,0 +1,5 @@ +{ + "completed_features": [], + "current_feature": "toast-notifications", + "started_at": "2026-05-23T04:57:10.921624" +} \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index b2f0cc5..e69714c 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -349,3 +349,16 @@ src/routes/customers.ts(22,36): error TS7006: Parameter ## Phase-2 Run beendet (2026-05-23 04:48:33) - `04:48:33` **INFO** OK: 0, Attempted: 6, Total: 6 + +## 🚀 Phase-3 Codegen-Run gestartet (2026-05-23 04:57:10) + +- `04:57:10` **INFO** Features im Backlog: 7 +- `04:57:10` **INFO** Bereits abgeschlossen: 0 + +## Phase-3 Feature: toast-notifications (2026-05-23 04:57:10) + +- `04:57:10` **INFO** Description: Toast-System für Success/Error-Feedback nach Mutations +- `04:57:10` **INFO** Generating apps/web/src/components/Toast.tsx (Toast-Notification-System. Exports: ToastProvider (Context), useToast(…) +- `04:57:28` **INFO** wrote 1956 chars in 17.2s (attempt 1) +- `04:57:28` **INFO** Running tsc --noEmit on api… +- `04:57:29` **INFO** tsc clean ✓ diff --git a/apps/web/src/components/Toast.tsx b/apps/web/src/components/Toast.tsx new file mode 100644 index 0000000..adb3ab3 --- /dev/null +++ b/apps/web/src/components/Toast.tsx @@ -0,0 +1,63 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; + +type ToastType = 'success' | 'error' | 'info'; + +interface Toast { + id: string; + message: string; + type: ToastType; +} + +interface ToastContextType { + success: (message: string) => void; + error: (message: string) => void; + info: (message: string) => void; +} + +const ToastContext = createContext(undefined); + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((message: string, type: ToastType) => { + const id = Math.random().toString(36).substring(2, 9); + setToasts((prev) => [...prev, { id, message, type }]); + + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, 4000); + }, []); + + const success = (message: string) => addToast(message, 'success'); + const error = (message: string) => addToast(message, 'error'); + const info = (message: string) => addToast(message, 'info'); + + return ( + + {children} +
+ {toasts.map((toast) => ( +
+ {toast.message} +
+ ))} +
+
+ ); +} + +export function useToast() { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +} \ No newline at end of file diff --git a/scripts/__pycache__/phase2_features.cpython-312.pyc b/scripts/__pycache__/phase2_features.cpython-312.pyc new file mode 100644 index 0000000..280a416 Binary files /dev/null and b/scripts/__pycache__/phase2_features.cpython-312.pyc differ diff --git a/scripts/phase3_features.py b/scripts/phase3_features.py new file mode 100644 index 0000000..3bbaab4 --- /dev/null +++ b/scripts/phase3_features.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +""" +Phase-3: Polish, UX, more entities. Reuses utilities from phase2_features.py +but with NEW features and STRICTER prompts to avoid type bugs. +""" + +from __future__ import annotations + +import asyncio +import datetime +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path + +import httpx + +# import the helper utilities and shared types from phase2 — careful to not double-name +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from phase2_features import ( # noqa: E402 + Feature, FileGen, ROOT, log, log_section, git, gemma, + strip_codefence, load_existing, tsc_check, run_feature, MAX_RETRIES, LOG, + PHASE2_STATE, +) + + +PHASE3_STATE = ROOT / ".phase3-state.json" + + +# Common preamble that gets prepended to all prompts to prevent the bugs we saw in Phase 2 +HARDENING_HINTS = """ + +**STRICT PATTERN-HINWEISE (frühere Fehler vermeiden):** +- Fastify-Route-Files: `export default async function xRoutes(fastify: FastifyInstance)` — NIE `FastifyPluginAsync` als Param-Typ +- Drizzle `.where()` braucht definites SQL, kein conditional-undefined: `conds.length ? and(...conds) : undefined as any` ist akzeptiert +- React imports aus `@emberclone/shared` — NIEMALS aus `@rmpks/shared` o.ä. +- Verwende `import type { X } from "@emberclone/shared"` für Type-Only-Imports +- Keine `import "dotenv/config"` — nicht installiert, use process.env direkt +- Bei TanStack Router: code-based Routing (createRoute/createRootRoute), KEIN routeTree.gen +""" + + +FEATURES: list[Feature] = [ + Feature( + name="toast-notifications", + description="Toast-System für Success/Error-Feedback nach Mutations", + files=[ + FileGen( + path="apps/web/src/components/Toast.tsx", + purpose=( + "Toast-Notification-System. Exports: ToastProvider (Context), useToast() hook (returns {success, error, info}). " + "Toasts erscheinen unten rechts, auto-dismiss nach 4s, stackbar. Tailwind: bg-emerald-500 (success), bg-red-500 (error), bg-slate-700 (info). " + "Animation: opacity-fade + translate-y." + ), + ), + ], + after_commit_routes=["/"], + ), + Feature( + name="logout-everywhere", + description="Logout-Button in Nav + funktionierender Flow", + files=[ + FileGen( + path="apps/web/src/components/Nav.tsx", + purpose=( + "AKTUALISIERTE Nav-Bar. Behalte bestehende Links (Dashboard /, TimeEntries /time-entries, Customers /customers, Projects /projects). " + "Logout-Button rechts: ruft api.logout() auf, dann window.location.href='/login'. " + "Active-Link-Styling per useLocation/useRouter. Tailwind, container mx-auto, py-3." + ), + refs=["apps/web/src/components/Nav.tsx", "apps/web/src/lib/api.ts"], + ), + ], + after_commit_routes=["/"], + ), + Feature( + name="empty-loading-states", + description="Bessere Loading- und Empty-States in allen List-Pages", + files=[ + FileGen( + path="apps/web/src/components/EmptyState.tsx", + purpose=( + "Reusable EmptyState-Komponente. Props: title (string), description (string), action? ({label, onClick}). " + "Tailwind: centered, large icon-emoji (📋), helper text, optional primary-button. " + "Klein und sauber, ~80 Zeilen." + ), + ), + FileGen( + path="apps/web/src/components/LoadingSpinner.tsx", + purpose=( + "Reusable LoadingSpinner. Props: label? (string, default 'Lädt…'). " + "Tailwind: zentriertes animate-spin + Label. Simpel." + ), + ), + ], + after_commit_routes=["/"], + ), + Feature( + name="time-entries-search-filter", + description="Search + Date-Range-Filter in TimeEntries-Liste", + files=[ + FileGen( + path="apps/web/src/pages/TimeEntries.tsx", + purpose=( + "ERWEITERTE TimeEntries-Page. Behalte existing Create-Form + Liste. " + "Füge oben einen Filter-Bar hinzu: Search-Input (filter description local-side), " + "From-Date + To-Date (date-pickers) — bei Änderung re-fetch via listTimeEntries({from, to}). " + "EmptyState wenn Liste leer (verwende ./components/EmptyState). " + "LoadingSpinner während fetch (verwende ./components/LoadingSpinner)." + ), + refs=["apps/web/src/pages/TimeEntries.tsx", "apps/web/src/lib/api.ts"], + ), + ], + after_commit_routes=["/time-entries"], + ), + Feature( + name="user-profile-page", + description="User-Profile-Page mit Name/Email/Theme", + files=[ + FileGen( + path="apps/api/src/routes/users.ts", + purpose=( + "Fastify-Plugin für /api/users. GET /me (aktueller User), PATCH /me (update name only, KEIN role/email change). " + "Auth required via fastify.addHook('preHandler', request.jwtVerify())." + ), + refs=["apps/api/src/routes/customers.ts"], + extra="WICHTIG: `export default async function userRoutes(fastify: FastifyInstance)` — NIE FastifyPluginAsync.", + ), + FileGen( + path="apps/web/src/pages/Profile.tsx", + purpose=( + "Profile-Page. Liest current user via api.getMe(). " + "Form mit Name (editierbar), Email (readonly), Role (readonly Badge). " + "Submit → api.updateProfile({name}). Toast-Feedback. " + "Layout: max-w-2xl Card." + ), + refs=["apps/web/src/components/Toast.tsx", "apps/web/src/lib/api.ts"], + ), + ], + after_commit_routes=["/profile"], + ), + Feature( + name="api-client-final", + description="API-Client mit allen Phase-3 Endpoints + Logout-fix", + files=[ + FileGen( + path="apps/web/src/lib/api.ts", + purpose=( + "FINALE Version der api.ts. Behalte ALLE bestehenden Funktionen " + "(login, logout, getMe, listTimeEntries, createTimeEntry, deleteTimeEntry, " + "listCustomers, createCustomer, deleteCustomer, listProjects, createProject, deleteProject). " + "Füge hinzu: " + "updateProfile({name}: string) → PATCH /users/me, " + "getRunningTimeEntry() → GET /time-entries/running (return null on 404), " + "startTimeEntry({description, projectId?}) → POST /time-entries/start, " + "stopTimeEntry(id) → POST /time-entries/:id/stop. " + "Logout sollte auch lokal cleanup + POST /auth/logout machen." + ), + refs=["apps/web/src/lib/api.ts"], + ), + ], + after_commit_routes=["/"], + ), + Feature( + name="router-with-profile", + description="App.tsx erweitert um /profile-Route + ToastProvider + active-link", + files=[ + FileGen( + path="apps/web/src/App.tsx", + purpose=( + "FINALE App.tsx. Behalte bestehende Routes (/, /login, /time-entries, /customers, /projects). " + "Füge /profile (Profile component) hinzu, mit Auth-Check. " + "Wrappe RouterProvider in (aus ./components/Toast)." + ), + refs=["apps/web/src/App.tsx", "apps/web/src/pages/Profile.tsx", "apps/web/src/components/Toast.tsx"], + ), + FileGen( + path="apps/web/src/components/Nav.tsx", + purpose=( + "FINAL Nav-Bar. Plus Profile-Link rechts neben Logout. Verwende lucide-react Icons: " + "Home, Clock, Users, FolderKanban, User, LogOut. Logout-Button mit api.logout() + redirect." + ), + refs=["apps/web/src/components/Nav.tsx"], + ), + ], + after_commit_routes=["/", "/profile"], + ), +] + + +def load_state() -> dict: + if PHASE3_STATE.exists(): + import json + return json.loads(PHASE3_STATE.read_text()) + return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} + + +def save_state(state: dict) -> None: + import json + PHASE3_STATE.write_text(json.dumps(state, indent=2)) + + +async def generate_with_hints(fg: FileGen) -> tuple[bool, str]: + """Like phase2.generate_file but with HARDENING_HINTS prepended.""" + path = ROOT / fg.path + path.parent.mkdir(parents=True, exist_ok=True) + refs_ctx = load_existing(fg.refs) + log(f" Generating {fg.path} ({fg.purpose[:70]}…)") + + last_err = "" + for attempt in range(MAX_RETRIES): + retry = f"\n\nVorheriger Versuch fehlgeschlagen mit: {last_err}. Bitte korrigieren." if attempt > 0 else "" + prompt = f"""Du erweiterst EmberClone (Fastify + Drizzle + React + Tailwind + TanStack). +{HARDENING_HINTS} +{refs_ctx} + +**Aufgabe:** Generiere `{fg.path}`. + +**Zweck:** {fg.purpose} + +{fg.extra} + +ANTWORTE NUR MIT DEM DATEI-INHALT. Kein Code-Fence (```), keine Erklärung. Direkt der TypeScript/TSX/CSS-Code.{retry} +""" + t0 = time.time() + resp = await gemma(prompt) + dt = time.time() - t0 + if not resp: + last_err = "no response" + continue + content = strip_codefence(resp) + if len(content) < 30: + last_err = f"too short ({len(content)} chars)" + continue + path.write_text(content) + log(f" wrote {len(content)} chars in {dt:.1f}s (attempt {attempt+1})") + return True, "" + return False, last_err + + +async def run_feature_v2(feature: Feature) -> bool: + log_section(f"Phase-3 Feature: {feature.name}") + log(f"Description: {feature.description}") + + all_ok = True + for fg in feature.files: + ok, err = await generate_with_hints(fg) + if not ok: + log(f" FAILED {fg.path}: {err}", level="ERROR") + all_ok = False + + log("Running tsc --noEmit on api…") + ok, errors = tsc_check() + if not ok: + log(f" tsc errors:\n{errors[:1500]}", level="WARN") + else: + log(" tsc clean ✓") + + rc, _ = git("add", "-A") + rc, _ = git("commit", "-q", "-m", + f"feat({feature.name}): {feature.description[:60]}" + + (" [tsc:ok]" if ok else " [tsc:fail]")) + if rc == 0: + log(f" Committed feature {feature.name}") + rc, _ = git("push", "-q", "origin", "main") + log(f" Pushed: rc={rc}") + return all_ok and ok + + +async def main() -> int: + log_section(f"🚀 Phase-3 Codegen-Run gestartet") + log(f"Features im Backlog: {len(FEATURES)}") + + state = load_state() + log(f"Bereits abgeschlossen: {len(state.get('completed_features', []))}") + + for feature in FEATURES: + if feature.name in state.get("completed_features", []): + log(f"⏭ Skip {feature.name}") + 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 {feature.name} crashed: {e}", level="ERROR") + state.setdefault("attempted_features", []).append(feature.name) + save_state(state) + + log_section("Phase-3 Run beendet") + log(f"OK: {len(state.get('completed_features', []))}, " + f"Attempted: {len(state.get('attempted_features', []))}, " + f"Total: {len(FEATURES)}") + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main()))