25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

1056 satır
32KB

  1. {
  2. "document_type": "developer_concept",
  3. "language": "de-CH",
  4. "title": "QC Text Builder App - Go-first Implementation Concept",
  5. "version": "1.1.0",
  6. "status": "ready_for_implementation",
  7. "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.",
  8. "executive_summary": {
  9. "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.",
  10. "non_negotiables": [
  11. "Nur AI-Templates",
  12. "Nur Bearer-Token-Auth",
  13. "Keine produktive Nutzung von /generate-content zur Texterzeugung",
  14. "Keine DCM- oder EFL-Integration",
  15. "Keine externen Bilder oder Dateisysteme",
  16. "Nur eigene Texte in content.aiData"
  17. ]
  18. },
  19. "product_scope": {
  20. "in_scope": [
  21. "Quick Creator API Client in Go",
  22. "lokaler Template-Katalog",
  23. "Template-Onboarding via /generate-content für Key-Discovery",
  24. "lokale Persistenz von Template-Manifests",
  25. "UI zur Bearbeitung template-spezifischer Textfelder",
  26. "Site-Erstellung via POST /sites",
  27. "Job-Polling via GET /jobs/{jobId}",
  28. "Anzeige von previewUrl und optional editorUrl",
  29. "redaktierte API-Logs",
  30. "Admin-UI für Settings, Templates, Builds"
  31. ],
  32. "out_of_scope": [
  33. "DCM",
  34. "EFL",
  35. "Media-/Bild-Handling",
  36. "CRM-Integration im MVP",
  37. "nachträgliche Content-Manipulation über HAL/Site API",
  38. "Multi-tenant SaaS für Endkunden",
  39. "vollautomatische semantische Feldzuordnung über alle Templates"
  40. ]
  41. },
  42. "source_basis": {
  43. "type": "user_provided_docs_only",
  44. "used_inputs": [
  45. "Quick Creator API OAS-Auszug",
  46. "Quick Creator Schritt-für-Schritt-Anleitung",
  47. "DCM Dokumentation",
  48. "EFL Dokumentation",
  49. "HAL API Objektdefinitionen"
  50. ],
  51. "important_findings": [
  52. "Auth-Doku ist widersprüchlich; produktiv wird Bearer-Token angenommen.",
  53. "content.aiData ist für AI-Templates vorgesehen.",
  54. "Template-Keys sind template-spezifisch und müssen exakt getroffen werden.",
  55. "Schema-Endpunkt ist nicht verlässlich genug als alleinige Quelle.",
  56. "/generate-content eignet sich als Discovery-Quelle für echte Key-Namen."
  57. ]
  58. },
  59. "primary_architecture_decisions": {
  60. "backend_language": "Go",
  61. "go_version": "1.24+",
  62. "web_stack": {
  63. "router": "chi",
  64. "rendering": "html/template for MVP",
  65. "frontend_enhancement": "HTMX optional",
  66. "api_style": "hybrid: HTML admin UI + JSON internal endpoints"
  67. },
  68. "database": {
  69. "production": "PostgreSQL",
  70. "development": "SQLite allowed",
  71. "migration_tool": "goose",
  72. "query_strategy": "sqlc preferred, sqlx acceptable"
  73. },
  74. "background_jobs": {
  75. "mode": "in-process goroutines for MVP",
  76. "polling": "persistent DB-backed polling states",
  77. "later_upgrade": "separate worker process if needed"
  78. },
  79. "http_client": {
  80. "library": "net/http",
  81. "timeouts": "strict per-request timeouts",
  82. "retry_logic": "custom retry wrapper"
  83. },
  84. "config": {
  85. "env_loader": "caarlos0/env or stdlib",
  86. "secrets": "env vars or secret manager",
  87. "token_storage": "encrypted at rest if persisted"
  88. }
  89. },
  90. "why_go": {
  91. "reasons": [
  92. "Go passt gut für API-Orchestrierung, Polling und robuste Services.",
  93. "ein einzelnes statisches Binary vereinfacht Deployment massiv",
  94. "geringer Runtime-Overhead",
  95. "klare Typisierung für externe JSON-Kontrakte",
  96. "gute Eignung für Hintergrundjobs und HTTP-Clients"
  97. ],
  98. "tradeoffs": [
  99. "weniger komfortabel für dynamische Admin-UIs als Python/JS",
  100. "mehr Initialaufwand für Templates/Form-Handling"
  101. ]
  102. },
  103. "project_layout": {
  104. "root_structure": [
  105. "cmd/qctextbuilder/main.go",
  106. "internal/app/app.go",
  107. "internal/config/config.go",
  108. "internal/httpserver/server.go",
  109. "internal/httpserver/middleware/",
  110. "internal/httpserver/handlers/",
  111. "internal/httpserver/views/",
  112. "internal/qcclient/client.go",
  113. "internal/qcclient/types.go",
  114. "internal/qcclient/errors.go",
  115. "internal/templatesvc/service.go",
  116. "internal/onboarding/service.go",
  117. "internal/mapping/service.go",
  118. "internal/buildsvc/service.go",
  119. "internal/polling/service.go",
  120. "internal/store/",
  121. "internal/store/postgres/",
  122. "internal/store/sqlite/",
  123. "internal/domain/",
  124. "internal/logging/",
  125. "internal/crypto/",
  126. "internal/validation/",
  127. "migrations/",
  128. "web/templates/",
  129. "web/static/",
  130. "test/integration/"
  131. ],
  132. "design_rules": [
  133. "Keine Business-Logik in HTTP-Handlern",
  134. "Quick Creator JSON-Typen zentral in qcclient/types.go",
  135. "Store-Interfaces vom Business-Layer entkoppeln",
  136. "Jede externe API-Operation über qcclient kapseln",
  137. "Polling-Logik getrennt vom Build-Service halten"
  138. ]
  139. },
  140. "package_design": {
  141. "internal/domain": {
  142. "purpose": "interne Kernmodelle unabhängig von DB und HTTP",
  143. "types": [
  144. "Template",
  145. "TemplateManifest",
  146. "TemplateField",
  147. "SiteBuild",
  148. "AppSettings"
  149. ]
  150. },
  151. "internal/qcclient": {
  152. "purpose": "alle Quick Creator Requests und Response-Typen",
  153. "responsibilities": [
  154. "Bearer-Auth setzen",
  155. "Request/Response Logging via Hook",
  156. "Retry/Timeout/Error-Mapping",
  157. "JSON Marshalling/Unmarshalling"
  158. ]
  159. },
  160. "internal/templatesvc": {
  161. "purpose": "Templates synchronisieren und lokal verwalten"
  162. },
  163. "internal/onboarding": {
  164. "purpose": "Template Discovery, Manifest-Erstellung, Validierung"
  165. },
  166. "internal/mapping": {
  167. "purpose": "Roh-Key-Eingaben in finale aiData-Struktur assemblieren"
  168. },
  169. "internal/buildsvc": {
  170. "purpose": "Sites bauen, Payloads speichern, Polling anstossen"
  171. },
  172. "internal/polling": {
  173. "purpose": "laufende Job-Abfragen gegen Quick Creator"
  174. },
  175. "internal/store": {
  176. "purpose": "Persistenz-Abstraktion"
  177. },
  178. "internal/httpserver/handlers": {
  179. "purpose": "Admin UI und JSON-Endpunkte"
  180. }
  181. },
  182. "domain_model": {
  183. "Template": {
  184. "fields": {
  185. "ID": "int64",
  186. "Name": "string",
  187. "Description": "string",
  188. "Locale": "string",
  189. "ThumbnailURL": "string",
  190. "TemplatePreviewURL": "string",
  191. "Type": "string",
  192. "PaletteReady": "bool",
  193. "RawJSON": "json.RawMessage",
  194. "IsAITemplate": "bool",
  195. "IsOnboarded": "bool",
  196. "ManifestStatus": "string",
  197. "LastDiscoveredAt": "time.Time|null"
  198. }
  199. },
  200. "TemplateManifest": {
  201. "fields": {
  202. "ID": "uuid/string",
  203. "TemplateID": "int64",
  204. "Version": "int",
  205. "Source": "string",
  206. "LanguageUsedForDiscovery": "string",
  207. "DiscoveryPayloadJSON": "json.RawMessage",
  208. "DiscoveryResponseJSON": "json.RawMessage",
  209. "FlattenedManifestJSON": "json.RawMessage",
  210. "IsActive": "bool",
  211. "CreatedAt": "time.Time",
  212. "UpdatedAt": "time.Time"
  213. }
  214. },
  215. "TemplateField": {
  216. "fields": {
  217. "ID": "uuid/string",
  218. "TemplateID": "int64",
  219. "ManifestID": "uuid/string",
  220. "Section": "string",
  221. "KeyName": "string",
  222. "Path": "string",
  223. "FieldKind": "string",
  224. "SampleValue": "string",
  225. "IsEnabled": "bool",
  226. "IsRequiredByUs": "bool",
  227. "DisplayLabel": "string",
  228. "DisplayOrder": "int",
  229. "Notes": "string"
  230. }
  231. },
  232. "SiteBuild": {
  233. "fields": {
  234. "ID": "uuid/string",
  235. "TemplateID": "int64",
  236. "ManifestID": "uuid/string",
  237. "RequestName": "string",
  238. "GlobalDataJSON": "json.RawMessage",
  239. "AIDataJSON": "json.RawMessage",
  240. "FinalSitesPayloadJSON": "json.RawMessage",
  241. "QCJobID": "int64|null",
  242. "QCSiteID": "int64|null",
  243. "QCStatus": "string",
  244. "QCPreviewURL": "string",
  245. "QCEditorURL": "string",
  246. "QCResultJSON": "json.RawMessage",
  247. "QCErrorJSON": "json.RawMessage",
  248. "StartedAt": "time.Time|null",
  249. "FinishedAt": "time.Time|null"
  250. }
  251. },
  252. "AppSettings": {
  253. "fields": {
  254. "QCBaseURL": "string",
  255. "QCBearerTokenEncrypted": "string",
  256. "LanguageOutputMode": "string",
  257. "JobPollIntervalSeconds": "int",
  258. "JobPollTimeoutSeconds": "int"
  259. }
  260. }
  261. },
  262. "database_schema": {
  263. "notes": [
  264. "PostgreSQL bevorzugt",
  265. "SQLite für lokale Entwicklung zulässig",
  266. "JSONB in PostgreSQL verwenden",
  267. "bei SQLite JSON als TEXT speichern"
  268. ],
  269. "tables": [
  270. {
  271. "name": "app_settings",
  272. "columns": {
  273. "id": "uuid pk",
  274. "qc_base_url": "text not null",
  275. "qc_bearer_token_encrypted": "text not null",
  276. "language_output_mode": "text not null default 'upper'",
  277. "job_poll_interval_seconds": "integer not null default 5",
  278. "job_poll_timeout_seconds": "integer not null default 300",
  279. "created_at": "timestamp not null",
  280. "updated_at": "timestamp not null"
  281. }
  282. },
  283. {
  284. "name": "qc_templates",
  285. "columns": {
  286. "id": "bigint pk",
  287. "name": "text not null",
  288. "description": "text not null",
  289. "locale": "text not null",
  290. "thumbnail_url": "text",
  291. "template_preview_url": "text",
  292. "type": "text not null",
  293. "palette_ready": "boolean not null",
  294. "raw_template_json": "json/jsonb not null",
  295. "is_ai_template": "boolean not null",
  296. "is_onboarded": "boolean not null default false",
  297. "manifest_status": "text not null default 'missing'",
  298. "last_discovered_at": "timestamp",
  299. "created_at": "timestamp not null",
  300. "updated_at": "timestamp not null"
  301. }
  302. },
  303. {
  304. "name": "qc_template_manifests",
  305. "columns": {
  306. "id": "uuid pk",
  307. "template_id": "bigint not null references qc_templates(id)",
  308. "manifest_version": "integer not null",
  309. "source": "text not null",
  310. "language_used_for_discovery": "text not null",
  311. "discovery_payload_json": "json/jsonb not null",
  312. "discovery_response_json": "json/jsonb not null",
  313. "flattened_manifest_json": "json/jsonb not null",
  314. "is_active": "boolean not null default true",
  315. "created_at": "timestamp not null",
  316. "updated_at": "timestamp not null"
  317. }
  318. },
  319. {
  320. "name": "qc_template_fields",
  321. "columns": {
  322. "id": "uuid pk",
  323. "template_id": "bigint not null references qc_templates(id)",
  324. "manifest_id": "uuid not null references qc_template_manifests(id)",
  325. "section": "text not null",
  326. "key_name": "text not null",
  327. "path": "text not null",
  328. "field_kind": "text not null",
  329. "sample_value": "text",
  330. "is_enabled": "boolean not null default true",
  331. "is_required_by_us": "boolean not null default false",
  332. "display_label": "text",
  333. "display_order": "integer not null default 0",
  334. "notes": "text",
  335. "created_at": "timestamp not null",
  336. "updated_at": "timestamp not null"
  337. },
  338. "indexes": [
  339. "unique(template_id, manifest_id, path)"
  340. ]
  341. },
  342. {
  343. "name": "site_builds",
  344. "columns": {
  345. "id": "uuid pk",
  346. "template_id": "bigint not null references qc_templates(id)",
  347. "manifest_id": "uuid not null references qc_template_manifests(id)",
  348. "request_name": "text not null",
  349. "global_data_json": "json/jsonb not null",
  350. "ai_data_json": "json/jsonb not null",
  351. "final_sites_payload_json": "json/jsonb not null",
  352. "qc_job_id": "bigint",
  353. "qc_site_id": "bigint",
  354. "qc_status": "text not null",
  355. "qc_preview_url": "text",
  356. "qc_editor_url": "text",
  357. "qc_result_json": "json/jsonb",
  358. "qc_error_json": "json/jsonb",
  359. "started_at": "timestamp",
  360. "finished_at": "timestamp",
  361. "created_at": "timestamp not null",
  362. "updated_at": "timestamp not null"
  363. },
  364. "indexes": [
  365. "index(qc_status)",
  366. "index(qc_job_id)"
  367. ]
  368. },
  369. {
  370. "name": "api_logs",
  371. "columns": {
  372. "id": "uuid pk",
  373. "scope": "text not null",
  374. "method": "text not null",
  375. "url": "text not null",
  376. "request_headers_redacted_json": "json/jsonb not null",
  377. "request_body_json": "json/jsonb",
  378. "response_status": "integer",
  379. "response_body_json": "json/jsonb",
  380. "duration_ms": "integer",
  381. "created_at": "timestamp not null"
  382. }
  383. }
  384. ]
  385. },
  386. "quick_creator_api_contract": {
  387. "base_url": "https://qc-api.yggdrasil.dev-mono.net/api/v1",
  388. "auth": {
  389. "mode": "Bearer Token",
  390. "header": "Authorization: Bearer <TOKEN>",
  391. "mvp_policy": "Nur diesen Auth-Modus implementieren",
  392. "explicitly_not_used": [
  393. "/app/login"
  394. ]
  395. },
  396. "endpoints_used": [
  397. {
  398. "method": "GET",
  399. "path": "/health",
  400. "purpose": "Health/Reachability"
  401. },
  402. {
  403. "method": "GET",
  404. "path": "/templates?type=ai",
  405. "purpose": "AI-Templates laden"
  406. },
  407. {
  408. "method": "GET",
  409. "path": "/templates/{templateId}",
  410. "purpose": "Template-Details"
  411. },
  412. {
  413. "method": "GET",
  414. "path": "/templates/{templateId}/schema",
  415. "purpose": "optionale Zusatzdaten, nicht Quelle der Wahrheit"
  416. },
  417. {
  418. "method": "POST",
  419. "path": "/generate-content",
  420. "purpose": "nur Discovery / Key-Extraktion"
  421. },
  422. {
  423. "method": "POST",
  424. "path": "/sites",
  425. "purpose": "produktive Site-Erstellung"
  426. },
  427. {
  428. "method": "GET",
  429. "path": "/jobs/{jobId}",
  430. "purpose": "Polling / previewUrl"
  431. },
  432. {
  433. "method": "GET",
  434. "path": "/sites/{siteId}/editor-url",
  435. "purpose": "optional editor login url"
  436. }
  437. ]
  438. },
  439. "go_types_qcclient": {
  440. "notes": [
  441. "Diese Typen in internal/qcclient/types.go definieren",
  442. "Externe Response-Envelope immer als eigenes Typmodell abbilden"
  443. ],
  444. "types": {
  445. "APIResponse": "type APIResponse[T any] struct { Status string `json:\"status\"`; Data T `json:\"data\"` }",
  446. "APIError": "type APIError struct { Status string `json:\"status\"`; Message string `json:\"message\"`; Code string `json:\"code\"`; Details map[string]any `json:\"details,omitempty\"` }",
  447. "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\"` }",
  448. "TemplateHeading": "type TemplateHeading struct { TranslationKey string `json:\"translationKey\"` }",
  449. "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\"` }",
  450. "GenerateContentData": "type GenerateContentData map[string]map[string]any",
  451. "CreateSiteRequest": "type CreateSiteRequest struct { TemplateID int64 `json:\"templateId\"`; GlobalData map[string]any `json:\"globalData\"`; Content CreateSiteContent `json:\"content\"` }",
  452. "CreateSiteContent": "type CreateSiteContent struct { AIData map[string]map[string]any `json:\"aiData\"` }",
  453. "CreateSiteResponseData": "type CreateSiteResponseData struct { Status string `json:\"status\"`; JobID int64 `json:\"jobId\"` }",
  454. "JobStatusResult": "type JobStatusResult struct { SiteID int64 `json:\"siteId\"`; PreviewURL string `json:\"previewUrl\"` }",
  455. "JobStatusData": "type JobStatusData struct { JobID int64 `json:\"jobId\"`; Status string `json:\"status\"`; CreatedAt int64 `json:\"createdAt\"`; Result JobStatusResult `json:\"result\"` }",
  456. "SiteEditorLoginData": "type SiteEditorLoginData struct { LoginURL string `json:\"loginUrl\"` }"
  457. }
  458. },
  459. "qcclient_interface_design": {
  460. "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) }",
  461. "implementation_rules": [
  462. "Request body und Response body für Logs separat puffern",
  463. "Authorization-Header in Logs immer redaktieren",
  464. "400/401/404 nicht retrien",
  465. "429/5xx mit Exponential Backoff retrien",
  466. "Fehlerantworten in APIError zu mappen versuchen, sonst Raw Body behalten"
  467. ]
  468. },
  469. "template_onboarding_strategy": {
  470. "summary": "Pro AI-Template einmalige Discovery durchführen und Feldmanifest lokal speichern.",
  471. "steps": [
  472. "1. GET /templates?type=ai synchronisieren.",
  473. "2. Ein Template auswählen.",
  474. "3. Optional /templates/{templateId} und /templates/{templateId}/schema speichern.",
  475. "4. Discovery via POST /generate-content mit Dummy-Daten ausführen.",
  476. "5. Response data rekursiv flatten und section/key/path extrahieren.",
  477. "6. Felder mit sample_value '#IMAGE#' oder ähnlichem als image markieren und standardmässig deaktivieren.",
  478. "7. Textfelder als enabled markieren.",
  479. "8. Manifest und Fields in DB speichern.",
  480. "9. UI-Review zulassen: Labels, Reihenfolge, Aktivierung anpassen.",
  481. "10. Validierungs-Build mit Minimaltexten fahren.",
  482. "11. Bei Erfolg Manifeststatus = validated."
  483. ],
  484. "discovery_payload_default": {
  485. "templateId": 1378062,
  486. "globalData": {
  487. "companyName": "Discovery Company",
  488. "businessType": "dentist",
  489. "siteLanguage": "EN",
  490. "email": "discovery@example.com",
  491. "phone": "+41 44 000 00 00",
  492. "address": {
  493. "line1": "Discovery Street 1",
  494. "line2": "",
  495. "city": "Zurich",
  496. "region": "ZH",
  497. "postalCode": "8000",
  498. "country": "CH"
  499. }
  500. },
  501. "empty": false,
  502. "toneOfVoice": "Professional",
  503. "targetAudience": "B2B"
  504. },
  505. "flattening_algorithm": {
  506. "goal": "Aus GenerateContentData ein persistierbares Feldmanifest bauen",
  507. "pseudocode": [
  508. "für jede section im top-level object",
  509. " für jeden key/value in section object",
  510. " path = section + '.' + key",
  511. " sample = stringify(value)",
  512. " field_kind = detectFieldKind(sample)",
  513. " persist field"
  514. ],
  515. "field_kind_rules": [
  516. "sample == '#IMAGE#' => image",
  517. "string => text",
  518. "sonst => unknown"
  519. ]
  520. }
  521. },
  522. "mapping_service_design": {
  523. "summary": "Im MVP wird raw-key-mode verwendet: interne App arbeitet direkt mit template-spezifischen Roh-Keys.",
  524. "interface": "type Service interface { AssembleAIData(fields []TemplateField, values map[string]string) (map[string]map[string]any, error) }",
  525. "rules": [
  526. "Nur aktivierte Felder berücksichtigen",
  527. "Nur bekannte Paths akzeptieren",
  528. "Leere Strings standardmässig weglassen",
  529. "image/unknown-Felder im MVP nicht senden",
  530. "sections nur senden, wenn mindestens ein Feld vorhanden ist"
  531. ],
  532. "input_example": {
  533. "fieldValues": {
  534. "text.textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich",
  535. "text.textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung.",
  536. "services.servicesTitle_r4694_154": "Unsere Leistungen",
  537. "services.servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin."
  538. }
  539. },
  540. "output_example": {
  541. "text": {
  542. "textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich",
  543. "textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung."
  544. },
  545. "services": {
  546. "servicesTitle_r4694_154": "Unsere Leistungen",
  547. "servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin."
  548. }
  549. }
  550. },
  551. "site_build_strategy": {
  552. "summary": "Produktiv wird nur POST /sites mit eigener aiData verwendet.",
  553. "rules": [
  554. "Template muss AI sein",
  555. "validiertes oder mindestens reviewed Manifest muss existieren",
  556. "globalData.companyName und globalData.email sind Pflicht",
  557. "globalData.username explizit setzen",
  558. "accountId weglassen",
  559. "colorPalette weglassen",
  560. "content.files weglassen",
  561. "nur textliche aiData senden"
  562. ],
  563. "final_payload_example": {
  564. "templateId": 1378062,
  565. "globalData": {
  566. "companyName": "Muster AG",
  567. "businessType": "dentist",
  568. "username": "muster-ag-zuerich",
  569. "email": "info@muster.ch",
  570. "phone": "+41 44 123 45 67",
  571. "siteLanguage": "DE",
  572. "address": {
  573. "line1": "Bahnhofstrasse 1",
  574. "line2": "",
  575. "city": "Zuerich",
  576. "region": "ZH",
  577. "zip": "8001",
  578. "country": "CH"
  579. }
  580. },
  581. "content": {
  582. "aiData": {
  583. "text": {
  584. "textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich",
  585. "textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung im Herzen von Zürich."
  586. },
  587. "services": {
  588. "servicesTitle_r4694_154": "Unsere Leistungen",
  589. "servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin."
  590. },
  591. "testimonials": {
  592. "testimonialsName_c3006_153": "Sabine M.",
  593. "testimonialsDescription_c3006_152": "Freundlich, professionell und sehr angenehm."
  594. }
  595. }
  596. }
  597. }
  598. },
  599. "build_service_design": {
  600. "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 }",
  601. "start_build_flow": [
  602. "1. Template laden",
  603. "2. aktives Manifest laden",
  604. "3. Input validieren",
  605. "4. aiData via mapping service assemblieren",
  606. "5. finales /sites Payload erzeugen",
  607. "6. SiteBuild Datensatz im Status draft oder queued anlegen",
  608. "7. POST /sites ausführen",
  609. "8. jobId speichern und Status queued setzen",
  610. "9. Polling-Worker triggern",
  611. "10. BuildResult an UI zurückgeben"
  612. ],
  613. "StartBuildRequest_type": {
  614. "fields": {
  615. "TemplateID": "int64",
  616. "RequestName": "string",
  617. "GlobalData": "map[string]any",
  618. "FieldValues": "map[string]string"
  619. }
  620. },
  621. "BuildResult_type": {
  622. "fields": {
  623. "BuildID": "string",
  624. "QCJobID": "int64",
  625. "Status": "string"
  626. }
  627. }
  628. },
  629. "polling_service_design": {
  630. "summary": "DB-backed Polling mit goroutines, aber ohne separate Queue im MVP.",
  631. "worker_model": {
  632. "mode": "ticker-based supervisor + per-build poll execution",
  633. "default_interval_seconds": 5,
  634. "default_timeout_seconds": 300
  635. },
  636. "algorithm": [
  637. "1. Supervisor sucht regelmässig site_builds mit qc_status IN ('queued','processing').",
  638. "2. Für jeden Build PollOnce ausführen.",
  639. "3. GET /jobs/{jobId} aufrufen.",
  640. "4. Wenn result status done => previewUrl/siteId speichern, finished_at setzen, qc_status=done.",
  641. "5. Wenn externer Fehler => qc_error_json speichern und qc_status=failed.",
  642. "6. Wenn Timeout überschritten => qc_status=timeout."
  643. ],
  644. "concurrency_rules": [
  645. "Nicht denselben Build parallel pollen",
  646. "optimistic locking oder advisory flag verwenden",
  647. "max concurrent polls konfigurierbar"
  648. ]
  649. },
  650. "internal_http_api": {
  651. "json_endpoints": [
  652. {
  653. "method": "POST",
  654. "path": "/api/settings/test-qc-connection",
  655. "purpose": "Health/Templates-Test"
  656. },
  657. {
  658. "method": "POST",
  659. "path": "/api/templates/sync",
  660. "purpose": "AI-Templates synchronisieren"
  661. },
  662. {
  663. "method": "GET",
  664. "path": "/api/templates",
  665. "purpose": "lokale Templates"
  666. },
  667. {
  668. "method": "GET",
  669. "path": "/api/templates/{id}",
  670. "purpose": "Template inkl. Manifest und Fields"
  671. },
  672. {
  673. "method": "POST",
  674. "path": "/api/templates/{id}/onboard",
  675. "purpose": "Discovery ausführen"
  676. },
  677. {
  678. "method": "POST",
  679. "path": "/api/templates/{id}/validate",
  680. "purpose": "Minimaler Test-Build"
  681. },
  682. {
  683. "method": "PUT",
  684. "path": "/api/templates/{id}/fields",
  685. "purpose": "Field-Manifest anpassen"
  686. },
  687. {
  688. "method": "POST",
  689. "path": "/api/site-builds",
  690. "purpose": "Build starten"
  691. },
  692. {
  693. "method": "GET",
  694. "path": "/api/site-builds/{id}",
  695. "purpose": "Build-Details"
  696. },
  697. {
  698. "method": "POST",
  699. "path": "/api/site-builds/{id}/poll",
  700. "purpose": "manuelles Polling"
  701. },
  702. {
  703. "method": "POST",
  704. "path": "/api/site-builds/{id}/fetch-editor-url",
  705. "purpose": "optionale editorUrl laden"
  706. }
  707. ],
  708. "html_routes": [
  709. "/settings",
  710. "/templates",
  711. "/templates/{id}",
  712. "/builds/new",
  713. "/builds/{id}"
  714. ]
  715. },
  716. "ui_specification": {
  717. "mvp_style": "server rendered Go templates with optional HTMX partial updates",
  718. "screens": [
  719. {
  720. "name": "Settings",
  721. "features": [
  722. "QC Base URL",
  723. "Bearer Token speichern",
  724. "Connection Test",
  725. "Polling-Intervall konfigurieren",
  726. "Timeout konfigurieren",
  727. "Language Output Mode konfigurieren"
  728. ]
  729. },
  730. {
  731. "name": "Templates",
  732. "features": [
  733. "AI-Templates listen",
  734. "Onboarding-Status anzeigen",
  735. "Template Preview URL anzeigen",
  736. "Sync-Button"
  737. ]
  738. },
  739. {
  740. "name": "Template Detail",
  741. "features": [
  742. "Raw Template JSON",
  743. "Schema Raw JSON optional",
  744. "Discovery starten",
  745. "Felder anzeigen",
  746. "Felder aktiv/deaktivieren",
  747. "DisplayLabel und Reihenfolge pflegen",
  748. "Manifest-Status"
  749. ]
  750. },
  751. {
  752. "name": "New Build",
  753. "features": [
  754. "Template auswählen",
  755. "GlobalData Formular",
  756. "alle aktivierten Textfelder als Formular rendern",
  757. "finales aiData JSON vorab anzeigen",
  758. "finales /sites Payload anzeigen",
  759. "Build starten"
  760. ]
  761. },
  762. {
  763. "name": "Build Detail",
  764. "features": [
  765. "Build-Status",
  766. "QC jobId",
  767. "QC siteId",
  768. "previewUrl",
  769. "editorUrl optional",
  770. "Request-/Response-Snapshots",
  771. "Fehlerdetails"
  772. ]
  773. }
  774. ]
  775. },
  776. "validation_rules": {
  777. "before_onboarding": [
  778. "QC Token vorhanden",
  779. "Template existiert lokal oder kann geladen werden",
  780. "Template.Type == 'AI'"
  781. ],
  782. "before_build": [
  783. "Template ist AI",
  784. "aktives Manifest vorhanden",
  785. "Manifeststatus in reviewed oder validated",
  786. "companyName gesetzt",
  787. "email gesetzt",
  788. "username gesetzt",
  789. "mindestens ein aktiver Textwert vorhanden"
  790. ],
  791. "field_rules": [
  792. "Unbekannte fieldValues-Keys ablehnen",
  793. "deaktivierte Felder ignorieren oder ablehnen",
  794. "nur field_kind=text im MVP senden",
  795. "Leere Strings standardmässig nicht senden",
  796. "nil/null nicht an Quick Creator senden"
  797. ],
  798. "language_rules": [
  799. "intern Sprache als uppercase 2-letter speichern",
  800. "outgoing default uppercase",
  801. "bei BAD_REQUEST optional 1 lower-case Retry"
  802. ]
  803. },
  804. "logging_and_observability": {
  805. "logging": [
  806. "Structured JSON Logs",
  807. "jede externe QC-Request mit redaktierten Headers loggen",
  808. "jede externe QC-Response mit Status und Dauer loggen",
  809. "Build-Zustandswechsel loggen",
  810. "Discovery-Läufe vollständig loggen"
  811. ],
  812. "metrics": [
  813. "qc_requests_total",
  814. "qc_request_duration_ms",
  815. "template_sync_total",
  816. "template_onboard_total",
  817. "template_onboard_failed_total",
  818. "site_build_started_total",
  819. "site_build_done_total",
  820. "site_build_failed_total",
  821. "site_build_timeout_total"
  822. ],
  823. "redaction_rules": [
  824. "Authorization Header nie im Klartext loggen",
  825. "Tokens in UI nie anzeigen",
  826. "sensible Felder in Snapshots optional maskieren"
  827. ]
  828. },
  829. "security": {
  830. "requirements": [
  831. "Token verschlüsselt speichern, z. B. AES-GCM mit App Key aus ENV",
  832. "Admin-Zugriff absichern",
  833. "CSRF-Schutz falls server-rendered Form-POSTs",
  834. "Secure Cookies bei Session-basierter Admin-Auth",
  835. "Role-based access für Settings und Logs"
  836. ]
  837. },
  838. "implementation_details_go": {
  839. "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\"` }",
  840. "http_server_setup": [
  841. "chi.NewRouter()",
  842. "request id middleware",
  843. "recoverer middleware",
  844. "structured logger middleware",
  845. "timeout middleware"
  846. ],
  847. "store_interfaces": {
  848. "TemplateStore": "UpsertTemplates, GetTemplateByID, ListTemplates, SetTemplateManifestStatus",
  849. "ManifestStore": "CreateManifest, GetActiveManifestByTemplateID, ReplaceFields, ListFieldsByManifestID, UpdateFieldSettings",
  850. "BuildStore": "CreateBuild, UpdateBuildQueued, UpdateBuildDone, UpdateBuildFailed, GetBuildByID, ListActiveBuilds",
  851. "SettingsStore": "GetSettings, SaveSettings"
  852. },
  853. "error_style": {
  854. "rule": "wrap errors with context using fmt.Errorf(\"...: %w\", err)",
  855. "typed_errors": [
  856. "ErrUnauthorized",
  857. "ErrBadRequest",
  858. "ErrNotFound",
  859. "ErrInvalidTemplateType",
  860. "ErrManifestMissing",
  861. "ErrBuildTimeout"
  862. ]
  863. }
  864. },
  865. "testing_strategy": {
  866. "unit_tests": [
  867. "Flatten GenerateContentData zu Fields",
  868. "AssembleAIData mit enabled/disabled fields",
  869. "Validation von fieldValues",
  870. "Sprache-Normalisierung",
  871. "Error-Mapping von QC-Responses"
  872. ],
  873. "integration_tests": [
  874. "ListAITemplates gegen echte Dev-API",
  875. "Onboarding eines echten AI-Templates",
  876. "CreateSite mit Minimalpayload",
  877. "Polling bis done",
  878. "Fetch editorUrl optional"
  879. ],
  880. "manual_tests": [
  881. "Settings speichern und Verbindung testen",
  882. "Templates synchronisieren",
  883. "Template onboarden",
  884. "Labels im UI anpassen",
  885. "Build mit eigenem Text starten",
  886. "Preview öffnen und Texte visuell prüfen"
  887. ]
  888. },
  889. "acceptance_criteria": {
  890. "must_have": [
  891. "Die Go-App kompiliert zu einem deploybaren Binary.",
  892. "Die App kann mit Bearer-Token AI-Templates synchronisieren.",
  893. "Die App kann ein AI-Template onboarden und die aiData-Key-Struktur speichern.",
  894. "Die App kann eigene Texte auf gespeicherte Template-Keys mappen.",
  895. "Die App kann eine Site über POST /sites bauen.",
  896. "Die App kann den Jobstatus pollen und previewUrl anzeigen.",
  897. "Alle QC-Requests/Responses werden redaktiert protokolliert.",
  898. "Es wird kein DCM/EFL verwendet."
  899. ],
  900. "must_not": [
  901. "Kein produktiver Einsatz von /generate-content zur Texterzeugung",
  902. "Keine Standard-Templates",
  903. "Keine Bild-/Media-Integration",
  904. "Keine unbekannten Keys im finalen aiData"
  905. ]
  906. },
  907. "implementation_plan": [
  908. {
  909. "phase": 1,
  910. "name": "Bootstrap",
  911. "tasks": [
  912. "Go-Modul anlegen",
  913. "Config laden",
  914. "DB initialisieren",
  915. "Migrationsstruktur anlegen",
  916. "HTTP-Server Grundgerüst bauen"
  917. ]
  918. },
  919. {
  920. "phase": 2,
  921. "name": "QC Client",
  922. "tasks": [
  923. "HTTP Client Wrapper bauen",
  924. "Response Envelope-Typen bauen",
  925. "Health/ListTemplates/GetTemplate/GetSchema implementieren",
  926. "Logging/Retry/Error-Mapping implementieren"
  927. ]
  928. },
  929. {
  930. "phase": 3,
  931. "name": "Template Catalog & Onboarding",
  932. "tasks": [
  933. "Template Sync Service bauen",
  934. "Discovery via /generate-content bauen",
  935. "Flattening und Manifest-Persistenz bauen",
  936. "Template-Detail-UI bauen"
  937. ]
  938. },
  939. {
  940. "phase": 4,
  941. "name": "Build Flow",
  942. "tasks": [
  943. "Mapping Service bauen",
  944. "Build Form bauen",
  945. "POST /sites integrieren",
  946. "SiteBuild Persistenz bauen"
  947. ]
  948. },
  949. {
  950. "phase": 5,
  951. "name": "Polling & Detail View",
  952. "tasks": [
  953. "Polling Worker bauen",
  954. "Build Detail View bauen",
  955. "previewUrl/editorUrl Handling ergänzen"
  956. ]
  957. },
  958. {
  959. "phase": 6,
  960. "name": "Härtung",
  961. "tasks": [
  962. "Token-Verschlüsselung",
  963. "CSRF/Auth falls nötig",
  964. "Retry/Fallback Feinschliff",
  965. "Tests und Fehlerbilder"
  966. ]
  967. }
  968. ],
  969. "prohibitions": [
  970. "Nicht /app/login als produktiven Auth-Pfad implementieren.",
  971. "Nicht Standard-Templates zulassen.",
  972. "Nicht DCM/EFL mit reinmischen.",
  973. "Nicht unbekannte oder deaktivierte Keys an Quick Creator senden.",
  974. "Nicht image-Felder im MVP befüllen.",
  975. "Nicht Geschäftslogik in Handlern ablegen.",
  976. "Nicht direkt aus UI gegen Quick Creator callen; immer über Service-Layer."
  977. ],
  978. "safe_defaults": {
  979. "auth": "Bearer Token only",
  980. "language": "uppercase on first try",
  981. "manifest_source_of_truth": "/generate-content response",
  982. "empty_values": "skip",
  983. "images": "disabled",
  984. "poll_interval_seconds": 5,
  985. "poll_timeout_seconds": 300
  986. },
  987. "example_build_request_to_our_app": {
  988. "templateId": 1378062,
  989. "requestName": "Muster AG Zürich",
  990. "globalData": {
  991. "companyName": "Muster AG",
  992. "businessType": "dentist",
  993. "username": "muster-ag-zuerich",
  994. "email": "info@muster.ch",
  995. "phone": "+41 44 123 45 67",
  996. "siteLanguage": "DE",
  997. "address": {
  998. "line1": "Bahnhofstrasse 1",
  999. "line2": "",
  1000. "city": "Zuerich",
  1001. "region": "ZH",
  1002. "zip": "8001",
  1003. "country": "CH"
  1004. }
  1005. },
  1006. "fieldValues": {
  1007. "text.textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich",
  1008. "text.textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung im Herzen von Zürich.",
  1009. "services.servicesTitle_r4694_154": "Unsere Leistungen",
  1010. "services.servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin.",
  1011. "testimonials.testimonialsName_c3006_153": "Sabine M.",
  1012. "testimonials.testimonialsDescription_c3006_152": "Freundlich, professionell und sehr angenehm."
  1013. }
  1014. },
  1015. "example_final_qc_payload": {
  1016. "templateId": 1378062,
  1017. "globalData": {
  1018. "companyName": "Muster AG",
  1019. "businessType": "dentist",
  1020. "username": "muster-ag-zuerich",
  1021. "email": "info@muster.ch",
  1022. "phone": "+41 44 123 45 67",
  1023. "siteLanguage": "DE",
  1024. "address": {
  1025. "line1": "Bahnhofstrasse 1",
  1026. "line2": "",
  1027. "city": "Zuerich",
  1028. "region": "ZH",
  1029. "zip": "8001",
  1030. "country": "CH"
  1031. }
  1032. },
  1033. "content": {
  1034. "aiData": {
  1035. "text": {
  1036. "textTitle_m1820_149": "Zahnarztpraxis Muster in Zürich",
  1037. "textDescription_c3825_151": "Moderne Zahnmedizin mit persönlicher Betreuung im Herzen von Zürich."
  1038. },
  1039. "services": {
  1040. "servicesTitle_r4694_154": "Unsere Leistungen",
  1041. "servicesDescription_r4694_155": "Kontrollen, Dentalhygiene und ästhetische Zahnmedizin."
  1042. },
  1043. "testimonials": {
  1044. "testimonialsName_c3006_153": "Sabine M.",
  1045. "testimonialsDescription_c3006_152": "Freundlich, professionell und sehr angenehm."
  1046. }
  1047. }
  1048. }
  1049. },
  1050. "developer_summary": {
  1051. "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.",
  1052. "first_milestone": "Template Sync + Onboarding eines AI-Templates + gespeichertes Field-Manifest",
  1053. "second_milestone": "Site-Build mit eigenem Text + Job-Polling + previewUrl"
  1054. }
  1055. }