From a7e53ca0a3282404c61d775ee1c08310a6c23a75 Mon Sep 17 00:00:00 2001 From: GreenEclipse Date: Mon, 17 Aug 2026 21:56:09 +0200 Subject: [PATCH] fix --- apps/api/internal/httpapi/v1_openapi.go | 106 ++++++++++++++---- apps/api/internal/httpapi/v1_openapi_test.go | 38 +++++++ apps/api/internal/processing/sanitize.go | 22 +++- apps/api/internal/processing/sanitize_test.go | 18 +-- apps/web/scripts/phrase-map.json | 32 +++--- apps/web/src/lib/docs-guide/tree.ts | 2 +- 6 files changed, 162 insertions(+), 56 deletions(-) diff --git a/apps/api/internal/httpapi/v1_openapi.go b/apps/api/internal/httpapi/v1_openapi.go index 8d7d5da..9aa5f4e 100644 --- a/apps/api/internal/httpapi/v1_openapi.go +++ b/apps/api/internal/httpapi/v1_openapi.go @@ -123,6 +123,27 @@ info: processed_product_id / raw_product_id). Use GET /products when a UUID is needed. Display name is items[].name (title omitted when identical). + ### AI descriptions (HTML body, not a short blurb) + + Default processing_type full runs normalize, parse specs, fill fields, EPREL + (when enabled), categorize, then AI enhance. Enhance writes the display name + and a full product body. + + Completed items[].description is that body: one HTML string (tags matching + section types h1/h2/h3/h4, p, ul) — never a JSON array and never a one- or + two-sentence slogan. Length and structure come from the assigned category + description_template (description formula): ordered sections with their own + instructions. A formula can ask for a short paragraph, 100-word blocks, + several heading/list sections, or a very large multi-section article. There + is no request-body size enum; configure length on the category. When a + category has no formula, the default is still 1-3 factual paragraphs with + limited HTML, not a headline-only stub. + + items[].description on the start body is optional source text the model may + rewrite. Poll GET /products/process/{id} for the generated HTML. + meta_title / meta_description are separate short SEO fields and may be + omitted depending on company configuration. + Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate from this public API-key surface. Other path aliases (/categories/create, /attributes/create, /campaigns) appear next to canonical paths. @@ -674,7 +695,10 @@ paths: description: Enriched display title processed_description: type: string - description: Enriched long description text + description: | + Enriched product body as one HTML string from the category + description formula. Length follows that formula (including very + large multi-section copy), not a short summary. status: type: string enum: @@ -788,14 +812,23 @@ paths: - Processing summary: Start processing by EAN description: | - Public process API (handleV1StartProcess). Upserts raw products from - items[].ean (GTIN), enqueues one processing job, returns HTTP 200 with a data envelope. + Public process API. Upserts raw products from items[].ean (GTIN), enqueues + one processing job, returns HTTP 200 with a data envelope. + + Default processing_type full runs the full pipeline including AI enhance: + the model writes a retail title (name) and a full HTML product description + driven by the category description formula. That body can be long — + multiple headings, paragraphs, and lists, including very large + multi-section articles — not a one- or two-sentence summary. Primary body: items[].ean. Alternate body on the same handler: raw_product_ids (raw_products.id UUID list — not GET /products data[].id). Prefer items for public integrations. On items[].ean, assertV1ProcessGates runs before EnsureRaw so plan/credit failures cannot spam catalog writes (HTTP 402 coded envelope). + Poll GET /products/process/{id} until status is COMPLETED, then read + items[].description for the generated HTML. + Not an alias of POST /process (flat ProcessingJob / 202). Do not mix envelopes. requestBody: $ref: "#/components/requestBodies/StartProcessByEAN" @@ -903,9 +936,13 @@ paths: jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob). On COMPLETED items, name is the product display name (title is omitted when - identical). Internal UUIDs (id / processed_product_id / raw_product_id) are - omitted — use GET /products for those. Optional meta_title and - meta_description may be omitted depending on company configuration. + identical). description is the generated product body: one HTML string + following the category description formula (h1/h2/h3/h4, p, ul). Expect + real catalog copy — often several paragraphs and lists, sometimes a very + large article — not a one- or two-sentence blurb. Internal UUIDs (id / + processed_product_id / raw_product_id) are omitted — use GET /products for + those. Optional meta_title and meta_description are short SEO fields and + may be omitted depending on company configuration. parameters: - name: id in: path @@ -938,12 +975,13 @@ paths: category_id: '50' category_name: Cookers name: VOX electric cooker EHT 6020 WG - description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven." + description: "

VOX electric cooker EHT 6020 WG

