This commit is contained in:
seb
2026-11-01 05:37:15 +01:00
parent e6c15dd053
commit 8f9c941699
32 changed files with 145 additions and 210 deletions

View File

@@ -182,11 +182,11 @@ _none_
#### Toggle parent mode for an article — `POST /article/{sku}/parent` #### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `204 void` - Returns: `204 void`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices` #### Replace POS prices by SKU (net > gross) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Params: `sku: str, body: { prices: { net?: num, gross?: num, taxRate?: num, minmultiplier?: num }[] }`
- Returns: `204 void` - Returns: `204 void`
#### Print article label — `POST /article/{sku}/print` #### Print article label — `POST /article/{sku}/print`
- Params: `sku: str, body: { templateId: int, printerId: int, quantity: int }` - Params: `sku: str, body: { templateId: int, agentprinterId: int, quantity: int }`
- Returns: `200 { success: bool }` - Returns: `200 { success: bool }`
#### List article properties — `GET /article/{sku}/property` #### List article properties — `GET /article/{sku}/property`
- Params: `sku: str` - Params: `sku: str`
@@ -559,7 +559,7 @@ _none_
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] } }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] } }`
- Returns: `201 { labeltemplate: T9 }` - Returns: `201 { labeltemplate: T9 }`
#### Test print label template — `POST /labeltemplate/testprint` #### Test print label template — `POST /labeltemplate/testprint`
- Params: `body: { printerId?: int, agentprinterId?: int, templateId?: int, objects?: any[], width?: int, height?: int, dpi?: int, sku?: str, quantity?: int }` - Params: `body: { agentprinterId: int, templateId?: int, objects?: any[], width?: int, height?: int, dpi?: int, sku?: str, quantity?: int }`
- Returns: `200 { success: bool }` - Returns: `200 { success: bool }`
#### Delete label template — `DELETE /labeltemplate/{id}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`

View File

@@ -145,11 +145,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
#### Toggle parent mode for an article — `POST /article/{sku}/parent` #### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `204 void` - Returns: `204 void`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices` #### Replace POS prices by SKU (net > gross) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Params: `sku: str, body: { prices: { net?: num, gross?: num, taxRate?: num, minmultiplier?: num }[] }`
- Returns: `204 void` - Returns: `204 void`
#### Print article label — `POST /article/{sku}/print` #### Print article label — `POST /article/{sku}/print`
- Params: `sku: str, body: { templateId: int, printerId: int, quantity: int }` - Params: `sku: str, body: { templateId: int, agentprinterId: int, quantity: int }`
- Returns: `200 { success: bool }` - Returns: `200 { success: bool }`
#### List article properties — `GET /article/{sku}/property` #### List article properties — `GET /article/{sku}/property`
- Params: `sku: str` - Params: `sku: str`
@@ -431,7 +431,7 @@ _none_
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T19[] } }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T19[] } }`
- Returns: `201 { labeltemplate: T6 }` - Returns: `201 { labeltemplate: T6 }`
#### Test print label template — `POST /labeltemplate/testprint` #### Test print label template — `POST /labeltemplate/testprint`
- Params: `body: { printerId?: int, agentprinterId?: int, templateId?: int, objects?: any[], width?: int, height?: int, dpi?: int, sku?: str, quantity?: int }` - Params: `body: { agentprinterId: int, templateId?: int, objects?: any[], width?: int, height?: int, dpi?: int, sku?: str, quantity?: int }`
- Returns: `200 { success: bool }` - Returns: `200 { success: bool }`
#### Delete label template — `DELETE /labeltemplate/{id}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`

View File

@@ -17,13 +17,13 @@ Prints a label for the article using a label template.
| Field | Required | Type | Description | | Field | Required | Type | Description |
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `templateId` | yes | integer | | | `templateId` | yes | integer | |
| `printerId` | yes | integer | | | `agentprinterId` | yes | integer | |
| `quantity` | yes | integer | | | `quantity` | yes | integer | |
```json ```json
{ {
"templateId": 1, "templateId": 1,
"printerId": 1, "agentprinterId": 1,
"quantity": 1 "quantity": 1
} }
``` ```

View File

@@ -2,11 +2,11 @@
Protected. Requires a valid session cookie (`credentials: 'include'`). Protected. Requires a valid session cookie (`credentials: 'include'`).
Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) Replace POS prices by SKU (net > gross)
Replace-all prices for the company POS customer group. Replace-all prices for the company POS customer group.
`net` takes precedence over `price` (legacy alias for net) and `gross`. `net` takes precedence over `gross`.
`tax` and `taxRate` both specify the tax rate percentage (e.g. 19 for 19%), not monetary tax. `taxRate` specifies the tax rate percentage (e.g. 19 for 19%), not monetary tax.
When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`. When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`.
Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET). Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET).
Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
@@ -23,11 +23,9 @@ Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
| Field | Required | Type | Description | | Field | Required | Type | Description |
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `prices` | yes | object[] | | | `prices` | yes | object[] | |
| `prices[].net` | no | number | Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required. | | `prices[].net` | no | number | Net price (excl. VAT). Either `net` or `gross` is required. |
| `prices[].gross` | no | number | Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. | | `prices[].gross` | no | number | Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. |
| `prices[].price` | no | number | Legacy alias for `net`. |
| `prices[].taxRate` | no | number | Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). | | `prices[].taxRate` | no | number | Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). |
| `prices[].tax` | no | number | Alias for `taxRate` percentage. |
| `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request. | | `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request. |
```json ```json

View File

@@ -12,8 +12,7 @@ Placeholders in text and barcodes are interpolated using a sample article or the
| Field | Required | Type | Description | | Field | Required | Type | Description |
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `printerId` | no | integer | | | `agentprinterId` | yes | integer | Agent printer ID |
| `agentprinterId` | no | integer | |
| `templateId` | no | integer | | | `templateId` | no | integer | |
| `objects` | no | any[] | | | `objects` | no | any[] | |
| `width` | no | integer | | | `width` | no | integer | |
@@ -24,7 +23,6 @@ Placeholders in text and barcodes are interpolated using a sample article or the
```json ```json
{ {
"printerId": 1,
"agentprinterId": 1, "agentprinterId": 1,
"templateId": 1, "templateId": 1,
"objects": [], "objects": [],

View File

@@ -367,7 +367,7 @@ agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
*/ */
async function handleState(req, res) { async function handleState(req, res) {
try { try {
const rawState = req.body?.state !== undefined ? req.body.state : (req.body?.clientstate !== undefined ? req.body.clientstate : req.body); const rawState = req.body?.state;
setAgentState(req.agent.id, rawState); setAgentState(req.agent.id, rawState);
const version = extractAgentVersion(req); const version = extractAgentVersion(req);
@@ -377,9 +377,7 @@ async function handleState(req, res) {
await db.touchLastseen(req.agent.id); await db.touchLastseen(req.agent.id);
} }
const rawPrinters = Array.isArray(rawState?.printers) const rawPrinters = Array.isArray(rawState?.printers) ? rawState.printers : null;
? rawState.printers
: (Array.isArray(req.body?.printers) ? req.body.printers : null);
let anyAgentPrinterCreated = false; let anyAgentPrinterCreated = false;
if (rawPrinters && rawPrinters.length > 0) { if (rawPrinters && rawPrinters.length > 0) {

View File

@@ -106,7 +106,7 @@ function corsMiddleware(req, res, next) {
const reqHeaders = req.headers['access-control-request-headers']; const reqHeaders = req.headers['access-control-request-headers'];
res.setHeader( res.setHeader(
'Access-Control-Allow-Headers', 'Access-Control-Allow-Headers',
reqHeaders || 'Content-Type, Authorization, x-agent-token, x-auth-token, authtoken, auth-token, token, x-token, agentversion, clientversion, version' reqHeaders || 'Content-Type, Authorization, x-agent-token, agentversion'
); );
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {

View File

@@ -10,16 +10,12 @@ export function extractAgentToken(req) {
return trimmed; return trimmed;
} }
for (const key of ['x-agent-token', 'x-auth-token', 'authtoken', 'auth-token', 'token', 'x-token']) { if (typeof req.headers['x-agent-token'] === 'string' && req.headers['x-agent-token'].trim()) {
const val = req.headers[key]; return req.headers['x-agent-token'].trim();
if (typeof val === 'string' && val.trim()) {
return val.trim();
}
} }
const queryToken = req.query?.authToken || req.query?.token; if (typeof req.query?.token === 'string' && req.query.token.trim()) {
if (typeof queryToken === 'string' && queryToken.trim()) { return req.query.token.trim();
return queryToken.trim();
} }
return ''; return '';
@@ -27,13 +23,8 @@ export function extractAgentToken(req) {
export function extractAgentVersion(req) { export function extractAgentVersion(req) {
const raw = req.headers['agentversion'] || const raw = req.headers['agentversion'] ||
req.headers['agent-version'] ||
req.headers['clientversion'] ||
req.headers['version'] ||
req.query?.agentversion || req.query?.agentversion ||
req.query?.version ||
req.body?.agentversion || req.body?.agentversion ||
req.body?.version ||
null; null;
return raw ? String(raw).trim() : null; return raw ? String(raw).trim() : null;
} }

View File

@@ -17,11 +17,11 @@ export function register(app, basePath = '') {
* /article/{sku}/prices: * /article/{sku}/prices:
* put: * put:
* tags: [Article] * tags: [Article]
* summary: Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) * summary: Replace POS prices by SKU (net > gross)
* description: | * description: |
* Replace-all prices for the company POS customer group. * Replace-all prices for the company POS customer group.
* `net` takes precedence over `price` (legacy alias for net) and `gross`. * `net` takes precedence over `gross`.
* `tax` and `taxRate` both specify the tax rate percentage (e.g. 19 for 19%), not monetary tax. * `taxRate` specifies the tax rate percentage (e.g. 19 for 19%), not monetary tax.
* When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`. * When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`.
* Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET). * Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET).
* Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. * Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
@@ -50,19 +50,13 @@ export function register(app, basePath = '') {
* properties: * properties:
* net: * net:
* type: number * type: number
* description: Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required. * description: Net price (excl. VAT). Either `net` or `gross` is required.
* gross: * gross:
* type: number * type: number
* description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. * description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`.
* price:
* type: number
* description: Legacy alias for `net`.
* taxRate: * taxRate:
* type: number * type: number
* description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). * description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%).
* tax:
* type: number
* description: Alias for `taxRate` percentage.
* minmultiplier: * minmultiplier:
* type: number * type: number
* description: Defaults to 1; must be unique per request. * description: Defaults to 1; must be unique per request.

