diff --git a/.phase2-state.json b/.phase2-state.json new file mode 100644 index 0000000..64cd8bb --- /dev/null +++ b/.phase2-state.json @@ -0,0 +1,5 @@ +{ + "completed_features": [], + "current_feature": "customers-crud", + "started_at": "2026-05-23T04:42:59.289476" +} \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 59b80b5..1bf9743 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -128,3 +128,71 @@ Schritt-für-Schritt-Historie aller Gemma-Code-Generierungen. - `04:28:45` **INFO** ok: 17/18, fail: 1/18 - `04:28:45` **WARN** 1 Files mit final-Fehler. Manuelle Inspektion nötig. + +## 🟢 FINAL STATUS — EmberClone is LIVE (2026-05-23 04:36) + +**Erreichbar:** +- Web-UI: http://localhost:5174 (HTTP 200) +- API: http://localhost:4001/health (HTTP 200) +- Login: `admin@emberclone.local` / `emberclone2026` (verifiziert ✓) + +**Gemma-Generation:** 17/18 Files in ~4 Minuten (04:24:46 → 04:28:45), durchschnittlich ~15 s pro File. + +**Claude-Fixes nach Generation** (nötige Korrekturen): +1. `packages/shared/src/schemas.ts`: `UserInsertSchema = UserRoleSchema.extend(...)` → `z.object(...)` (Gemma verwendete `.extend()` auf einem `z.enum`) +2. `apps/web/src/main.tsx`: rewrite — Gemma hatte file-based-Routing (`routeTree.gen`) + code-based-Routing (`App.tsx`) gemischt +3. `apps/web/src/lib/api.ts`: rewrite — wrong package import `@rmpks/shared` + `z.infer` was unsinnig +4. `apps/web/src/pages/TimeEntries.tsx`: gleiche zwei Bugs +5. `apps/api/src/index.ts`: `import "dotenv/config"` entfernt (dotenv nicht in deps) +6. `apps/web/package.json` + `apps/api/package.json`: `@emberclone/shared: workspace:*` ergänzt + +**Stats:** +- Gemma-only: 17 commits (1 ATTEMPT-Marker für trivialen "too short" false-positive) +- Claude-fixes: 1 commit (claude-fix: ...) +- Insgesamt 21 commits pushed zu `oxofrmbl/EmberClone` +- Pipeline-Gesamtzeit: ~14 min (Scaffold 04:22 → Live 04:36) + +**Was funktioniert** (verifiziert): +- ✅ Web frontend lädt +- ✅ API health-check +- ✅ Login mit Default-Admin +- ✅ DB-Migrations + Admin-Seed +- ✅ JWT-basierte Auth-Pipeline + +**Noch nicht getestet** (für User um 12 zu probieren): +- ⏳ TimeEntry-CRUD im UI (vermutlich funktional, aber nicht End-to-End klickend verifiziert) +- ⏳ Projects/Customers — keine UI dafür (waren nicht in den ersten 18 Files) + +**Stop-Marker:** `/tmp/emberclone-built` exists, web=200. Kein neuer Wakeup. + +## 🚀 Phase-2 Codegen-Run gestartet (2026-05-23 04:42:59) + +- `04:42:59` **INFO** Features im Backlog: 6 +- `04:42:59` **INFO** Bereits abgeschlossen: 0 + +## Feature: customers-crud (2026-05-23 04:42:59) + +- `04:42:59` **INFO** Description: Customers-CRUD: API-Routes + Web-Page mit List + Create-Form + Delete +- `04:42:59` **INFO** Files: 2 +- `04:42:59` **INFO** Generating apps/api/src/routes/customers.ts (Fastify-Plugin für CRUD /api/customers. GET / (list, only active by de…) +- `04:43:20` **INFO** wrote 2355 chars in 21.6s (attempt 1) +- `04:43:20` **INFO** Generating apps/web/src/pages/Customers.tsx (Customers-Page mit TanStack-Query Liste + Inline-Create-Form (nur 'nam…) +- `04:43:55` **INFO** wrote 4259 chars in 34.3s (attempt 1) +- `04:43:55` **INFO** Running tsc --noEmit on api… +- `04:43:56` **WARN** tsc errors: +src/routes/auth.ts(9,11): error TS2339: Property 'post' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(9,33): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/auth.ts(9,42): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/auth.ts(22,27): error TS2339: Property 'jwt' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(44,11): error TS2339: Property 'get' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(44,29): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/auth.ts(44,38): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/auth.ts(70,11): error TS2339: Property 'post' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(70,34): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/auth.ts(70,43): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/customers.ts(14,11): error TS2339: Property 'addHook' does not exist on type 'FastifyPluginAsync'. +src/routes/customers.ts(14,40): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/customers.ts(14,49): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/customers.ts(22,11): error TS2339: Property 'get' does not exist on type 'FastifyPluginAsync'. +src/routes/customers.ts(22,27): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/customers.ts(22,36): error TS7006: Parameter diff --git a/apps/api/src/routes/customers.ts b/apps/api/src/routes/customers.ts new file mode 100644 index 0000000..c1e1982 --- /dev/null +++ b/apps/api/src/routes/customers.ts @@ -0,0 +1,96 @@ +import { FastifyPluginAsync } from "fastify" +import { db } from "../db" +import { customers } from "../db/schema" +import { eq, and } from "drizzle-orm" +import { z } from "zod" + +const CustomerSchema = z.object({ + name: z.string().min(1) +}) + +const CustomerUpdateSchema = CustomerSchema.partial() + +export default async function customerRoutes(fastify: FastifyPluginAsync) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + fastify.get("/", async (request, reply) => { + const { onlyActive } = request.query as { onlyActive?: string } + const isActiveDefault = onlyActive !== "false" + + const results = await db + .select() + .from(customers) + .where(isActiveDefault ? eq(customers.active, true) : undefined) + .orderBy(customers.name) + + return results + }) + + fastify.get("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + + const [customer] = await db + .select() + .from(customers) + .where(eq(customers.id, id)) + .limit(1) + + if (!customer) { + return reply.code(404).send({ message: "Customer not found" }) + } + + return customer + }) + + fastify.post("/", async (request, reply) => { + const body = CustomerSchema.parse(request.body) + + const [customer] = await db + .insert(customers) + .values({ + name: body.name + }) + .returning() + + return reply.code(201).send(customer) + }) + + fastify.patch("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + const body = CustomerUpdateSchema.parse(request.body) + + const [customer] = await db + .update(customers) + .set(body) + .where(eq(customers.id, id)) + .returning() + + if (!customer) { + return reply.code(404).send({ message: "Customer not found" }) + } + + return customer + }) + + fastify.delete("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + + const [customer] = await db + .update(customers) + .set({ active: false }) + .where(eq(customers.id, id)) + .returning() + + if (!customer) { + return reply.code(404).send({ message: "Customer not found" }) + } + + return reply.code(204).send() + }) +} \ No newline at end of file diff --git a/apps/web/src/pages/Customers.tsx b/apps/web/src/pages/Customers.tsx new file mode 100644 index 0000000..4df7c41 --- /dev/null +++ b/apps/web/src/pages/Customers.tsx @@ -0,0 +1,111 @@ +import { useState } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { api } from "../lib/api" + +export default function Customers() { + const queryClient = useQueryClient() + const [name, setName] = useState("") + + const { data: customers, isLoading, isError } = useQuery({ + queryKey: ["customers"], + queryFn: () => api.listCustomers() + }) + + const createMutation = useMutation({ + mutationFn: (name: string) => api.createCustomer({ name }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["customers"] }) + setName("") + } + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.deleteCustomer(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["customers"] }) + } + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim()) return + createMutation.mutate(name) + } + + if (isLoading) return
Loading customers...
+ if (isError) return
Error loading customers.
+ + return ( +
+
+

Customers

+

Manage your client database

+
+ +
+

Add New Customer

+
+
+ + setName(e.target.value)} + placeholder="Enter company or person name" + /> +
+
+ +
+
+
+ +
+ + + + + + + + + {customers && customers.length > 0 ? ( + customers.map((customer: any) => ( + + + + + )) + ) : ( + + + + )} + +
NameActions
{customer.name} + +
+ No customers found. Add your first customer above. +
+
+
+ ) +} \ No newline at end of file diff --git a/scripts/phase2_features.py b/scripts/phase2_features.py new file mode 100644 index 0000000..d86293c --- /dev/null +++ b/scripts/phase2_features.py @@ -0,0 +1,387 @@ +#!/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