The VOX EHT 6020 WG is a free-standing electric cooker for everyday household cooking. Four ceramic hobs and a 65 L fan oven let you boil, fry, and bake at the same time, while energy class A helps keep running costs down. The 60 cm width fits a standard kitchen niche. An enamel oven cavity and removable inner door glass are designed for cleaning after heavy use.

Run sauces or pasta on the hob while the oven handles a roast or tray bake. Heat is even across the ceramic zones, and residual-heat indicators reduce the chance of accidental contact after cooking. Fan mode suits multi-level baking; conventional heat remains available for dishes that prefer still air. A full-width grill finishes cheese, gratins, and toast.

Key features

Install in a standard 60 cm cooker slot with the dedicated electric supply shown on the rating plate. Leave rear clearance for ventilation and use a heat-resistant worktop next to the hob. This product is for household kitchens, not commercial catering loads.

Use cookware that matches each zone so energy is not wasted around the pan. Wipe the ceramic surface regularly and empty crumbs from the oven floor to preserve appearance and efficiency. For spare parts, quote model EHT 6020 WG and the EAN on the rating plate.

" attributes: brand: Vox product_model: EHT6020WG main_image: https://images.example.com/products/vox-eht6020wg.jpg - more_images: null + more_images: + - https://images.example.com/products/vox-eht6020wg-side.jpg eprel: id: "1234567" label: https://eprel.ec.europa.eu/label/Example @@ -4389,7 +4427,9 @@ paths: summary: Processing job status (raw_product_ids surface) description: | Public processing job JSON: status, counts, timestamps, processing_type. - Omits current_step, step_progress, and company_id. Completed jobs still include items[]. + Omits current_step, step_progress, and company_id. Completed jobs still include + items[] with the same AI HTML description contract as GET /products/process/{id} + (category formula body, not a one- or two-sentence blurb). parameters: - $ref: "#/components/parameters/ID" responses: @@ -4421,6 +4461,7 @@ paths: status: processed name: Acme Wireless Earbuds ANC Black category: Headphones + description: "

Acme Wireless Earbuds ANC Black

Acme Wireless Earbuds ANC Black are in-ear headphones for commuting and desk work. Active noise cancellation, a secure fit, and a charging case cover a full workday of listening. The compact buds sit flush enough for glasses and hats, and the case fits a jacket pocket.

Pair over Bluetooth and switch between a phone and a laptop with the onboard controls. Transparency mode lets nearby voices through without removing the buds. The default ear-tip set covers common sizes; swap tips if bass feels weak or the seal slips during a walk.

In the box

Charge the case before first use. Keep the charging contacts dry and store the buds in the case so they sleep and recharge between sessions. For replacement tips or a spare case, quote the product name and the EAN on the box.

" total_items: 1 '400': description: Path id is not a UUID. @@ -5133,7 +5174,10 @@ components: description: Optional product title seed for enrichment description: type: string - description: Optional long description seed for enrichment + description: | + Optional source/seed text. AI enhance rewrites this into the + category description-formula HTML body. It is not the final + poll payload and does not cap length to a short summary. specifications: type: array description: Optional key/value specification pairs for attribute hints @@ -5193,10 +5237,13 @@ components: Not PresentProduct.id. Use GET /products data[].raw_product_id. processing_type: description: | - full (default); named steps category|title|description|attributes (string or + full (default) runs the whole pipeline including AI enhance, which + writes formula-driven HTML descriptions (not a one- or two-sentence + blurb). Named steps category|title|description|attributes (string or array of those steps); dual-mode also accepts v2 dashboard types such as normalize_only, enhance_only, attributes_only, eprel_only, categorize_only (ParseV1ProcessingType). Alias processingType accepted when values match. + The description step still emits the category formula HTML body. oneOf: - type: string enum: @@ -5550,8 +5597,13 @@ components: type: string description: type: string + description: | + Product body. After processing this is the AI HTML description + (category formula), not a one- or two-sentence summary. processed_description: type: string + description: | + Enriched HTML body from AI enhance. Same contract as ProcessItem.description. status: type: string example: completed @@ -5664,8 +5716,16 @@ components: nullable: true title_template: nullable: true + description: | + Title formula for AI enhance. Controls how processed name is built + (type, brand, model, and other slots). description_template: nullable: true + description: | + Description formula for AI enhance. Ordered sections (h1/h2/h3/h4, p, ul) + with per-section instructions. Controls HTML structure and length of + processed items[].description — short, long, or very large copy. + Not a one- or two-sentence cap. created_at: type: string format: date-time @@ -6297,10 +6357,11 @@ components: description: | One COMPLETED process line (public contract). Successful items expose category as the human-readable display name (category_id holds - categories.unique_id), a description string that may include section - HTML (h1/h2/h3/h4, p, ul — never a JSON array), optional SEO meta_title / - meta_description (plain text; some companies omit these fields), - optional eprel object or null, clean attributes, and images. + categories.unique_id), a description string that is generated HTML + product copy (h1/h2/h3/h4, p, ul — never a JSON array and never a one- + or two-sentence slogan), optional SEO meta_title / meta_description + (plain text; some companies omit these fields), optional eprel object + or null, clean attributes, and images. Product display name is name (primary). title is omitted when identical to name. Internal UUIDs (id, processed_product_id, raw_product_id) are omitted from this public shape — use catalog APIs when a product UUID is required. @@ -6317,8 +6378,6 @@ components: description: | Human-readable category display name (same value as category_name when resolved). Prefer this for UI. Machine unique_id is category_id. - ASSUMPTION: historically this field held categories.unique_id; clients - that need the opaque id must read category_id (additive). category_id: type: string nullable: true @@ -6356,9 +6415,14 @@ components: type: string nullable: true description: | - Product body description as a string — never a one-element JSON array. - May include section HTML tags (h1/h2/h3/h4, p, ul). Plain-text - descriptions remain plain. Optional meta_description stays plain SEO text. + Generated product body as one HTML string — never a JSON array and + never a one- or two-sentence slogan. AI enhance follows the assigned + category description_template (description formula): ordered sections + of type h1, h2, h3, h4, p, and ul, each with its own instructions. + Output can be a short paragraph, several 100-word blocks, or a very + large multi-section article. When no formula is set, expect 1-3 + factual paragraphs with limited HTML. Optional meta_description stays + plain SEO text and is not a substitute for this field. attributes: type: object additionalProperties: true @@ -6477,7 +6541,7 @@ components: category_id: '50' category_name: Cookers name: VOX electric cooker EHT 6020 WG - description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven. Energy class A with practical everyday capacity." + description: "

