#!/usr/bin/env python3 """Phase-20: slack-stub, github-link, time-budget, budget-alerts, recurring-entries.""" 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 PHASE20_STATE = ROOT / ".phase20-state.json" FEATURES: list[Feature] = [ Feature( name="time-budget-per-project", description="Budget-Feld (Stunden) pro Project + Anzeige used/total", files=[ FileGen( path="apps/api/src/db/schema.ts", purpose=( "WICHTIG: BEHALTE alle existierenden Tabellen — füge nur Spalte hinzu. " "Konkret: füge `budgetHours: integer('budget_hours')` (nullable) auf projects-Tabelle. " "BEHALTE explizit: users, customers, projects, projectTemplates, timeEntries, timeEntryAttachments, timeEntryComments, " "appSettings, auditLog, documents, webhooks, savedViews, apiKeys, passwordResetTokens, invitations." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/web/src/pages/Projects.tsx", purpose=( "ERWEITERT — behalte Create-Form. Füge Budget-Spalte: zeigt used/total Stunden mit Progress-Bar pro Project. " "Bei >100% rot. Edit-Modal mit Budget-Input. Verwende api.getProjectStats für hours." ), refs=["apps/web/src/pages/Projects.tsx"], ), ], ), Feature( name="recurring-time-entries", description="Template-System für wiederkehrende Entries (z.B. Daily-Standup 30min)", files=[ FileGen( path="apps/web/src/pages/TimeEntries.tsx", purpose=( "ERWEITERT — füge 'Aus Template' Dropdown im Create-Form, lädt api.listTimeEntryTemplates(), " "Auswahl pre-fillt description/projectId/durationMinutes (durationMinutes ergibt now/now+duration)." ), refs=["apps/web/src/pages/TimeEntries.tsx"], ), ], ), Feature( name="slack-integration-stub", description="Slack-Integration-Stub Card auf Integrations-Page", files=[ FileGen( path="apps/web/src/pages/Integrations.tsx", purpose=( "ERWEITERT — die bestehende Slack-Karte bekommt jetzt 'Configure'-Button (statt 'Coming Soon'). " "Klick öffnet Modal mit Webhook-URL-Input. Submit speichert (würde später als Setting). " "Plus: Test-Button → sendet 'Hello from EmberClone' POST zur URL." ), refs=["apps/web/src/pages/Integrations.tsx"], ), ], ), Feature( name="github-link-on-entries", description="GitHub-Link-Feld pro TimeEntry (z.B. PR-URL)", files=[ FileGen( path="apps/api/src/db/schema.ts", purpose=( "WICHTIG: BEHALTE alle bestehenden Tabellen. Füge nur Spalte `externalLink: text('external_link')` (nullable) zu timeEntries. " "ALLE anderen Tabellen unverändert." ), refs=["apps/api/src/db/schema.ts"], ), FileGen( path="apps/web/src/pages/TimeEntries.tsx", purpose=( "ERWEITERT — behalte alles. Füge im Create-Form optional 'GitHub/Link'-Input. " "In Liste: kleines link-icon wenn externalLink set, hover zeigt URL." ), refs=["apps/web/src/pages/TimeEntries.tsx"], ), ], ), Feature( name="budget-alerts", description="Toast-Warning bei Project-Budget >80% und >100%", files=[ FileGen( path="apps/web/src/pages/Projects.tsx", purpose=( "ERWEITERT — behalte alles. Beim Mount: für jedes Project check budget vs used hours. " "Wenn >80% aber ≤100%: useToast().info('Budget für X bei 85%'). >100%: useToast().error('Budget für X überschritten')." ), refs=["apps/web/src/pages/Projects.tsx"], ), ], ), Feature( name="api-client-phase20", description="API: pinned-customers + budget-update Endpoints", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "ERWEITERT — behalte ALLES. Füge: updateProjectBudget(id, hours), setExternalLink(entryId, url). " "Plus: testSlackWebhook(url) (POST mit json {text:'Hello from EmberClone'})." ), refs=["apps/web/src/lib/api.ts"], ), ], ), ] def load_state() -> dict: if PHASE20_STATE.exists(): return json.loads(PHASE20_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state: dict) -> None: PHASE20_STATE.write_text(json.dumps(state, indent=2)) async def main() -> int: log_section("🚀 Phase-20 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-20 Run beendet") log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}") return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))