From e2e7f8d5668822ac68fd240a439eb0ac63e479a1 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 8 Oct 2026 12:44:53 +0200 Subject: [PATCH] u --- docs/api/routes-all.md | 4 +-- docs/api/routes.md | 4 +-- docs/api/routes/article/create.md | 2 ++ docs/api/routes/article/update.md | 2 ++ src/api/routes/article.js | 42 +++++++++++++++++++++----- src/db/article.js | 4 +-- src/db/store.js | 15 +++++++--- src/services/companyImport.js | 16 ++++++++-- src/services/conversationManager.js | 37 +++++++++++++++++++++++ src/tools/set-user-route.js | 46 +++++++++++++++++++++++++++++ todo.md | 21 ++++++++++++- 11 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 src/tools/set-user-route.js diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 4ac3556..46cf254 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -58,7 +58,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). ### Routes #### Create article in category — `POST /article` -- Params: `body: { categoryId: int, name: str, track_stock?: bool }` +- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }` - Returns: `201 T31` #### Export articles as ZIP — `GET /article/export.zip` - Params: `query: categoryId?: int` @@ -70,7 +70,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Params: `sku: str` - Returns: `200 T1` #### Update article by SKU — `PATCH /article/{sku}` -- Params: `sku: str, body: { name?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` +- Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` - Returns: `200 T1` #### Get Stückliste (bundle items) for an article — `GET /article/{sku}/articlelist` - Params: `sku: str` diff --git a/docs/api/routes.md b/docs/api/routes.md index 6fbe7f0..c451074 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -53,7 +53,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). ### Routes #### Create article in category — `POST /article` -- Params: `body: { categoryId: int, name: str, track_stock?: bool }` +- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }` - Returns: `201 T29` #### Delete article by SKU — `DELETE /article/{sku}` - Params: `sku: str, query: cascade?: bool` @@ -62,7 +62,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Params: `sku: str` - Returns: `200 T1` #### Update article by SKU — `PATCH /article/{sku}` -- Params: `sku: str, body: { name?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` +- Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` - Returns: `200 T1` #### Get Stückliste (bundle items) for an article — `GET /article/{sku}/articlelist` - Params: `sku: str` diff --git a/docs/api/routes/article/create.md b/docs/api/routes/article/create.md index 79e5b7c..f2d2fc5 100644 --- a/docs/api/routes/article/create.md +++ b/docs/api/routes/article/create.md @@ -12,6 +12,8 @@ Creates an article under an authorized category (`parentId`). |-------|----------|------|-------------| | `categoryId` | yes | integer | Category ID under which to create the article | | `name` | yes | string | | +| `desc` | no | string | Article description text/HTML | +| `seo` | no | string | SEO text | | `track_stock` | no | boolean | Whether to track inventory stock (defaults to true) | ```json diff --git a/docs/api/routes/article/update.md b/docs/api/routes/article/update.md index 7f309c4..3ace10f 100644 --- a/docs/api/routes/article/update.md +++ b/docs/api/routes/article/update.md @@ -19,6 +19,8 @@ Patches article fields. At least one of `name`, `unitamount`, `unitSi` required. | Field | Required | Type | Description | |-------|----------|------|-------------| | `name` | no | string | | +| `desc` | no | string | Article description text/HTML | +| `seo` | no | string | SEO text | | `unitamount` | no | number | Must be finite and `> 0` | | `unitSi` | no | string | Existing company unit `si` | | `track_stock` | no | boolean | Whether to track inventory stock | diff --git a/src/api/routes/article.js b/src/api/routes/article.js index da72d78..9f546e9 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -167,6 +167,12 @@ export function register(app, basePath = '') { * description: Category ID under which to create the article * name: * type: string + * desc: + * type: string + * description: Article description text/HTML + * seo: + * type: string + * description: SEO text * track_stock: * type: boolean * description: Whether to track inventory stock (defaults to true) @@ -215,7 +221,7 @@ export function register(app, basePath = '') { * $ref: '#/components/schemas/Error' */ app.post(`${basePath}/article`, async (req, res) => { - const rawParent = req.body?.categoryId ?? req.body?.parentId; + const rawParent = req.body?.categoryId; if (rawParent === undefined || rawParent === null) { return res.status(400).json({ error: 'categoryId is required' }); } @@ -229,7 +235,7 @@ export function register(app, basePath = '') { return res.status(400).json({ error: 'name is required' }); } - const rawTrack = req.body?.track_stock ?? req.body?.trackStock; + const rawTrack = req.body?.track_stock; if (rawTrack !== undefined && typeof rawTrack !== 'boolean' && rawTrack !== 0 && rawTrack !== 1) { return res.status(400).json({ error: 'Invalid track_stock' }); } @@ -237,12 +243,15 @@ export function register(app, basePath = '') { const customSku = typeof req.body?.sku === 'string' && req.body.sku.trim() ? req.body.sku.trim() : undefined; try { + const desc = req.body?.desc; const created = await db.createUnderParent({ googleAuthId: req.user.id, parentId, name, track_stock, sku: customSku, + desc: desc !== undefined ? desc : undefined, + seo: req.body?.seo !== undefined ? req.body.seo : undefined, }); emitArticlesSignal(req); const article = { @@ -438,6 +447,12 @@ export function register(app, basePath = '') { * properties: * name: * type: string + * desc: + * type: string + * description: Article description text/HTML + * seo: + * type: string + * description: SEO text * unitamount: * type: number * description: Must be finite and `> 0` @@ -672,12 +687,23 @@ export function register(app, basePath = '') { } if (Object.prototype.hasOwnProperty.call(body, 'track_stock')) { patch.track_stock = body.track_stock; - } else if (Object.prototype.hasOwnProperty.call(body, 'trackStock')) { - patch.track_stock = body.trackStock; + } + if (Object.prototype.hasOwnProperty.call(body, 'desc')) { + patch.desc = body.desc; + } + if (Object.prototype.hasOwnProperty.call(body, 'seo')) { + patch.seo = body.seo; } - if (!('name' in patch) && !('unitamount' in patch) && !('unitSi' in patch) && !('track_stock' in patch)) { - return res.status(400).json({ error: 'name, unitamount, unitSi, or track_stock is required' }); + if ( + !('name' in patch) && + !('unitamount' in patch) && + !('unitSi' in patch) && + !('track_stock' in patch) && + !('desc' in patch) && + !('seo' in patch) + ) { + return res.status(400).json({ error: 'name, unitamount, unitSi, track_stock, desc, or seo is required' }); } try { @@ -1443,7 +1469,7 @@ export function register(app, basePath = '') { return res.status(400).json({ error: skuParsed.error }); } - const raw = req.body?.track_stock ?? req.body?.trackStock; + const raw = req.body?.track_stock; if (raw === undefined || (typeof raw !== 'boolean' && raw !== 0 && raw !== 1)) { return res.status(400).json({ error: 'Invalid track_stock' }); } @@ -1783,7 +1809,7 @@ export function register(app, basePath = '') { if (typeof isParent !== 'boolean') { return res.status(400).json({ error: 'isParent boolean is required' }); } - const attributeNameId = req.body?.attributeNameId ?? req.body?.parentAttributeNameId; + const attributeNameId = req.body?.attributeNameId; try { const result = await db.toggleParent(req.user.id, parsed.sku, { diff --git a/src/db/article.js b/src/db/article.js index d63c755..6acb0f2 100644 --- a/src/db/article.js +++ b/src/db/article.js @@ -1,8 +1,8 @@ import { getStore } from './store.js'; -export async function createUnderParent({ googleAuthId, parentId, name, track_stock, sku }) { +export async function createUnderParent(args) { const store = await getStore(); - return store.article.createUnderParent({ googleAuthId, parentId, name, track_stock, sku }); + return store.article.createUnderParent(args); } export async function getBySku(googleAuthId, sku) { diff --git a/src/db/store.js b/src/db/store.js index 56e81b2..e9da6b8 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -70,10 +70,17 @@ function wrapStoreWithTiming(rawStore) { storeLog.debug(`${namespace}.${method} ${chalk.gray(`(${Date.now() - start}ms)`)}`); return result; } catch (err) { - storeLog.error( - `${namespace}.${method} ${chalk.red('FAILED')} ` + - `${chalk.gray(`(${Date.now() - start}ms)`)} ${chalk.red(err.message)}` - ); + const isNotFound = /not found/i.test(err?.message); + if (isNotFound) { + storeLog.debug( + `${namespace}.${method} ${chalk.gray(`(${Date.now() - start}ms)`)} ${chalk.gray(err.message)}` + ); + } else { + storeLog.error( + `${namespace}.${method} ${chalk.red('FAILED')} ` + + `${chalk.gray(`(${Date.now() - start}ms)`)} ${chalk.red(err.message)}` + ); + } throw err; } }; diff --git a/src/services/companyImport.js b/src/services/companyImport.js index 96208dc..cd9e4f9 100644 --- a/src/services/companyImport.js +++ b/src/services/companyImport.js @@ -343,9 +343,17 @@ export async function executeCompanyImport({ jtlArtIdToSku.set(jtlArtId, sku); let name = ''; + let desc = ''; + let seo = ''; + try { - const desc = JSON.parse(a.descriptions || '[]'); - name = desc[0]?.cName || ''; + const descriptions = JSON.parse(a.descriptions || '[]'); + const descItem = Array.isArray(descriptions) ? descriptions[0] : descriptions; + if (descItem) { + name = String(descItem.cName || '').trim(); + desc = String(descItem.cBeschreibung || '').trim(); + seo = String(descItem.cKurzBeschreibung || '').trim(); + } } catch {} if (!name) name = `Artikel ${sku}`; @@ -388,6 +396,8 @@ export async function executeCompanyImport({ await store.article.updateBySku(googleAuthId, sku, { name: name.trim(), track_stock: trackStock, + desc: desc || undefined, + seo: seo || undefined, }); // Update prices @@ -432,6 +442,8 @@ export async function executeCompanyImport({ name: name.trim(), sku, track_stock: trackStock, + desc: desc || undefined, + seo: seo || undefined, }); // Set price diff --git a/src/services/conversationManager.js b/src/services/conversationManager.js index 5a66c2e..d83598a 100644 --- a/src/services/conversationManager.js +++ b/src/services/conversationManager.js @@ -28,6 +28,7 @@ import { createPdfReadTool } from '../tools/pdf-read.js'; import { createSearchArticlesTool } from '../tools/search-articles.js'; import { createEanChecksumTool } from '../tools/ean-checksum.js'; import { createNetGrossCalcTool } from '../tools/net-gross-calc.js'; +import { createSetUserRouteTool } from '../tools/set-user-route.js'; import { resolveOpenRouterKey, resolveDefaultModel, resolveModelReasoning, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js'; const KEEP_ALIVE_MS = 20_000; @@ -132,6 +133,7 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) { '16. edit_image: Generate or edit images using the configured company image model (modelslugimage). Supports attachments (low/high quality), size tiers (1K/2K/4K), and aspect ratios (1:1/4:3/3:4). Writes output to VFS (always AVIF format).', '17. ean_checksum: Validate EAN/GTIN/UPC barcode check digits or calculate missing check digit for barcodes (EAN-13, EAN-8, UPC-A, GTIN-14).', '18. net_gross_calc: Convert between net and gross prices with tax/VAT rate (default 19%). Provides 2-decimal display values and high-precision net prices for API article creation.', + '19. set-user-route: Navigate the user to a frontend route (e.g. when the user says "show me the article", "open quote 1001", or asks to view a page/entity).', '', 'VIRTUAL FILESYSTEM:', '- Persistent per-company VFS. Paths are root-relative forward-slash (e.g. "labels/sheet1.png").', @@ -157,7 +159,37 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) { '- GROSS vs NET PRICES: User prices are almost always GROSS (incl. VAT); API requires NET. Use net_gross_calc to compute high-precision (4–6 decimals) NET prices for API requests: `net = gross / (1 + taxRate / 100)`.', '- DISPLAYING GENERATED IMAGES: Inline markdown images supported. Embed written VFS image paths: `![descriptive alt](artifacts/example.png)`. Other files (PDF, JSON, CSV) do not render inline; provide download links.', '- LABEL TEMPLATES (/labeltemplate): Must use Dashback JSON schema `{ "objects": [ ... ] }`. Do NOT use ZPL.', + '- USER NAVIGATION: When the user says "show me the article", "open this order", or asks to navigate to a screen/entity, call set-user-route with the appropriate path from the Route Construction Matrix.', '- Always Format output with markdown and emoji.', + '', + 'USER PERSPECTIVE ROUTE CONSTRUCTION MATRIX:', + '| Destination | Path Pattern | Parameters / Construction | Example |', + '|:---|:---|:---|:---|', + '| **Article Search / Index** | `/artikel` | Query params: `?target={query}&filter={filter}&offset={n}` | `/artikel?target=kabel` |', + '| **Article Detail** | `/artikel/:sku` | Encode SKU: `/artikel/${encodeURIComponent(sku)}` | `/artikel/ART-10042` |', + '| **Warehouse / Stock** | `/artikel/lager` | Static | `/artikel/lager` |', + '| **Brands** | `/artikel/marken` | Static | `/artikel/marken` |', + '| **Label Templates** | `/artikel/vorlagen` | Static | `/artikel/vorlagen` |', + '| **Attributes & Features** | `/artikel/merkmale` | Static | `/artikel/merkmale` |', + '| **Category Tree Root** | `/kategorien` | Static | `/kategorien` |', + '| **Category Subtree** | `/kategorien/*` | Join encoded slugs: `/kategorien/${slug1}/${slug2}` | `/kategorien/elektronik/kabel` |', + '| **Delivery List** | `/lieferung` | Static | `/lieferung` |', + '| **New Delivery Draft** | `/lieferung/neu` | Static (receipt & draft line editor) | `/lieferung/neu` |', + '| **Delivery Detail** | `/lieferung/:deliveryId` | Delivery document ID: `/lieferung/${deliveryId}` | `/lieferung/42` |', + '| **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` |', + '| **Quote List (Angebote)** | `/auftrag` | Static | `/auftrag` |', + '| **New Quote** | `/auftrag/neu` | Static draft form | `/auftrag/neu` |', + '| **Quote Detail** | `/auftrag/:quoteId` | Quote document ID: `/auftrag/${quoteId}` | `/auftrag/1001` |', + '| **Order List (Bestellungen)**| `/auftrag/bestellung` | Static | `/auftrag/bestellung` |', + '| **New Order** | `/auftrag/bestellung/neu` | Static draft form | `/auftrag/bestellung/neu` |', + '| **Order Detail** | `/auftrag/bestellung/:orderId` | Order document ID: `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` |', + '| **Customer List** | `/kunde` | Static | `/kunde` |', + '| **New Customer** | `/kunde/neu` | Static form | `/kunde/neu` |', + '| **Customer Detail** | `/kunde/:entityId` | Customer entity ID: `/kunde/${entityId}` | `/kunde/312` |', + '| **Customer Groups** | `/kunde/gruppen` | Static (pricing & customer tiers) | `/kunde/gruppen` |', + '| **Article History** | `/infos` or `/infos/articlehistory` | Static | `/infos/articlehistory` |', + '| **Global History** | `/infos/globalhistory` | Static audit log | `/infos/globalhistory` |', + '| **Statistics** | `/infos/statistics` | Static KPIs & charts | `/infos/statistics` |', ...(routesDoc ? [ '', 'DASHBACK API ROUTES:', @@ -874,6 +906,11 @@ export class ConversationManager { createSearchArticlesTool({ googleAuthId, companyId: this.companyId }), createEanChecksumTool(), createNetGrossCalcTool(), + createSetUserRouteTool({ + onRoute: (route) => { + this.broadcast('route', { route }); + }, + }), ], }); diff --git a/src/tools/set-user-route.js b/src/tools/set-user-route.js new file mode 100644 index 0000000..82f9ba9 --- /dev/null +++ b/src/tools/set-user-route.js @@ -0,0 +1,46 @@ +import { defineTool } from 'libllmorbiter'; + +/** + * Creates the set-user-route tool. + * Sends the user to a specific frontend route (e.g. when the user says "show me the article", + * "open quote 1001", "go to orders", etc.). + * + * @param {{ onRoute?: (route: string) => void }} [options] + */ +export function createSetUserRouteTool({ onRoute } = {}) { + return defineTool({ + name: 'set-user-route', + description: + 'Navigate the user to a frontend route. Use this when the user says "show me the article" or asks to see, open, or view a specific page or entity (e.g. an article detail page, orders, quotes, deliveries, or customer). Refer to the User Perspective Route Construction Matrix for valid route patterns.', + strict: false, + parameters: { + type: 'object', + additionalProperties: false, + required: ['route'], + properties: { + route: { + type: 'string', + description: 'Target frontend route path according to the Route Construction Matrix (e.g. "/artikel/ART-10042", "/kategorien/elektronik/kabel", "/auftrag/1001").', + }, + }, + }, + invoke: async ({ route }) => { + const trimmedRoute = typeof route === 'string' ? route.trim() : ''; + if (!trimmedRoute) { + throw new Error('route is required'); + } + if (typeof onRoute === 'function') { + try { + onRoute(trimmedRoute); + } catch (err) { + console.error('set-user-route onRoute callback error:', err); + } + } + return { + success: true, + route: trimmedRoute, + message: `Navigated user to ${trimmedRoute}`, + }; + }, + }); +} diff --git a/todo.md b/todo.md index 48a7a1c..a2c3f3e 100644 --- a/todo.md +++ b/todo.md @@ -9,4 +9,23 @@ Mein Aufräumskript löste eine Fehlermeldung aus GET /articleIndex( windowSize ~25 Artikel mit den Bezeichnungen ZZ-*(ART-000478 … ART-000503) Attributname ZZ-StressAttr(ID 14) + seine Werte, Eigenschaftsname ZZ-StressPropund zwei Kategorien mit den Namen ZZ-STRESS(IDs 76, 77) -attributename/orderDer Aufruf von Note war [1, 14]erfolgreich, daher wurde auch die Sortierposition von Farbe geändert – möglicherweise ist eine manuelle Wiederherstellung erforderlich. Geben Sie mir Bescheid, und ich führe den Teardown erneut auf 1000 Seiten durch. \ No newline at end of file +attributename/orderDer Aufruf von Note war [1, 14]erfolgreich, daher wurde auch die Sortierposition von Farbe geändert – möglicherweise ist eine manuelle Wiederherstellung erforderlich. Geben Sie mir Bescheid, und ich führe den Teardown erneut auf 1000 Seiten durch. + + +# ERP Ledger Architecture: TSE Genesis Seed + +## Core Idea +We use a KassenSichV **`SonstigerVorgang`** transaction as the absolute cryptographic "Genesis Block" for our ERP ledger. Instead of using artificial software zero-hashes, the ledger's initial trust state is anchored in a legally binding, physical hardware/cloud TSE signature. + +## Mapping Overview + +| ERP Ledger Element | TSE Implementation | Purpose | +| :--- | :--- | :--- | +| **Genesis Entry** | `processType: "SonstigerVorgang"` | Legally logs system initialization for tax auditors. | +| **Seed Metadata** | `processData: "System-Event: Ledger-Init..."` | Explains the entry purpose transparently. | +| **`prev_hash` Anchor** | Returned TSE `signature` / `tx_id` | Becomes the cryptographic baseline for Transaction #2. | + +## Quick Validation Loop +```text +[TSE Signed Genesis] -> [Tx 2 (prev_hash = Genesis Signature)] -> [Tx 3] -> ... +```