{ "document_type": "developer_concept", "language": "de-CH", "title": "QC Text Builder App - Go-first Implementation Concept", "version": "1.1.0", "status": "ready_for_implementation", "goal": "Eine interne Webanwendung in Go bauen, die Quick Creator ausschliesslich als Website-Builder nutzt und eigene Texte direkt in content.aiData von AI-Templates einspeist.", "executive_summary": { "one_sentence": "Baue eine Go-Webapp, die AI-Templates aus Quick Creator synchronisiert, pro Template via Discovery die internen aiData-Keys ermittelt, diese lokal speichert und danach Websites mit vollständig eigenem Text über POST /sites erzeugt.", "non_negotiables": [ "Nur AI-Templates", "Nur Bearer-Token-Auth", "Keine produktive Nutzung von /generate-content zur Texterzeugung", "Keine DCM- oder EFL-Integration", "Keine externen Bilder oder Dateisysteme", "Nur eigene Texte in content.aiData" ] }, "product_scope": { "in_scope": [ "Quick Creator API Client in Go", "lokaler Template-Katalog", "Template-Onboarding via /generate-content für Key-Discovery", "lokale Persistenz von Template-Manifests", "UI zur Bearbeitung template-spezifischer Textfelder", "Site-Erstellung via POST /sites", "Job-Polling via GET /jobs/{jobId}", "Anzeige von previewUrl und optional editorUrl", "redaktierte API-Logs", "Admin-UI für Settings, Templates, Builds" ], "out_of_scope": [ "DCM", "EFL", "Media-/Bild-Handling", "CRM-Integration im MVP", "nachträgliche Content-Manipulation über HAL/Site API", "Multi-tenant SaaS für Endkunden", "vollautomatische semantische Feldzuordnung über alle Templates" ] }, "source_basis": { "type": "user_provided_docs_only", "used_inputs": [ "Quick Creator API OAS-Auszug", "Quick Creator Schritt-für-Schritt-Anleitung", "DCM Dokumentation", "EFL Dokumentation", "HAL API Objektdefinitionen" ], "important_findings": [ "Auth-Doku ist widersprüchlich; produktiv wird Bearer-Token angenommen.", "content.aiData ist für AI-Templates vorgesehen.", "Template-Keys sind template-spezifisch und müssen exakt getroffen werden.", "Schema-Endpunkt ist nicht verlässlich genug als alleinige Quelle.", "/generate-content eignet sich als Discovery-Quelle für echte Key-Namen." ] }, "primary_architecture_decisions": { "backend_language": "Go", "go_version": "1.24+", "web_stack": { "router": "chi", "rendering": "html/template for MVP", "frontend_enhancement": "HTMX optional", "api_style": "hybrid: HTML admin UI + JSON internal endpoints" }, "database": { "production": "PostgreSQL", "development": "SQLite allowed", "migration_tool": "goose", "query_strategy": "sqlc preferred, sqlx acceptable" }, "background_jobs": { "mode": "in-process goroutines for MVP", "polling": "persistent DB-backed polling states", "later_upgrade": "separate worker process if needed" }, "http_client": { "library": "net/http", "timeouts": "strict per-request timeouts", "retry_logic": "custom retry wrapper" }, "config": { "env_loader": "caarlos0/env or stdlib", "secrets": "env vars or secret manager", "token_storage": "encrypted at rest if persisted" } }, "why_go": { "reasons": [ "Go passt gut für API-Orchestrierung, Polling und robuste Services.", "ein einzelnes statisches Binary vereinfacht Deployment massiv", "geringer Runtime-Overhead", "klare Typisierung für externe JSON-Kontrakte", "gute Eignung für Hintergrundjobs und HTTP-Clients" ], "tradeoffs": [ "weniger komfortabel für dynamische Admin-UIs als Python/JS", "mehr Initialaufwand für Templates/Form-Handling" ] }, "project_layout": { "root_structure": [ "cmd/qctextbuilder/main.go", "internal/app/app.go", "internal/config/config.go", "internal/httpserver/server.go", "internal/httpserver/middleware/", "internal/httpserver/handlers/", "internal/httpserver/views/", "internal/qcclient/client.go", "internal/qcclient/types.go", "internal/qcclient/errors.go", "internal/templatesvc/service.go", "internal/onboarding/service.go", "internal/mapping/service.go", "internal/buildsvc/service.go", "internal/polling/service.go", "internal/store/", "internal/store/postgres/", "internal/store/sqlite/", "internal/domain/", "internal/logging/", "internal/crypto/", "internal/validation/", "migrations/", "web/templates/", "web/static/", "test/integration/" ], "design_rules": [ "Keine Business-Logik in HTTP-Handlern", "Quick Creator JSON-Typen zentral in qcclient/types.go", "Store-Interfaces vom Business-Layer entkoppeln", "Jede externe API-Operation über qcclient kapseln", "Polling-Logik getrennt vom Build-Service halten" ] }, "package_design": { "internal/domain": { "purpose": "interne Kernmodelle unabhängig von DB und HTTP", "types": [ "Template", "TemplateManifest", "TemplateField", "SiteBuild", "AppSettings" ] }, "internal/qcclient": { "purpose": "alle Quick Creator Requests und Response-Typen", "responsibilities": [ "Bearer-Auth setzen", "Request/Response Logging via Hook", "Retry/Timeout/Error-Mapping", "JSON Marshalling/Unmarshalling" ] }, "internal/templatesvc": { "purpose": "Templates synchronisieren und lokal verwalten" }, "internal/onboarding": { "purpose": "Template Discovery, Manifest-Erstellung, Validierung" }, "internal/mapping": { "purpose": "Roh-Key-Eingaben in finale aiData-Struktur assemblieren" }, "internal/buildsvc": { "purpose": "Sites bauen, Payloads speichern, Polling anstossen" }, "internal/polling": { "purpose": "laufende Job-Abfragen gegen Quick Creator" }, "internal/store": { "purpose": "Persistenz-Abstraktion" }, "internal/httpserver/handlers": { "purpose": "Admin UI und JSON-Endpunkte" } }, "domain_model": { "Template": { "fields": { "ID": "int64", "Name": "string", "Description": "string", "Locale": "string", "ThumbnailURL": "string", "TemplatePreviewURL": "string", "Type": "string", "PaletteReady": "bool", "RawJSON": "json.RawMessage", "IsAITemplate": "bool", "IsOnboarded": "bool", "ManifestStatus": "string", "LastDiscoveredAt": "time.Time|null" } }, "TemplateManifest": { "fields": { "ID": "uuid/string", "TemplateID": "int64", "Version": "int", "Source": "string", "LanguageUsedForDiscovery": "string", "DiscoveryPayloadJSON": "json.RawMessage", "DiscoveryResponseJSON": "json.RawMessage", "FlattenedManifestJSON": "json.RawMessage", "IsActive": "bool", "CreatedAt": "time.Time", "UpdatedAt": "time.Time" } }, "TemplateField": { "fields": { "ID": "uuid/string", "TemplateID": "int64", "ManifestID": "uuid/string", "Section": "string", "KeyName": "string", "Path": "string", "FieldKind": "string", "SampleValue": "string", "IsEnabled": "bool", "IsRequiredByUs": "bool", "DisplayLabel": "string", "DisplayOrder": "int", "Notes": "string" } }, "SiteBuild": { "fields": { "ID": "uuid/string", "TemplateID": "int64", "ManifestID": "uuid/string", "RequestName": "string", "GlobalDataJSON": "json.RawMessage", "AIDataJSON": "json.RawMessage", "FinalSitesPayloadJSON": "json.RawMessage", "QCJobID": "int64|null", "QCSiteID": "int64|null", "QCStatus": "string", "QCPreviewURL": "string", "QCEditorURL": "string", "QCResultJSON": "json.RawMessage", "QCErrorJSON": "json.RawMessage", "StartedAt": "time.Time|null", "FinishedAt": "time.Time|null" } }, "AppSettings": { "fields": { "QCBaseURL": "string", "QCBearerTokenEncrypted": "string", "LanguageOutputMode": "string", "JobPollIntervalSeconds": "int", "JobPollTimeoutSeconds": "int" } } }, "database_schema": { "notes": [ "PostgreSQL bevorzugt", "SQLite für lokale Entwicklung zulässig", "JSONB in PostgreSQL verwenden", "bei SQLite JSON als TEXT speichern" ], "tables": [ { "name": "app_settings", "columns": { "id": "uuid pk", "qc_base_url": "text not null", "qc_bearer_token_encrypted": "text not null", "language_output_mode": "text not null default 'upper'", "job_poll_interval_seconds": "integer not null default 5", "job_poll_timeout_seconds": "integer not null default 300", "created_at": "timestamp not null", "updated_at": "timestamp not null" } }, { "name": "qc_templates", "columns": { "id": "bigint pk", "name": "text not null", "description": "text not null", "locale": "text not null", "thumbnail_url": "text", "template_preview_url": "text", "type": "text not null", "palette_ready": "boolean not null", "raw_template_json": "json/jsonb not null", "is_ai_template": "boolean not null", "is_onboarded": "boolean not null default false", "manifest_status": "text not null default 'missing'", "last_discovered_at": "timestamp", "created_at": "timestamp not null", "updated_at": "timestamp not null" } }, { "name": "qc_template_manifests", "columns": { "id": "uuid pk", "template_id": "bigint not null references qc_templates(id)", "manifest_version": "integer not null", "source": "text not null", "language_used_for_discovery": "text not null", "discovery_payload_json": "json/jsonb not null", "discovery_response_json": "json/jsonb not null", "flattened_manifest_json": "json/jsonb not null", "is_active": "boolean not null default true", "created_at": "timestamp not null", "updated_at": "timestamp not null" } }, { "name": "qc_template_fields", "columns": { "id": "uuid pk", "template_id": "bigint not null references qc_templates(id)", "manifest_id": "uuid not null references qc_template_manifests(id)", "section": "text not null", "key_name": "text not null", "path": "text not null", "field_kind": "text not null", "sample_value": "text", "is_enabled": "boolean not null default true", "is_required_by_us": "boolean not null default false", "display_label": "text", "display_order": "integer not null default 0", "notes": "text", "created_at": "timestamp not null", "updated_at": "timestamp not null" }, "indexes": [ "unique(template_id, manifest_id, path)" ] }, { "name": "site_builds", "columns": { "id": "uuid pk", "template_id": "bigint not null references qc_templates(id)", "manifest_id": "uuid not null references qc_template_manifests(id)", "request_name": "text not null", "global_data_json": "json/jsonb not null", "ai_data_json": "json/jsonb not null", "final_sites_payload_json": "json/jsonb not null", "qc_job_id": "bigint", "qc_site_id": "bigint", "qc_status": "text not null", "qc_preview_url": "text", "qc_editor_url": "text", "qc_result_json": "json/jsonb", "qc_error_json": "json/jsonb", "started_at": "timestamp", "finished_at": "timestamp", "created_at": "timestamp not null", "updated_at": "timestamp not null" }, "indexes": [ "index(qc_status)", "index(qc_job_id)" ] }, { "name": "api_logs", "columns": { "id": "uuid pk", "scope": "text not null", "method": "text not null", "url": "text not null", "request_headers_redacted_json": "json/jsonb not null", "request_body_json": "json/jsonb", "response_status": "integer", "response_body_json": "json/jsonb", "duration_ms": "integer", "created_at": "timestamp not null" } } ] }, "quick_creator_api_contract": { "base_url": "https://qc-api.yggdrasil.dev-mono.net/api/v1", "auth": { "mode": "Bearer Token", "header": "Authorization: Bearer ", "mvp_policy": "Nur diesen Auth-Modus implementieren", "explicitly_not_used": [ "/app/login" ] }, "endpoints_used": [ { "method": "GET", "path": "/health", "purpose": "Health/Reachability" }, { "method": "GET", "path": "/templates?type=ai", "purpose": "AI-Templates laden" }, { "method": "GET", "path": "/templates/{templateId}", "purpose": "Template-Details" }, { "method": "GET", "path": "/templates/{templateId}/schema", "purpose": "optionale Zusatzdaten, nicht Quelle der Wahrheit" }, { "method": "POST", "path": "/generate-content", "purpose": "nur Discovery / Key-Extraktion" }, { "method": "POST", "path": "/sites", "purpose": "produktive Site-Erstellung" }, { "method": "GET", "path": "/jobs/{jobId}", "purpose": "Polling / previewUrl" }, { "method": "GET", "path": "/sites/{siteId}/editor-url", "purpose": "optional editor login url" } ] }, "go_types_qcclient": { "notes": [ "Diese Typen in internal/qcclient/types.go definieren", "Externe Response-Envelope immer als eigenes Typmodell abbilden" ], "types": { "APIResponse": "type APIResponse[T any] struct { Status string `json:\"status\"`; Data T `json:\"data\"` }", "APIError": "type APIError struct { Status string `json:\"status\"`; Message string `json:\"message\"`; Code string `json:\"code\"`; Details map[string]any `json:\"details,omitempty\"` }", "Template": "type Template struct { ID int64 `json:\"id\"`; Name string `json:\"name\"`; Description string `json:\"description\"`; Locale string `json:\"locale\"`; ThumbnailURL string `json:\"thumbnailUrl\"`; TemplatePreviewURL string `json:\"templatePreviewUrl\"`; Type string `json:\"type\"`; PaletteReady bool `json:\"paletteReady\"`; GlobalColors map[string]any `json:\"globalColors\"`; TextsWithGlobalColors map[string]any `json:\"textsWithGlobalColors\"`; TemplateHeadings []TemplateHeading `json:\"templateHeadings,omitempty\"` }", "TemplateHeading": "type TemplateHeading struct { TranslationKey string `json:\"translationKey\"` }", "GenerateContentRequest": "type GenerateContentRequest struct { TemplateID int64 `json:\"templateId\"`; GlobalData map[string]any `json:\"globalData\"`; Empty bool `json:\"empty\"`; ToneOfVoice string `json:\"toneOfVoice,omitempty\"`; TargetAudience string `json:\"targetAudience,omitempty\"`; CustomTemplateData map[string]any `json:\"customTemplateData,omitempty\"` }", "GenerateContentData": "type GenerateContentData map[string]map[string]any", "CreateSiteRequest": "type CreateSiteRequest struct { TemplateID int64 `json:\"templateId\"`; GlobalData map[string]any `json:\"globalData\"`; Content CreateSiteContent `json:\"content\"` }", "CreateSiteContent": "type CreateSiteContent struct { AIData map[string]map[string]any `json:\"aiData\"` }", "CreateSiteResponseData": "type CreateSiteResponseData struct { Status string `json:\"status\"`; JobID int64 `json:\"jobId\"` }", "JobStatusResult": "type JobStatusResult struct { SiteID int64 `json:\"siteId\"`; PreviewURL string `json:\"previewUrl\"` }", "JobStatusData": "type JobStatusData struct { JobID int64 `json:\"jobId\"`; Status string `json:\"status\"`; CreatedAt int64 `json:\"createdAt\"`; Result JobStatusResult `json:\"result\"` }", "SiteEditorLoginData": "type SiteEditorLoginData struct { LoginURL string `json:\"loginUrl\"` }" } }, "qcclient_interface_design": { "interface": "type Client interface { Health(ctx context.Context) error; ListAITemplates(ctx context.Context) ([]Template, error); GetTemplate(ctx context.Context, templateID int64) (*Template, error); GetTemplateSchema(ctx context.Context, templateID int64) (json.RawMessage, error); GenerateContent(ctx context.Context, req GenerateContentRequest) (GenerateContentData, json.RawMessage, error); CreateSite(ctx context.Context, req CreateSiteRequest) (*CreateSiteResponseData, json.RawMessage, error); GetJob(ctx context.Context, jobID int64) (*JobStatusData, json.RawMessage, error); GetEditorURL(ctx context.Context, siteID int64) (*SiteEditorLoginData, json.RawMessage, error) }", "implementation_rules": [ "Request body und Response body für Logs separat puffern", "Authorization-Header in Logs immer redaktieren", "400/401/404 nicht retrien", "429/5xx mit Exponential Backoff retrien", "Fehlerantworten in APIError zu mappen versuchen, sonst Raw Body behalten" ] }, "template_onboarding_strategy": { "summary": "Pro AI-Template einmalige Discovery durchführen und Feldmanifest lokal speichern.", "steps": [ "1. GET /templates?type=ai synchronisieren.", "2. Ein Template auswählen.", "3. Optional /templates/{templateId} und /templates/{templateId}/schema speichern.", "4. Discovery via POST /generate-content mit Dummy-Daten ausführen.", "5. Response data rekursiv flatten und section/key/path extrahieren.", "6. Felder mit sample_value '#IMAGE#' oder ähnlichem als image markieren und standardmässig deaktivieren.", "7. Textfelder als enabled markieren.", "8. Manifest und Fields in DB speichern.", "9. UI-Review zulassen: Labels, Reihenfolge, Aktivierung anpassen.", "10. Validierungs-Build mit Minimaltexten fahren.", "11. Bei Erfolg Manifeststatus = validated." ], "discovery_payload_default": { "templateId": 1378062, "globalData": { "companyName": "Discovery Company", "businessType": "dentist", "siteLanguage": "EN", "email": "discovery@example.com", "phone": "+41 44 000 00 00", "address": { "line1": "Discovery Street 1", "line2": "", "city": "Zurich", "region": "ZH", "postalCode": "8000", "country": "CH" } }, "empty": false, "toneOfVoice": "Professional", "targetAudience": "B2B" }, "flattening_algorithm": { "goal": "Aus GenerateContentData ein persistierbares Feldmanifest bauen", "pseudocode": [ "für jede section im top-level object", " für jeden key/value in section object", " path = section + '.' + key", " sample = stringify(value)", " field_kind = detectFieldKind(sample)", " persist field" ], "field_kind_rules": [ "sample == '#IMAGE#' => image", "string => text", "sonst => unknown" ] } }, "mapping_service_design": { "summary": "Im MVP wird raw-key-mode verwendet: interne App arbeitet direkt mit template-spezifischen Roh-Keys.", "interface": "type Service interface { AssembleAIData(fields []TemplateField, values map[string]string) (map[string]map[string]any, error) }", "rules": [ "Nur aktivierte Felder berücksichtigen", "Nur bekannte Paths akzeptieren", "Leere Strings standardmässig weglassen", "image/unknown-Felder im MVP nicht senden", "sections nur senden, wenn mindestens ein Feld vorhanden ist" ], "input_example": { "fieldValues": { "text.textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich", "text.textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung.", "services.servicesTitle_r4694_154": "Unsere Leistungen", "services.servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin." } }, "output_example": { "text": { "textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich", "textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung." }, "services": { "servicesTitle_r4694_154": "Unsere Leistungen", "servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin." } } }, "site_build_strategy": { "summary": "Produktiv wird nur POST /sites mit eigener aiData verwendet.", "rules": [ "Template muss AI sein", "validiertes oder mindestens reviewed Manifest muss existieren", "globalData.companyName und globalData.email sind Pflicht", "globalData.username explizit setzen", "accountId weglassen", "colorPalette weglassen", "content.files weglassen", "nur textliche aiData senden" ], "final_payload_example": { "templateId": 1378062, "globalData": { "companyName": "Muster AG", "businessType": "dentist", "username": "muster-ag-zuerich", "email": "info@muster.ch", "phone": "+41 44 123 45 67", "siteLanguage": "DE", "address": { "line1": "Bahnhofstrasse 1", "line2": "", "city": "Zuerich", "region": "ZH", "zip": "8001", "country": "CH" } }, "content": { "aiData": { "text": { "textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich", "textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung im Herzen von Zürich." }, "services": { "servicesTitle_r4694_154": "Unsere Leistungen", "servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin." }, "testimonials": { "testimonialsName_c3006_153": "Sabine M.", "testimonialsDescription_c3006_152": "Freundlich, professionell und sehr angenehm." } } } } }, "build_service_design": { "interface": "type Service interface { StartBuild(ctx context.Context, req StartBuildRequest) (*BuildResult, error); PollOnce(ctx context.Context, buildID string) error; FetchEditorURL(ctx context.Context, buildID string) error }", "start_build_flow": [ "1. Template laden", "2. aktives Manifest laden", "3. Input validieren", "4. aiData via mapping service assemblieren", "5. finales /sites Payload erzeugen", "6. SiteBuild Datensatz im Status draft oder queued anlegen", "7. POST /sites ausführen", "8. jobId speichern und Status queued setzen", "9. Polling-Worker triggern", "10. BuildResult an UI zurückgeben" ], "StartBuildRequest_type": { "fields": { "TemplateID": "int64", "RequestName": "string", "GlobalData": "map[string]any", "FieldValues": "map[string]string" } }, "BuildResult_type": { "fields": { "BuildID": "string", "QCJobID": "int64", "Status": "string" } } }, "polling_service_design": { "summary": "DB-backed Polling mit goroutines, aber ohne separate Queue im MVP.", "worker_model": { "mode": "ticker-based supervisor + per-build poll execution", "default_interval_seconds": 5, "default_timeout_seconds": 300 }, "algorithm": [ "1. Supervisor sucht regelmässig site_builds mit qc_status IN ('queued','processing').", "2. Für jeden Build PollOnce ausführen.", "3. GET /jobs/{jobId} aufrufen.", "4. Wenn result status done => previewUrl/siteId speichern, finished_at setzen, qc_status=done.", "5. Wenn externer Fehler => qc_error_json speichern und qc_status=failed.", "6. Wenn Timeout überschritten => qc_status=timeout." ], "concurrency_rules": [ "Nicht denselben Build parallel pollen", "optimistic locking oder advisory flag verwenden", "max concurrent polls konfigurierbar" ] }, "internal_http_api": { "json_endpoints": [ { "method": "POST", "path": "/api/settings/test-qc-connection", "purpose": "Health/Templates-Test" }, { "method": "POST", "path": "/api/templates/sync", "purpose": "AI-Templates synchronisieren" }, { "method": "GET", "path": "/api/templates", "purpose": "lokale Templates" }, { "method": "GET", "path": "/api/templates/{id}", "purpose": "Template inkl. Manifest und Fields" }, { "method": "POST", "path": "/api/templates/{id}/onboard", "purpose": "Discovery ausführen" }, { "method": "POST", "path": "/api/templates/{id}/validate", "purpose": "Minimaler Test-Build" }, { "method": "PUT", "path": "/api/templates/{id}/fields", "purpose": "Field-Manifest anpassen" }, { "method": "POST", "path": "/api/site-builds", "purpose": "Build starten" }, { "method": "GET", "path": "/api/site-builds/{id}", "purpose": "Build-Details" }, { "method": "POST", "path": "/api/site-builds/{id}/poll", "purpose": "manuelles Polling" }, { "method": "POST", "path": "/api/site-builds/{id}/fetch-editor-url", "purpose": "optionale editorUrl laden" } ], "html_routes": [ "/settings", "/templates", "/templates/{id}", "/builds/new", "/builds/{id}" ] }, "ui_specification": { "mvp_style": "server rendered Go templates with optional HTMX partial updates", "screens": [ { "name": "Settings", "features": [ "QC Base URL", "Bearer Token speichern", "Connection Test", "Polling-Intervall konfigurieren", "Timeout konfigurieren", "Language Output Mode konfigurieren" ] }, { "name": "Templates", "features": [ "AI-Templates listen", "Onboarding-Status anzeigen", "Template Preview URL anzeigen", "Sync-Button" ] }, { "name": "Template Detail", "features": [ "Raw Template JSON", "Schema Raw JSON optional", "Discovery starten", "Felder anzeigen", "Felder aktiv/deaktivieren", "DisplayLabel und Reihenfolge pflegen", "Manifest-Status" ] }, { "name": "New Build", "features": [ "Template auswählen", "GlobalData Formular", "alle aktivierten Textfelder als Formular rendern", "finales aiData JSON vorab anzeigen", "finales /sites Payload anzeigen", "Build starten" ] }, { "name": "Build Detail", "features": [ "Build-Status", "QC jobId", "QC siteId", "previewUrl", "editorUrl optional", "Request-/Response-Snapshots", "Fehlerdetails" ] } ] }, "validation_rules": { "before_onboarding": [ "QC Token vorhanden", "Template existiert lokal oder kann geladen werden", "Template.Type == 'AI'" ], "before_build": [ "Template ist AI", "aktives Manifest vorhanden", "Manifeststatus in reviewed oder validated", "companyName gesetzt", "email gesetzt", "username gesetzt", "mindestens ein aktiver Textwert vorhanden" ], "field_rules": [ "Unbekannte fieldValues-Keys ablehnen", "deaktivierte Felder ignorieren oder ablehnen", "nur field_kind=text im MVP senden", "Leere Strings standardmässig nicht senden", "nil/null nicht an Quick Creator senden" ], "language_rules": [ "intern Sprache als uppercase 2-letter speichern", "outgoing default uppercase", "bei BAD_REQUEST optional 1 lower-case Retry" ] }, "logging_and_observability": { "logging": [ "Structured JSON Logs", "jede externe QC-Request mit redaktierten Headers loggen", "jede externe QC-Response mit Status und Dauer loggen", "Build-Zustandswechsel loggen", "Discovery-Läufe vollständig loggen" ], "metrics": [ "qc_requests_total", "qc_request_duration_ms", "template_sync_total", "template_onboard_total", "template_onboard_failed_total", "site_build_started_total", "site_build_done_total", "site_build_failed_total", "site_build_timeout_total" ], "redaction_rules": [ "Authorization Header nie im Klartext loggen", "Tokens in UI nie anzeigen", "sensible Felder in Snapshots optional maskieren" ] }, "security": { "requirements": [ "Token verschlüsselt speichern, z. B. AES-GCM mit App Key aus ENV", "Admin-Zugriff absichern", "CSRF-Schutz falls server-rendered Form-POSTs", "Secure Cookies bei Session-basierter Admin-Auth", "Role-based access für Settings und Logs" ] }, "implementation_details_go": { "config_struct": "type Config struct { HTTPAddr string `env:\"HTTP_ADDR\" envDefault\":8080\"`; DBDriver string `env:\"DB_DRIVER\" envDefault:\"postgres\"`; DBURL string `env:\"DB_URL,required\"`; AppSecret string `env:\"APP_SECRET,required\"`; QCBaseURL string `env:\"QC_BASE_URL\" envDefault:\"https://qc-api.yggdrasil.dev-mono.net/api/v1\"`; QCToken string `env:\"QC_TOKEN\"`; PollIntervalSeconds int `env:\"POLL_INTERVAL_SECONDS\" envDefault:\"5\"`; PollTimeoutSeconds int `env:\"POLL_TIMEOUT_SECONDS\" envDefault:\"300\"` }", "http_server_setup": [ "chi.NewRouter()", "request id middleware", "recoverer middleware", "structured logger middleware", "timeout middleware" ], "store_interfaces": { "TemplateStore": "UpsertTemplates, GetTemplateByID, ListTemplates, SetTemplateManifestStatus", "ManifestStore": "CreateManifest, GetActiveManifestByTemplateID, ReplaceFields, ListFieldsByManifestID, UpdateFieldSettings", "BuildStore": "CreateBuild, UpdateBuildQueued, UpdateBuildDone, UpdateBuildFailed, GetBuildByID, ListActiveBuilds", "SettingsStore": "GetSettings, SaveSettings" }, "error_style": { "rule": "wrap errors with context using fmt.Errorf(\"...: %w\", err)", "typed_errors": [ "ErrUnauthorized", "ErrBadRequest", "ErrNotFound", "ErrInvalidTemplateType", "ErrManifestMissing", "ErrBuildTimeout" ] } }, "testing_strategy": { "unit_tests": [ "Flatten GenerateContentData zu Fields", "AssembleAIData mit enabled/disabled fields", "Validation von fieldValues", "Sprache-Normalisierung", "Error-Mapping von QC-Responses" ], "integration_tests": [ "ListAITemplates gegen echte Dev-API", "Onboarding eines echten AI-Templates", "CreateSite mit Minimalpayload", "Polling bis done", "Fetch editorUrl optional" ], "manual_tests": [ "Settings speichern und Verbindung testen", "Templates synchronisieren", "Template onboarden", "Labels im UI anpassen", "Build mit eigenem Text starten", "Preview öffnen und Texte visuell prüfen" ] }, "acceptance_criteria": { "must_have": [ "Die Go-App kompiliert zu einem deploybaren Binary.", "Die App kann mit Bearer-Token AI-Templates synchronisieren.", "Die App kann ein AI-Template onboarden und die aiData-Key-Struktur speichern.", "Die App kann eigene Texte auf gespeicherte Template-Keys mappen.", "Die App kann eine Site über POST /sites bauen.", "Die App kann den Jobstatus pollen und previewUrl anzeigen.", "Alle QC-Requests/Responses werden redaktiert protokolliert.", "Es wird kein DCM/EFL verwendet." ], "must_not": [ "Kein produktiver Einsatz von /generate-content zur Texterzeugung", "Keine Standard-Templates", "Keine Bild-/Media-Integration", "Keine unbekannten Keys im finalen aiData" ] }, "implementation_plan": [ { "phase": 1, "name": "Bootstrap", "tasks": [ "Go-Modul anlegen", "Config laden", "DB initialisieren", "Migrationsstruktur anlegen", "HTTP-Server Grundgerüst bauen" ] }, { "phase": 2, "name": "QC Client", "tasks": [ "HTTP Client Wrapper bauen", "Response Envelope-Typen bauen", "Health/ListTemplates/GetTemplate/GetSchema implementieren", "Logging/Retry/Error-Mapping implementieren" ] }, { "phase": 3, "name": "Template Catalog & Onboarding", "tasks": [ "Template Sync Service bauen", "Discovery via /generate-content bauen", "Flattening und Manifest-Persistenz bauen", "Template-Detail-UI bauen" ] }, { "phase": 4, "name": "Build Flow", "tasks": [ "Mapping Service bauen", "Build Form bauen", "POST /sites integrieren", "SiteBuild Persistenz bauen" ] }, { "phase": 5, "name": "Polling & Detail View", "tasks": [ "Polling Worker bauen", "Build Detail View bauen", "previewUrl/editorUrl Handling ergänzen" ] }, { "phase": 6, "name": "Härtung", "tasks": [ "Token-Verschlüsselung", "CSRF/Auth falls nötig", "Retry/Fallback Feinschliff", "Tests und Fehlerbilder" ] } ], "prohibitions": [ "Nicht /app/login als produktiven Auth-Pfad implementieren.", "Nicht Standard-Templates zulassen.", "Nicht DCM/EFL mit reinmischen.", "Nicht unbekannte oder deaktivierte Keys an Quick Creator senden.", "Nicht image-Felder im MVP befüllen.", "Nicht Geschäftslogik in Handlern ablegen.", "Nicht direkt aus UI gegen Quick Creator callen; immer über Service-Layer." ], "safe_defaults": { "auth": "Bearer Token only", "language": "uppercase on first try", "manifest_source_of_truth": "/generate-content response", "empty_values": "skip", "images": "disabled", "poll_interval_seconds": 5, "poll_timeout_seconds": 300 }, "example_build_request_to_our_app": { "templateId": 1378062, "requestName": "Muster AG Zürich", "globalData": { "companyName": "Muster AG", "businessType": "dentist", "username": "muster-ag-zuerich", "email": "info@muster.ch", "phone": "+41 44 123 45 67", "siteLanguage": "DE", "address": { "line1": "Bahnhofstrasse 1", "line2": "", "city": "Zuerich", "region": "ZH", "zip": "8001", "country": "CH" } }, "fieldValues": { "text.textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich", "text.textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung im Herzen von Zürich.", "services.servicesTitle_r4694_154": "Unsere Leistungen", "services.servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin.", "testimonials.testimonialsName_c3006_153": "Sabine M.", "testimonials.testimonialsDescription_c3006_152": "Freundlich, professionell und sehr angenehm." } }, "example_final_qc_payload": { "templateId": 1378062, "globalData": { "companyName": "Muster AG", "businessType": "dentist", "username": "muster-ag-zuerich", "email": "info@muster.ch", "phone": "+41 44 123 45 67", "siteLanguage": "DE", "address": { "line1": "Bahnhofstrasse 1", "line2": "", "city": "Zuerich", "region": "ZH", "zip": "8001", "country": "CH" } }, "content": { "aiData": { "text": { "textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich", "textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung im Herzen von Zürich." }, "services": { "servicesTitle_r4694_154": "Unsere Leistungen", "servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin." }, "testimonials": { "testimonialsName_c3006_153": "Sabine M.", "testimonialsDescription_c3006_152": "Freundlich, professionell und sehr angenehm." } } } }, "developer_summary": { "instruction": "Implementiere die App vollständig in Go mit chi, sqlc/goose und einem klar getrennten Service-Layer. Verwende Quick Creator nur für AI-Template-Sync, Discovery der aiData-Key-Struktur und anschliessend Site-Builds mit eigenem Text über POST /sites.", "first_milestone": "Template Sync + Onboarding eines AI-Templates + gespeichertes Field-Manifest", "second_milestone": "Site-Build mit eigenem Text + Job-Polling + previewUrl" } }