EmberClone/scripts/phase20_features.py

160 lines
6.4 KiB
Python

#!/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()))