VOX electric cooker EHT 6020 WG

The VOX EHT 6020 WG is a free-standing electric cooker for everyday household cooking. Four ceramic hobs and a 65 L fan oven let you boil, fry, and bake at the same time, while energy class A helps keep running costs down. The 60 cm width fits a standard kitchen niche. An enamel oven cavity and removable inner door glass are designed for cleaning after heavy use.

Run sauces or pasta on the hob while the oven handles a roast or tray bake. Heat is even across the ceramic zones, and residual-heat indicators reduce the chance of accidental contact after cooking. Fan mode suits multi-level baking; conventional heat remains available for dishes that prefer still air. A full-width grill finishes cheese, gratins, and toast.

Key features

Install in a standard 60 cm cooker slot with the dedicated electric supply shown on the rating plate. Leave rear clearance for ventilation and use a heat-resistant worktop next to the hob. This product is for household kitchens, not commercial catering loads.

Use cookware that matches each zone so energy is not wasted around the pan. Wipe the ceramic surface regularly and empty crumbs from the oven floor to preserve appearance and efficiency. For spare parts, quote model EHT 6020 WG and the EAN on the rating plate.

" attributes: brand: Vox product_model: EHT6020WG diff --git a/apps/api/internal/httpapi/v1_openapi_test.go b/apps/api/internal/httpapi/v1_openapi_test.go index 7aae363..6c10e25 100644 --- a/apps/api/internal/httpapi/v1_openapi_test.go +++ b/apps/api/internal/httpapi/v1_openapi_test.go @@ -467,6 +467,44 @@ func TestV1OpenAPIYAMLOmitsInternalPromptWording(t *testing.T) { } } +func TestV1OpenAPIYAMLDocumentsAIDescriptionHTML(t *testing.T) { + t.Parallel() + + doc := string(v1OpenAPIYAML) + for _, needle := range []string{ + "AI descriptions (HTML body, not a short blurb)", + "description_template (description formula)", + "one- or two-sentence", + "

VOX electric cooker EHT 6020 WG

", + "

Key features

", + } { + if !strings.Contains(doc, needle) { + t.Fatalf("openapi missing AI description contract text %q", needle) + } + } + + root := mustParseOpenAPIRoot(t, v1OpenAPIYAML) + comps, _ := root["components"].(map[string]any) + examples, _ := comps["examples"].(map[string]any) + completed, _ := examples["ProcessCompletedExample"].(map[string]any) + val, _ := completed["value"].(map[string]any) + data, _ := val["data"].(map[string]any) + items, _ := data["items"].([]any) + if len(items) == 0 { + t.Fatal("ProcessCompletedExample: expected items") + } + item, _ := items[0].(map[string]any) + desc, _ := item["description"].(string) + for _, tag := range []string{"

", "

", "

", "