{
 "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 <TOKEN>",
 "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"
 }
}
