diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index eb278a1..4f32368 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -126,8 +126,8 @@ _none_ - Params: `sku: str, body: { eans: str[] }` - Returns: `204 void` #### Get article history by SKU — `GET /article/{sku}/history` -- Params: `sku: str, query: limit?: int` -- Returns: `200 { history: { id: int, date: str, text: str }[], truncated: bool }` +- Params: `sku: str, query: offset?: int, limit?: int, search?: str, seekMonths?: int` +- Returns: `200 { history: { id: int, date: str, text: str }[], total: int, unfilteredTotal: int, offset: int, limit: int }` #### Add article image — `POST /article/{sku}/image` - Params: `sku: str, image/avif` - Returns: `201 { image: { picId: int, sort: int, full: { w: int, h: int, size: int }, thumb: { w: int, h: int }, hash: str, thumbHash: str } }` diff --git a/docs/api/routes.md b/docs/api/routes.md index bf33855..3a4fda6 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -92,8 +92,8 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Params: `sku: str, body: { eans: str[] }` - Returns: `204 void` #### Get article history by SKU — `GET /article/{sku}/history` -- Params: `sku: str, query: limit?: int` -- Returns: `200 { history: { id: int, date: str, text: str }[], truncated: bool }` +- Params: `sku: str, query: offset?: int, limit?: int, search?: str, seekMonths?: int` +- Returns: `200 { history: { id: int, date: str, text: str }[], total: int, unfilteredTotal: int, offset: int, limit: int }` #### Delete article image — `DELETE /article/{sku}/image/{fullHash}` - Params: `sku: str, fullHash: str` - Returns: `204 void` diff --git a/docs/api/routes/article/history.md b/docs/api/routes/article/history.md index b0125aa..bdf16b8 100644 --- a/docs/api/routes/article/history.md +++ b/docs/api/routes/article/history.md @@ -5,6 +5,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). Get article history by SKU Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC). +Supports pagination, search filter, and date cutoff via `seekMonths`. Auth like GET by SKU. @@ -18,11 +19,14 @@ Auth like GET by SKU. | Param | Required | Type | Description | |-------|----------|------|-------------| -| `limit` | no | integer | Maximum number of entries to return (newest first). Omit for all entries. | +| `offset` | no | integer | Paging offset | +| `limit` | no | integer | Page size | +| `search` | no | string | Optional search filter for history text | +| `seekMonths` | no | integer | Negative integer cutoff months ago (e.g. -13) | ## Response `200` -Article history (newest first) +Paged article history list ```json { @@ -37,7 +41,11 @@ Article history (newest first) "date": "2026-08-01T09:00:00.000Z", "text": "Name geändert von \"\" auf \"Tee\"" } - ] + ], + "total": 2, + "unfilteredTotal": 2, + "offset": 0, + "limit": 50 } ``` @@ -45,7 +53,7 @@ Article history (newest first) | Status | Description | |--------|-------------| -| 400 | Missing sku — `{"error":"…"}` | +| 400 | Missing sku or invalid query parameters — `{"error":"…"}` | | 401 | Unauthorized — `{"error":"…"}` | | 403 | Not authorized — `{"error":"…"}` | | 404 | Article not found — `{"error":"…"}` | diff --git a/package-lock.json b/package-lock.json index ed61c71..914851f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor", "selfsigned": "^5.5.0", "sharp": "^0.35.5", - "skelehash-wasm": "^0.1.0", + "skelehash-wasm": "file:../skelehash-wasm", "swagger-ui-dist": "^5.33.3", "vm2": "^3.12.2", "zod": "^4.6.7" @@ -72,6 +72,7 @@ } }, "../llmorbiter": { + "name": "llmorbiter", "version": "0.1.0", "license": "0BSD", "dependencies": { @@ -81,6 +82,13 @@ "node": ">=18.0.0" } }, + "../skelehash-wasm": { + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "sharp": "^0.35.5" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.1.tgz", @@ -216,9 +224,9 @@ } }, "node_modules/@hpcc-js/wasm-graphviz": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.29.2.tgz", - "integrity": "sha512-AkArBAZERT5OxH52/+kvCHB07cZ6ZNd9GnOvjw+S880bR88D6i27XfK6xj4dBhUDxNA2QPPMdgEcky3x4TJ3KA==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.30.0.tgz", + "integrity": "sha512-T20IKosOZuex2qV049fQokVJy2MWv8w9Md5V/4mRKCXDf49fBCR50VD1yBlPYHaQlHPsFGRdqXN//r59vZliWQ==", "dev": true, "license": "Apache-2.0" }, @@ -787,9 +795,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.32.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", - "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", + "version": "1.32.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.2.tgz", + "integrity": "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", @@ -1467,18 +1475,18 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^2.0.0", + "content-type": "^2.1.0", "debug": "^4.4.3", "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "^0.7.3", "on-finished": "^2.4.1", - "qs": "^6.15.2", + "qs": "^6.16.1", "raw-body": "^3.0.2", "type-is": "^2.1.0" }, @@ -2267,9 +2275,9 @@ } }, "node_modules/dotenv": { - "version": "18.0.8", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", - "integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.9.tgz", + "integrity": "sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw==", "license": "BSD-2-Clause", "bin": { "dotenv": "dist/index.cjs" @@ -2748,9 +2756,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "funding": [ { "type": "github", @@ -2988,9 +2996,9 @@ } }, "node_modules/gaxios": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.1.tgz", - "integrity": "sha512-h4/R7ay7JFXU5kmAkp/Y4Y4Vmm7m83tD579GUgfQQsRRGQ4TzSedyHla6XgTWohQuVffT3ldN4FF6uCqlRGaEA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.2.tgz", + "integrity": "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -3002,9 +3010,9 @@ } }, "node_modules/gcp-metadata": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.1.tgz", - "integrity": "sha512-5/LvxarRKbGCA5LJ+ha2aV1iVm7/aALIW4PzeXnQR5dyXiNMWbeA4UOBnqERInLfH/xSFp0ZREcxodUzxhEw2Q==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.2.tgz", + "integrity": "sha512-hlaHpCBoNCzoUjMnBZq0LbUiYqPAhXFaAgyNwQqwWU44TZIYJK3RD3o2wkNdvPs8nnUXqJjTudnodLJkLMBjow==", "license": "Apache-2.0", "dependencies": { "gaxios": "^8.3.0", @@ -3926,9 +3934,9 @@ } }, "node_modules/jose": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", - "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz", + "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -4439,9 +4447,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", - "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.5.0.tgz", + "integrity": "sha512-BsqOzYGVq49JcW2IzoSAxeKYRevb+dGGPPWleOR8Lrlk4igwxu1/9pwSXlByhCb7MVZDrNJFC0IBP5/O5nLYRw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -4535,9 +4543,9 @@ } }, "node_modules/node-abi": { - "version": "3.99.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.99.0.tgz", - "integrity": "sha512-lLdwkeg2XUVPTHqWjPltTxH81zbQNbUWC+AEb9ni50KjxkUzAAx1HGtMT21sbntzfUGfIphhcklvs3imJeqDXw==", + "version": "3.101.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.101.0.tgz", + "integrity": "sha512-bkb/RkuWCE64eWu1PCh3Fu2pGc9mKRs+kOE+FU2bGHRYlQa86/D321DSATV54AsCQMKz3/xSt9sRgraZTpqdhQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -4653,15 +4661,19 @@ } }, "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz", + "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/once": { @@ -4865,9 +4877,9 @@ } }, "node_modules/pkijs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.1.tgz", - "integrity": "sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.3.tgz", + "integrity": "sha512-V9kOzaItwDgqlryhXysQsAXUEKi5O7kqWUaP3dXI7MLvkPqWWMb4wVb1QAtmEDCKI+XIAINsZoNKMFF7sZb1eg==", "license": "BSD-3-Clause", "workspaces": [ "website" @@ -5727,10 +5739,8 @@ } }, "node_modules/skelehash-wasm": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.0.tgz", - "integrity": "sha512-QdxF9YsJvKlT93O70dXFaWnsKVX7KDD0RgYf9cwgOT6kGnSMOdsO0rsdge+5Tg+vv6BjivbJ/2o5P1KcQsavEw==", - "license": "MIT" + "resolved": "../skelehash-wasm", + "link": true }, "node_modules/slice-ansi": { "version": "9.0.1", @@ -6000,9 +6010,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.33.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.33.3.tgz", - "integrity": "sha512-7xJKxAjf+/ajlW/V/dMfbiY+NC5BWtbvzzdI/AsX5UzMtOMudmFz/tjuKN0jN7QVe5drKKnx01CdYEJvTFvQlw==", + "version": "5.34.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.34.0.tgz", + "integrity": "sha512-H6tAIjr3XqE31PbkZl0OFya1ziRLDfTwh3J6OHdumPtPCLfjTnyLeagl/55kFNZcTOz9a9QI9WP0FXldwiGJag==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/package.json b/package.json index 9603c0d..6472c37 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor", "selfsigned": "^5.5.0", "sharp": "^0.35.5", - "skelehash-wasm": "^0.1.0", + "skelehash-wasm": "file:../skelehash-wasm", "swagger-ui-dist": "^5.33.3", "vm2": "^3.12.2", "zod": "^4.6.7" @@ -60,4 +60,4 @@ "uuid": "^11.1.1" } } -} +} \ No newline at end of file diff --git a/src/api/routes/article/crud.js b/src/api/routes/article/crud.js index f06ab97..53db475 100644 --- a/src/api/routes/article/crud.js +++ b/src/api/routes/article/crud.js @@ -602,6 +602,7 @@ export function register(app, basePath = '') { * summary: Get article history by SKU * description: | * Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC). + * Supports pagination, search filter, and date cutoff via `seekMonths`. * Auth like GET by SKU. * security: * - cookieAuth: [] @@ -614,29 +615,51 @@ export function register(app, basePath = '') { * type: string * description: Article SKU * - in: query + * name: offset + * schema: + * type: integer + * minimum: 0 + * default: 0 + * description: Paging offset + * - in: query * name: limit - * required: false * schema: * type: integer * minimum: 1 * maximum: 1000 - * description: Maximum number of entries to return (newest first). Omit for all entries. + * default: 50 + * description: Page size + * - in: query + * name: search + * schema: + * type: string + * description: Optional search filter for history text + * - in: query + * name: seekMonths + * schema: + * type: integer + * description: Negative integer cutoff months ago (e.g. -13) * responses: * 200: - * description: Article history (newest first) + * description: Paged article history list * content: * application/json: * schema: * type: object - * required: [history, truncated] + * required: [history, total, unfilteredTotal, offset, limit] * properties: * history: * type: array * items: * $ref: '#/components/schemas/ArticleHistoryEntry' - * truncated: - * type: boolean - * description: True when a limit was applied and the result set was exactly that size (more entries may exist). + * total: + * type: integer + * unfilteredTotal: + * type: integer + * offset: + * type: integer + * limit: + * type: integer * example: * history: * - id: 42 @@ -645,8 +668,12 @@ export function register(app, basePath = '') { * - id: 41 * date: '2026-08-01T09:00:00.000Z' * text: 'Name geändert von "" auf "Tee"' + * total: 2 + * unfilteredTotal: 2 + * offset: 0 + * limit: 50 * 400: - * description: Missing sku + * description: Missing sku or invalid query parameters * content: * application/json: * schema: @@ -678,8 +705,19 @@ export function register(app, basePath = '') { return res.status(400).json({ error: 'sku is required' }); } + const rawOffset = req.query.offset; const rawLimit = req.query.limit; - let limit = null; + + let offset = 0; + let limit = 50; + + if (rawOffset !== undefined) { + offset = parseInt(rawOffset, 10); + if (!Number.isInteger(offset) || offset < 0) { + return res.status(400).json({ error: 'offset must be an integer >= 0' }); + } + } + if (rawLimit !== undefined) { limit = parseInt(rawLimit, 10); if (!Number.isInteger(limit) || limit < 1 || limit > 1000) { @@ -687,10 +725,26 @@ export function register(app, basePath = '') { } } + const search = typeof req.query.search === 'string' && req.query.search.trim() !== '' + ? req.query.search.trim() + : undefined; + + let seekMonths = undefined; + if (req.query.seekMonths !== undefined) { + seekMonths = parseInt(req.query.seekMonths, 10); + if (!Number.isInteger(seekMonths)) { + return res.status(400).json({ error: 'seekMonths must be an integer' }); + } + } + try { - const history = await db.getHistoryBySku(req.user.id, decodeURIComponent(sku), limit != null ? { limit } : undefined); - const truncated = limit != null && history.length === limit; - res.status(200).json({ history, truncated }); + const result = await db.getHistoryBySku(req.user.id, decodeURIComponent(sku), { + offset, + limit, + search, + seekMonths, + }); + res.status(200).json(result); } catch (error) { const status = articleErrorStatus(error); if (status === 500) console.error('Get article history by sku failed:', error);