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