#!/usr/bin/env python3 """ Phase-2 feature loop. Adds features one at a time to the running EmberClone. Each feature: 1. Generate needed files via Gemma 2. tsc check (compile errors → re-prompt with errors as context) 3. Playwright screenshot of relevant route after change 4. Commit + push 5. Log to GENERATION_LOG.md Designed for continuous overnight execution. Each Feature is one "batch". """ from __future__ import annotations import asyncio import datetime import json import subprocess import sys import time from dataclasses import dataclass, field from pathlib import Path import httpx ROOT = Path(__file__).resolve().parent.parent LOG = ROOT / "GENERATION_LOG.md" PHASE2_STATE = ROOT / ".phase2-state.json" VLLM_URL = "http://127.0.0.1:8000/v1/chat/completions" MODEL = "gemma-4-31b" MAX_RETRIES = 3 def log(msg: str, level: str = "INFO") -> None: ts = datetime.datetime.now().strftime("%H:%M:%S") with LOG.open("a") as f: f.write(f"- `{ts}` **{level}** {msg}\n") print(f"[{ts} {level}] {msg}", flush=True) def log_section(title: str) -> None: ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") with LOG.open("a") as f: f.write(f"\n## {title} ({ts})\n\n") print(f"\n=== {title} ===", flush=True) def git(*args: str) -> tuple[int, str]: r = subprocess.run( ["git", "-c", "user.email=dennis.paradzinski@it.financeflow.de", "-c", "user.name=Dennis (via Claude+Gemma)", *args], cwd=ROOT, capture_output=True, text=True, ) return r.returncode, (r.stdout + r.stderr).strip() async def gemma(prompt: str, max_tokens: int = 6000) -> str | None: async with httpx.AsyncClient(timeout=600) as client: try: r = await client.post(VLLM_URL, json={ "model": MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "temperature": 0.2, }) r.raise_for_status() return r.json()["choices"][0]["message"]["content"] except Exception as e: log(f"Gemma-Call-Fehler: {type(e).__name__}: {e}", level="ERROR") return None def strip_codefence(text: str) -> str: t = text.strip() if t.startswith("```"): lines = t.split("\n") if lines[0].startswith("```"): lines = lines[1:] if lines and lines[-1].startswith("```"): lines = lines[:-1] t = "\n".join(lines) return t.strip() def load_existing(paths: list[str], max_each: int = 4000) -> str: out = "" for p in paths: fp = ROOT / p if fp.exists(): content = fp.read_text()[:max_each] out += f"\n\n### `{p}` (bereits existent, als Referenz):\n```{fp.suffix.lstrip('.')}\n{content}\n```" return out def tsc_check() -> tuple[bool, str]: """Run tsc --noEmit on api. Returns (ok, error_output).""" r = subprocess.run( ["pnpm", "--filter", "api", "exec", "tsc", "--noEmit", "-p", "tsconfig.json"], cwd=ROOT, capture_output=True, text=True, timeout=120, ) return r.returncode == 0, (r.stdout + r.stderr).strip() @dataclass class FileGen: path: str purpose: str refs: list[str] = field(default_factory=list) extra: str = "" @dataclass class Feature: name: str description: str files: list[FileGen] after_commit_routes: list[str] = field(default_factory=list) # routes to screenshot+verify # ---------- Feature definitions ---------- FEATURES: list[Feature] = [ Feature( name="customers-crud", description="Customers-CRUD: API-Routes + Web-Page mit List + Create-Form + Delete", files=[ FileGen( path="apps/api/src/routes/customers.ts", purpose=( "Fastify-Plugin für CRUD /api/customers. GET / (list, only active by default), " "GET /:id, POST / (create, name required), PATCH /:id, DELETE /:id (soft-delete via active=false). " "Alle Routes brauchen Auth (request.jwtVerify()). Verwende drizzle db.select/insert/update." ), refs=["apps/api/src/db/schema.ts", "apps/api/src/db/index.ts", "apps/api/src/routes/time-entries.ts"], ), FileGen( path="apps/web/src/pages/Customers.tsx", purpose=( "Customers-Page mit TanStack-Query Liste + Inline-Create-Form (nur 'name' Feld) + Delete-Button pro Eintrag. " "Tailwind, layout wie TimeEntries.tsx (Header → Form-Card → Table). " "Verwende api.listCustomers() / api.createCustomer({name}) / api.deleteCustomer(id)." ), refs=["apps/web/src/pages/TimeEntries.tsx", "apps/web/src/lib/api.ts"], ), ], after_commit_routes=["/customers"], ), Feature( name="projects-crud", description="Projects-CRUD: API + Web-Page mit Customer-Picker", files=[ FileGen( path="apps/api/src/routes/projects.ts", purpose=( "Fastify-Plugin /api/projects. CRUD wie customers.ts. Felder: name, customerId (FK), active. " "GET / optional ?customerId=X filter. Auth required." ), refs=["apps/api/src/routes/customers.ts", "apps/api/src/db/schema.ts"], ), FileGen( path="apps/web/src/pages/Projects.tsx", purpose=( "Projects-Page. Liste + Create-Form mit name (text) + customerId (select dropdown, lädt via api.listCustomers()). " "Layout konsistent mit Customers.tsx und TimeEntries.tsx." ), refs=["apps/web/src/pages/Customers.tsx"], ), ], after_commit_routes=["/projects"], ), Feature( name="api-client-extensions", description="Erweitere lib/api.ts um Customer + Project Endpoints + Logout fixes", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "ERWEITERTE Version der bestehenden api.ts. Behalte alle bestehenden Funktionen (login, logout, getMe, listTimeEntries, " "createTimeEntry, deleteTimeEntry). Füge hinzu: " "listCustomers(), createCustomer({name}), deleteCustomer(id), " "listProjects(opts?), createProject({name, customerId}), deleteProject(id). " "Verwende type imports aus '@emberclone/shared' (CustomerInsert, ProjectInsert)." ), refs=["apps/web/src/lib/api.ts"], ), ], after_commit_routes=["/"], ), Feature( name="router-with-new-pages", description="Erweitere App.tsx Routes um /customers, /projects + Navigation", files=[ FileGen( path="apps/web/src/components/Nav.tsx", purpose=( "Top-Nav-Bar React-Component. Links: Dashboard /, TimeEntries /time-entries, Customers /customers, Projects /projects. " "Verwende TanStack-Router Link. Active-State styling per useLocation. Logout-Button rechts. " "Tailwind, weißer Hintergrund, border-bottom, Container-zentriert." ), refs=["apps/web/src/lib/api.ts"], ), FileGen( path="apps/web/src/App.tsx", purpose=( "ERWEITERTE Router-Setup. Behalte bestehende Routes (/, /login, /time-entries). " "Füge hinzu: /customers (Customers component), /projects (Projects component), beide mit Auth-Check. " "Root-Route rendert