View File

@@ -14,6 +14,7 @@ import {
parseFullHash, parseFullHash,
articleErrorStatus, articleErrorStatus,
interpolateString, interpolateString,
interpolateObjects,
} from './shared.js'; } from './shared.js';
/** /**
@@ -433,16 +434,7 @@ export function register(app, basePath = '') {
? content ? content
: []; : [];
const interpolatedObjects = objects.map(obj => { const interpolatedObjects = interpolateObjects(objects, article);
const cloned = { ...obj };
if (typeof cloned.text === 'string') {
cloned.text = interpolateString(cloned.text, article);
}
if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
return cloned;
});
const printPayload = await renderLabel({ const printPayload = await renderLabel({
width: template.width, width: template.width,

View File

@@ -117,20 +117,57 @@ export function articleErrorStatus(error) {
return 500; return 500;
} }
const CURRENCY_FORMATTER = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
});
export function formatPrice(amount) {
if (amount == null || !Number.isFinite(Number(amount))) return '';
return CURRENCY_FORMATTER.format(Number(amount));
}
export function interpolateString(str, article) { export function interpolateString(str, article) {
if (typeof str !== 'string') return str; if (typeof str !== 'string') return str;
const net = article.net_price ?? article.prices?.[0]?.net; const basePrice = Array.isArray(article?.prices) && article.prices.length > 0
const taxRate = article.prices?.[0]?.taxRate ?? 0; ? article.prices[0]
const gross = net != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null; : null;
const ean = (article.eans && article.eans.length > 0) ? article.eans[0] : ''; const net = basePrice?.net != null && Number.isFinite(Number(basePrice.net))
? Number(basePrice.net)
: null;
const taxRate = basePrice?.taxRate != null && Number.isFinite(Number(basePrice.taxRate))
? Number(basePrice.taxRate)
: 0;
const gross = net != null
? Math.round((net * (1 + taxRate / 100) + Number.EPSILON) * 100) / 100
: null;
return str return str
.replace(/\{\{(?:ARTICLE_NAME|article_name|name)\}\}/gi, article.name || '') .replace(/\{\{name\}\}/gi, article?.name || '')
.replace(/\{\{SKU\}\}/gi, article.sku || '') .replace(/\{\{price\}\}/gi, formatPrice(gross));
.replace(/\{\{(?:NET_PRICE|net_price)\}\}/gi, net != null ? net.toString() : '') }
.replace(/\{\{(?:GROSS_PRICE|gross_price)\}\}/gi, gross != null ? gross.toString() : '')
.replace(/\{\{(?:PRICE|price)\}\}/gi, gross != null ? gross.toString() : (net != null ? net.toString() : '')) export function interpolateObjects(objects, article) {
.replace(/\{\{(?:EAN|ean)\}\}/gi, ean); if (!Array.isArray(objects)) return [];
const ean = Array.isArray(article?.eans) && article.eans.length > 0 ? String(article.eans[0]) : '';
return objects.map((obj) => {
const cloned = { ...obj };
if (typeof cloned.text === 'string') {
cloned.text = interpolateString(cloned.text, article);
}
if (cloned.type === 'barcode' || typeof cloned.code === 'string') {
const rawCode = String(cloned.code ?? '').trim();
if (rawCode === '0' || /^0+$/.test(rawCode)) {
if (ean) {
cloned.code = ean;
}
} else if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
}
return cloned;
});
} }
// #endregion // #endregion

View File

@@ -253,7 +253,7 @@ export function register(app, basePath = '') {
const parsed = parseSku(req.params.sku); const parsed = parseSku(req.params.sku);
if (parsed.error) return res.status(400).json({ error: parsed.error }); if (parsed.error) return res.status(400).json({ error: parsed.error });
if (req.body?.sku || req.body?.childSku) { if (req.body?.sku) {
return res.status(400).json({ return res.status(400).json({
error: 'sku is not allowed when creating a child article; omit sku to auto-generate or use PUT to attach an existing article', error: 'sku is not allowed when creating a child article; omit sku to auto-generate or use PUT to attach an existing article',
}); });
@@ -282,9 +282,7 @@ export function register(app, basePath = '') {
const parsed = parseSku(req.params.sku); const parsed = parseSku(req.params.sku);
if (parsed.error) return res.status(400).json({ error: parsed.error }); if (parsed.error) return res.status(400).json({ error: parsed.error });
const childSku = typeof req.body?.sku === 'string' && req.body.sku.trim() const childSku = typeof req.body?.sku === 'string' ? req.body.sku.trim() : '';
? req.body.sku.trim()
: (typeof req.body?.childSku === 'string' && req.body.childSku.trim() ? req.body.childSku.trim() : '');
if (!childSku) { if (!childSku) {
return res.status(400).json({ error: 'sku is required' }); return res.status(400).json({ error: 'sku is required' });
} }
@@ -354,7 +352,7 @@ export function register(app, basePath = '') {
const parsedChild = parseSku(req.params.childSku); const parsedChild = parseSku(req.params.childSku);
if (parsedChild.error) return res.status(400).json({ error: 'childSku is required' }); if (parsedChild.error) return res.status(400).json({ error: 'childSku is required' });
const deleteArticle = req.query.deleteArticle === 'true' || req.body?.deleteArticle === true; const deleteArticle = req.query.deleteArticle === 'true';
try { try {
const result = await db.removeChild(req.user.id, parsed.sku, parsedChild.sku, { const result = await db.removeChild(req.user.id, parsed.sku, parsedChild.sku, {

View File

@@ -141,7 +141,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: skuParsed.error }); return res.status(400).json({ error: skuParsed.error });
} }
const propertiesInput = req.body?.properties ?? req.body; const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) { if (!Array.isArray(propertiesInput)) {
return res.status(400).json({ error: 'properties array required' }); return res.status(400).json({ error: 'properties array required' });
} }

View File

@@ -252,12 +252,12 @@ export function register(app, basePath = '') {
// #region POST /backup/:id/vfs/recover // #region POST /backup/:id/vfs/recover
// MARK: handles POST /backup/:id/vfs/recover to restore a VFS file from a backup // MARK: handles POST /backup/:id/vfs/recover to restore a VFS file from a backup
const handleRecover = async (req, res) => { const handleRecover = async (req, res) => {
const filePath = req.params.path || req.body?.path || req.body?.file || req.body?.filename || req.query?.path; const filePath = req.body?.path;
if (!filePath) { if (!filePath) {
return res.status(400).json({ error: 'File path is required' }); return res.status(400).json({ error: 'File path is required' });
} }
const requestedCompanyId = req.body?.companyId ?? req.query?.companyId; const requestedCompanyId = req.body?.companyId;
const authorized = await resolveAuthorizedCompany(req, requestedCompanyId); const authorized = await resolveAuthorizedCompany(req, requestedCompanyId);
if (authorized.error) { if (authorized.error) {
return res.status(authorized.status).json({ error: authorized.error }); return res.status(authorized.status).json({ error: authorized.error });

View File

@@ -384,8 +384,6 @@ export function register(app, basePath = '') {
let includeChildren = true; let includeChildren = true;
if (req.query.includeChildren !== undefined) { if (req.query.includeChildren !== undefined) {
includeChildren = req.query.includeChildren !== 'false' && req.query.includeChildren !== '0'; includeChildren = req.query.includeChildren !== 'false' && req.query.includeChildren !== '0';
} else if (req.query.variants !== undefined) {
includeChildren = req.query.variants !== 'false' && req.query.variants !== '0';
} }
try { try {
@@ -395,7 +393,7 @@ export function register(app, basePath = '') {
.map((a) => ({ .map((a) => ({
...a, ...a,
childCount: Number(a.childCount ?? 0), childCount: Number(a.childCount ?? 0),
isParent: Boolean(a.isParent ?? a.is_parent), isParent: Boolean(a.isParent),
smallThumbW: a.smallThumbW ?? null, smallThumbW: a.smallThumbW ?? null,
smallThumbH: a.smallThumbH ?? null, smallThumbH: a.smallThumbH ?? null,
smallThumbBinary: a.smallThumbBinary ?? null, smallThumbBinary: a.smallThumbBinary ?? null,

View File

@@ -153,7 +153,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error }); return res.status(400).json({ error: idParsed.error });
} }
const propertiesInput = req.body?.properties ?? req.body; const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) { if (!Array.isArray(propertiesInput)) {
return res.status(400).json({ error: 'properties array required' }); return res.status(400).json({ error: 'properties array required' });
} }

View File

@@ -8,7 +8,7 @@ import { pngToZpl } from '../../services/zplService.js';
import { notifyAgentJob } from '../agent/index.js'; import { notifyAgentJob } from '../agent/index.js';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { applyZplCopies } from './zpljob.js'; import { applyZplCopies } from './zpljob.js';
import { interpolateString } from './article/shared.js'; import { interpolateString, interpolateObjects } from './article/shared.js';
// #region Helpers // #region Helpers
@@ -590,16 +590,15 @@ export function register(app, basePath = '') {
// Resolve sample article for placeholder interpolation // Resolve sample article for placeholder interpolation
let article = null; let article = null;
if (req.body?.sku) { if (req.body?.sku) {
try { const skuStr = String(req.body.sku).trim();
const result = await articleDb.getBySku(req.user.id, req.body.sku); const result = await articleDb.getBySku(req.user.id, skuStr);
article = result?.article; if (!result?.article) {
} catch { return res.status(404).json({ error: 'Article not found' });
// ignore
} }
} article = result.article;
if (!article) { } else {
article = { article = {
name: req.body?.name || template?.name || 'Musterartikel', name: template?.name || 'Musterartikel',
sku: 'TEST-12345', sku: 'TEST-12345',
prices: [{ net: 19.99, taxRate: 19 }], prices: [{ net: 19.99, taxRate: 19 }],
eans: ['4012345678901'], eans: ['4012345678901'],
@@ -625,16 +624,7 @@ export function register(app, basePath = '') {
objects = []; objects = [];
} }
const interpolatedObjects = objects.map((obj) => { const interpolatedObjects = interpolateObjects(objects, article);
const cloned = { ...obj };
if (typeof cloned.text === 'string') {
cloned.text = interpolateString(cloned.text, article);
}
if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
return cloned;
});
const printPayload = await renderLabel({ const printPayload = await renderLabel({
width: width || 448, width: width || 448,

View File

@@ -101,7 +101,7 @@ export function register(app, basePath = '') {
} }
const manifestarticleId = Number(req.params.manifestarticleId); const manifestarticleId = Number(req.params.manifestarticleId);
const transactionType = req.query.transaction_type ?? req.query.transactionType; const transactionType = req.query.transaction_type;
try { try {
const ledger = await db.listByManifestarticleId( const ledger = await db.listByManifestarticleId(
@@ -186,7 +186,7 @@ export function register(app, basePath = '') {
} }
const sku = req.params.sku; const sku = req.params.sku;
const transactionType = req.query.transaction_type ?? req.query.transactionType; const transactionType = req.query.transaction_type;
try { try {
const ledger = await db.listBySku(req.user.id, sku, transactionType); const ledger = await db.listBySku(req.user.id, sku, transactionType);
@@ -254,9 +254,9 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const rawManifestarticleId = req.query.manifestarticle_id ?? req.query.manifestarticleId; const rawManifestarticleId = req.query.manifestarticle_id;
const sku = req.query.sku; const sku = req.query.sku;
const transactionType = req.query.transaction_type ?? req.query.transactionType; const transactionType = req.query.transaction_type;
if (rawManifestarticleId == null && (sku == null || !String(sku).trim())) { if (rawManifestarticleId == null && (sku == null || !String(sku).trim())) {
return res.status(400).json({ return res.status(400).json({

View File

@@ -190,12 +190,8 @@ export function register(app, basePath = '') {
} }
const pairingpin = String(randomInt(100000, 999999)); const pairingpin = String(randomInt(100000, 999999));
const rootCategoryId = req.body.rootCategoryId !== undefined const rootCategoryId = req.body.rootCategoryId;
? req.body.rootCategoryId const syncRootArticles = req.body.syncRootArticles;
: req.body.root_category_id;
const syncRootArticles = req.body.syncRootArticles !== undefined
? req.body.syncRootArticles
: req.body.sync_root_articles;
const sort = req.body.sort !== undefined ? Number(req.body.sort) : undefined; const sort = req.body.sort !== undefined ? Number(req.body.sort) : undefined;
if (sort !== undefined && !Number.isInteger(sort)) { if (sort !== undefined && !Number.isInteger(sort)) {
return res.status(400).json({ error: 'sort must be an integer' }); return res.status(400).json({ error: 'sort must be an integer' });
@@ -319,14 +315,12 @@ export function register(app, basePath = '') {
payload.name = req.body.name.trim(); payload.name = req.body.name.trim();
} }
if (req.body.rootCategoryId !== undefined || req.body.root_category_id !== undefined) { if (req.body.rootCategoryId !== undefined) {
const rawCat = req.body.rootCategoryId !== undefined ? req.body.rootCategoryId : req.body.root_category_id; payload.rootCategoryId = req.body.rootCategoryId != null ? Number(req.body.rootCategoryId) : null;
payload.rootCategoryId = rawCat != null ? Number(rawCat) : null;
} }
if (req.body.syncRootArticles !== undefined || req.body.sync_root_articles !== undefined) { if (req.body.syncRootArticles !== undefined) {
const rawSync = req.body.syncRootArticles !== undefined ? req.body.syncRootArticles : req.body.sync_root_articles; payload.syncRootArticles = Boolean(req.body.syncRootArticles);
payload.syncRootArticles = Boolean(rawSync);
} }
if (req.body.sort !== undefined) { if (req.body.sort !== undefined) {

View File

@@ -221,7 +221,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const rawApId = req.body?.agentprinterId ?? req.body?.agentprinterid; const rawApId = req.body?.agentprinterId;
const parsedApId = parsePositiveInt(rawApId, 'agentprinterId'); const parsedApId = parsePositiveInt(rawApId, 'agentprinterId');
if (parsedApId.error) { if (parsedApId.error) {
return res.status(400).json({ error: parsedApId.error }); return res.status(400).json({ error: parsedApId.error });
@@ -302,7 +302,7 @@ export function register(app, basePath = '') {
} }
try { try {
const rawApId = req.body?.agentprinterId ?? req.query?.agentprinterId; const rawApId = req.body?.agentprinterId;
const agentprinterId = rawApId != null ? Number(rawApId) : undefined; const agentprinterId = rawApId != null ? Number(rawApId) : undefined;
const allJobs = await printerjobDb.list(req.user.id, { agentprinterId }); const allJobs = await printerjobDb.list(req.user.id, { agentprinterId });
const cancellable = allJobs.filter((j) => j.state === 'pending' || j.state === 'processing'); const cancellable = allJobs.filter((j) => j.state === 'pending' || j.state === 'processing');

View File

@@ -517,7 +517,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error }); return res.status(400).json({ error: idParsed.error });
} }
const skus = parseSkus(req.query.skus ?? req.query.sku); const skus = parseSkus(req.query.skus);
try { try {
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus); const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);
@@ -540,7 +540,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error }); return res.status(400).json({ error: idParsed.error });
} }
const skus = parseSkus(req.body?.skus ?? req.body?.sku); const skus = parseSkus(req.body?.skus);
try { try {
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus); const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);

View File

@@ -685,7 +685,7 @@ export function register(app, basePath = '') {
if (selected.error) { if (selected.error) {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const raw = req.body?.path || req.body?.dir || req.body?.name; const raw = req.body?.path;
const parsed = parseVfsRef(raw); const parsed = parseVfsRef(raw);
if (parsed.error) { if (parsed.error) {
return res.status(400).json({ error: parsed.error }); return res.status(400).json({ error: parsed.error });
@@ -787,8 +787,8 @@ export function register(app, basePath = '') {
if (selected.error) { if (selected.error) {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const rawOld = req.body?.oldPath || req.body?.from || req.body?.source; const rawOld = req.body?.oldPath;
const rawNew = req.body?.newPath || req.body?.to || req.body?.target; const rawNew = req.body?.newPath;
const parsedOld = parseVfsRef(rawOld); const parsedOld = parseVfsRef(rawOld);
if (parsedOld.error) { if (parsedOld.error) {
return res.status(400).json({ error: `oldPath: ${parsedOld.error}` }); return res.status(400).json({ error: `oldPath: ${parsedOld.error}` });

View File

@@ -87,7 +87,7 @@ export function buildStudioApp() {
// 5. Insert row // 5. Insert row
app.post('/api/tables/:table/row', async (req, res) => { app.post('/api/tables/:table/row', async (req, res) => {
try { try {
const result = await dbHelper.insertRow(req.params.table, req.body.row || req.body); const result = await dbHelper.insertRow(req.params.table, req.body.row);
res.json(result); res.json(result);
} catch (err) { } catch (err) {
res.status(400).json({ error: err.message }); res.status(400).json({ error: err.message });
@@ -175,7 +175,3 @@ export async function stopBackendMonitor() {
}); });
monitorLog.info('Backend Monitor Interface stopped'); monitorLog.info('Backend Monitor Interface stopped');
} }
// Aliases for compatibility
export const startSqlStudio = startBackendMonitor;
export const stopSqlStudio = stopBackendMonitor;

View File

@@ -116,25 +116,21 @@ async function snapshotSqliteDatabase(targetFile) {
fs.unlinkSync(targetFile); fs.unlinkSync(targetFile);
} }
try { const provider = await getProvider();
const provider = await getProvider(); if (provider?.db && typeof provider.db.backup === 'function') {
if (provider?.db && typeof provider.db.backup === 'function') { await provider.db.backup(targetFile);
await provider.db.backup(targetFile); const stats = fs.statSync(targetFile);
const stats = fs.statSync(targetFile); return { success: true, sizeBytes: stats.size };
return { success: true, sizeBytes: stats.size };
}
if (provider?.query) {
const escapedPath = targetFile.replace(/'/g, "''");
await provider.query(`VACUUM INTO '${escapedPath}'`);
const stats = fs.statSync(targetFile);
return { success: true, sizeBytes: stats.size };
}
} catch (err) {
backupLog.warn(`Online SQLite backup method failed (${err.message}), falling back to direct copy`);
} }
// Direct file fallback if (provider?.query) {
const escapedPath = targetFile.replace(/'/g, "''");
await provider.query(`VACUUM INTO '${escapedPath}'`);
const stats = fs.statSync(targetFile);
return { success: true, sizeBytes: stats.size };
}
// Direct file copy when provider is offline / not connected
const { sqlitePath } = resolveBackupPaths(); const { sqlitePath } = resolveBackupPaths();
if (fs.existsSync(sqlitePath)) { if (fs.existsSync(sqlitePath)) {
fs.copyFileSync(sqlitePath, targetFile); fs.copyFileSync(sqlitePath, targetFile);

View File

@@ -180,19 +180,17 @@ export function extractLiveStatus(live) {
const isOffline = live.status === 'offline' || live.unreachable === true || errors.unreachable === true; const isOffline = live.status === 'offline' || live.unreachable === true || errors.unreachable === true;
const online = !isOffline; const online = !isOffline;
const open = Boolean(live.open ?? live.headOpen ?? errors.headOpen ?? false); const open = Boolean(live.open || errors.headOpen);
const pause = Boolean(live.pause ?? live.paused ?? errors.pause ?? false); const pause = Boolean(live.pause || errors.pause);
const hasOtherError = Boolean( const hasOtherError = Boolean(
live.error || live.error ||
errors.other || errors.other ||
errors.error ||
Object.entries(errors).some(([key, val]) => !['headOpen', 'pause', 'unreachable'].includes(key) && Boolean(val)) Object.entries(errors).some(([key, val]) => !['headOpen', 'pause', 'unreachable'].includes(key) && Boolean(val))
); );
const rawLabelLength = live.labelLength ?? live.labellength; const labelLength = live.labelLength != null && Number.isFinite(Number(live.labelLength))
const labelLength = rawLabelLength != null && Number.isFinite(Number(rawLabelLength)) ? Number(live.labelLength)
? Number(rawLabelLength)
: null; : null;
return { return {

View File

@@ -4,7 +4,7 @@ import { optimizeImage } from 'wasm-image-optimization';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.js'; import env from '../config/env.js';
import { getImageByHash } from '../db/pic.js'; import { getImageByHash } from '../db/pic.js';
import { prepareVfs, syncVfsToDb, extractFullHash, isReadOnlyDatabasePath } from './vfs.js'; import { prepareVfs, extractFullHash, isReadOnlyDatabasePath } from './vfs.js';
import { resolveOpenRouterKey, resolveImageModel } from './llm-key.js'; import { resolveOpenRouterKey, resolveImageModel } from './llm-key.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
@@ -482,11 +482,7 @@ export function createEditImageTool({ googleAuthId, model: fallbackModel }) {
fs.mkdirSync(path.dirname(absTarget), { recursive: true }); fs.mkdirSync(path.dirname(absTarget), { recursive: true });
fs.writeFileSync(absTarget, finalBuffer); fs.writeFileSync(absTarget, finalBuffer);
try {
await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (syncErr) {
console.warn('[edit_image] warning: failed to sync output image to modelvfs db:', syncErr.message);
}
const outFinalRel = path.relative(vfsRoot, absTarget).replace(/\\/g, '/'); const outFinalRel = path.relative(vfsRoot, absTarget).replace(/\\/g, '/');

View File

@@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.js'; import env from '../config/env.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { createVfs, prepareVfs, syncVfsToDb } from './vfs.js'; import { createVfs, prepareVfs } from './vfs.js';
import { runSandboxJob } from '../services/javascriptSandbox.js'; import { runSandboxJob } from '../services/javascriptSandbox.js';
const DEFAULT_TIMEOUT_MS = 20_000; const DEFAULT_TIMEOUT_MS = 20_000;
@@ -367,19 +367,14 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
const isApplet = hasApplet; const isApplet = hasApplet;
const applet = isApplet ? readApplet(vfsRoot, appletvfsfile) : null; const applet = isApplet ? readApplet(vfsRoot, appletvfsfile) : null;
const source = isApplet ? applet.source : code; const source = isApplet ? applet.source : code;
let report; const report = await runInWorker({
try { code: source,
report = await runInWorker({ timeoutMs: hardTimeout,
code: source, vfsRoot,
timeoutMs: hardTimeout, googleAuthId,
vfsRoot, appletInput: isApplet ? (applet_input ?? {}) : undefined,
googleAuthId, appletFile: isApplet ? appletvfsfile.trim() : undefined,
appletInput: isApplet ? (applet_input ?? {}) : undefined, });
appletFile: isApplet ? appletvfsfile.trim() : undefined,
});
} finally {
await syncVfsToDb({ googleAuthId, vfsRoot });
}
if (report.timedOut) { if (report.timedOut) {
throw new ToolError( throw new ToolError(

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath } from './vfs.js'; import { isReadOnlyDatabasePath } from './vfs.js';
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js'; import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
import { prepareVfs, syncVfsToDb } from './vfs.js'; import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { getStore } from '../db/store.js'; import { getStore } from '../db/store.js';
@@ -262,17 +262,7 @@ export function createPatchTool({ googleAuthId }) {
if (isSkill) touchedSkills.add(clean); if (isSkill) touchedSkills.add(clean);
} }
if (applied.length > 0) {
try {
await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (err) {
console.error('[patch] failed to sync VFS to db:', err.message);
throw new ToolError(
`Patch applied to disk but could not be persisted: ${err.message}. ` +
`Files changed: ${applied.join(', ')}.`
);
}
}
const result = { const result = {
success: true, success: true,

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { pdfToText, splitPdfPages, pdfToPng } from 'pdfconvert-wasm'; import { pdfToText, splitPdfPages, pdfToPng } from 'pdfconvert-wasm';
import { prepareVfs, syncVfsToDb } from './vfs.js'; import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
const IMAGE_FOLDER = 'images'; const IMAGE_FOLDER = 'images';
@@ -186,13 +186,7 @@ export function createPdfReadTool({ googleAuthId }) {
}); });
} }
if (savedImages.length > 0) {
try {
await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (err) {
console.warn('[pdf_read] failed to sync rendered images to VFS DB:', err?.message || err);
}
}
result.images = savedImages; result.images = savedImages;
if (!result.totalPages) result.totalPages = images.length; if (!result.totalPages) result.totalPages = images.length;

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { optimizeImage } from 'wasm-image-optimization'; import { optimizeImage } from 'wasm-image-optimization';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import * as dbCompanySetting from '../db/companysetting.js'; import * as dbCompanySetting from '../db/companysetting.js';
import { prepareVfs, syncVfsToDb } from './vfs.js'; import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
const SERPER_ENDPOINT = 'https://google.serper.dev/images'; const SERPER_ENDPOINT = 'https://google.serper.dev/images';
@@ -249,16 +249,7 @@ export function createSearchImagesTool({ googleAuthId }) {
}); });
} }
if (results.length > 0) {
try {
await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (err) {
// Non-fatal: the files are on disk and will be picked up by the next
// javascript_execute / patch sync. Losing the images because a DB write
// failed would be a worse outcome than a slightly stale store.
console.warn('[search_images] failed to persist candidates:', err.message);
}
}
const result = { const result = {
query: term, query: term,

View File

@@ -257,14 +257,6 @@ export async function prepareVfs({ googleAuthId }) {
return vfsRoot; return vfsRoot;
} }
/**
* Purely disk-based VFS; no database syncing needed. Kept as no-op for backward compatibility.
* @param {{ googleAuthId?: number, vfsRoot?: string }} [ctx]
*/
export async function syncVfsToDb() {
// Purely disk-based VFS; no database syncing.
}
// #endregion // #endregion
// #region Sandboxed VFS Factory // #region Sandboxed VFS Factory

View File

@@ -13,7 +13,7 @@ const skeleCache = new Map();
export async function formatAttachment(file) { export async function formatAttachment(file) {
if (!file || typeof file !== 'object') return file; if (!file || typeof file !== 'object') return file;
let skeledash = file.skeledash || file.skelehash || file.meta?.skeledash || file.meta?.skelehash || null; let skeledash = file.skeledash || file.meta?.skeledash || null;
if (!skeledash && file.hash && skeleCache.has(file.hash)) { if (!skeledash && file.hash && skeleCache.has(file.hash)) {
skeledash = skeleCache.get(file.hash); skeledash = skeleCache.get(file.hash);
@@ -36,7 +36,6 @@ export async function formatAttachment(file) {
return { return {
...rest, ...rest,
skeledash, skeledash,
skelehash: skeledash,
}; };
} }