Compare commits

...

3 Commits

Author SHA1 Message Date
seb
689f12ba69 u 2026-11-01 05:57:35 +01:00
seb
bf081f875d u 2026-11-01 05:38:45 +01:00
seb
8f9c941699 u 2026-11-01 05:37:15 +01:00
32 changed files with 223 additions and 218 deletions

View File

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

View File

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

View File

@@ -2,11 +2,11 @@
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.
`net` takes precedence over `price` (legacy alias for net) and `gross`.
`tax` and `taxRate` both specify the tax rate percentage (e.g. 19 for 19%), not monetary tax.
`net` takes precedence over `gross`.
`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)`.
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.
@@ -23,11 +23,9 @@ Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `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[].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[].tax` | no | number | Alias for `taxRate` percentage. |
| `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request. |
```json

View File

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

View File

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

View File

@@ -106,7 +106,7 @@ function corsMiddleware(req, res, next) {
const reqHeaders = req.headers['access-control-request-headers'];
res.setHeader(
'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');
if (req.method === 'OPTIONS') {

View File

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

View File

@@ -17,11 +17,11 @@ export function register(app, basePath = '') {
* /article/{sku}/prices:
* put:
* tags: [Article]
* summary: Replace POS prices by SKU (net > gross; price=net, tax=taxRate %)
* summary: Replace POS prices by SKU (net > gross)
* description: |
* Replace-all prices for the company POS customer group.
* `net` takes precedence over `price` (legacy alias for net) and `gross`.
* `tax` and `taxRate` both specify the tax rate percentage (e.g. 19 for 19%), not monetary tax.
* `net` takes precedence over `gross`.
* `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)`.
* 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.
@@ -50,19 +50,13 @@ export function register(app, basePath = '') {
* properties:
* net:
* 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:
* type: number
* description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`.
* price:
* type: number
* description: Legacy alias for `net`.
* taxRate:
* type: number
* description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%).
* tax:
* type: number
* description: Alias for `taxRate` percentage.
* minmultiplier:
* type: number
* description: Defaults to 1; must be unique per request.

View File

@@ -14,6 +14,7 @@ import {
parseFullHash,
articleErrorStatus,
interpolateString,
interpolateObjects,
} from './shared.js';
/**
@@ -366,7 +367,8 @@ export function register(app, basePath = '') {
if (skuError) return res.status(400).json({ error: skuError });
const sku = parsedSku;
const { templateId, agentprinterId, quantity } = req.body ?? {};
const { templateId, quantity } = req.body ?? {};
const agentprinterId = req.body?.agentprinterId ?? req.body?.printerId;
if (templateId === undefined || templateId === null) {
return res.status(400).json({ error: 'templateId is required' });
@@ -402,7 +404,7 @@ export function register(app, basePath = '') {
if (!template || template.company_id !== companyId) {
return res.status(404).json({ error: 'Label template not found' });
}
let article;
try {
const result = await db.getBySku(req.user.id, sku);
@@ -433,16 +435,7 @@ export function register(app, basePath = '') {
? content
: [];
const interpolatedObjects = objects.map(obj => {
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 interpolatedObjects = interpolateObjects(objects, article);
const printPayload = await renderLabel({
width: template.width,
@@ -468,7 +461,7 @@ export function register(app, basePath = '') {
console.error('Print article label failed:', error);
res.status(500).json({ error: error.message });
// #endregion
// #endregion
}
});
}

View File

@@ -117,20 +117,66 @@ export function articleErrorStatus(error) {
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) {
if (typeof str !== 'string') return str;
const net = article.net_price ?? article.prices?.[0]?.net;
const taxRate = article.prices?.[0]?.taxRate ?? 0;
const gross = net != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null;
const ean = (article.eans && article.eans.length > 0) ? article.eans[0] : '';
const basePrice = Array.isArray(article?.prices) && article.prices.length > 0
? article.prices[0]
: null;
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
.replace(/\{\{(?:ARTICLE_NAME|article_name|name)\}\}/gi, article.name || '')
.replace(/\{\{SKU\}\}/gi, article.sku || '')
.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() : ''))
.replace(/\{\{(?:EAN|ean)\}\}/gi, ean);
.replace(/\{\{name\}\}/gi, article?.name || '')
.replace(/\{\{price\}\}/gi, formatPrice(gross));
}
export function interpolateObjects(objects, article) {
if (!Array.isArray(objects)) return [];
const ean = Array.isArray(article?.eans) && article.eans.length > 0 ? String(article.eans[0]).trim() : '';
const sku = String(article?.sku ?? '').trim();
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;
if (ean.length === 8) {
cloned.format = 'EAN8';
} else if (ean.length === 12 || ean.length === 13) {
cloned.format = 'EAN13';
}
} else if (sku) {
cloned.code = sku;
cloned.format = 'CODE128';
}
} else if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
}
return cloned;
});
}
// #endregion

View File

@@ -253,7 +253,7 @@ export function register(app, basePath = '') {
const parsed = parseSku(req.params.sku);
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({
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);
if (parsed.error) return res.status(400).json({ error: parsed.error });
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() : '');
const childSku = typeof req.body?.sku === 'string' ? req.body.sku.trim() : '';
if (!childSku) {
return res.status(400).json({ error: 'sku is required' });
}
@@ -354,7 +352,7 @@ export function register(app, basePath = '') {
const parsedChild = parseSku(req.params.childSku);
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 {
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 });
}
const propertiesInput = req.body?.properties ?? req.body;
const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) {
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
// MARK: handles POST /backup/:id/vfs/recover to restore a VFS file from a backup
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) {
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);
if (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;
if (req.query.includeChildren !== undefined) {
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 {
@@ -395,7 +393,7 @@ export function register(app, basePath = '') {
.map((a) => ({
...a,
childCount: Number(a.childCount ?? 0),
isParent: Boolean(a.isParent ?? a.is_parent),
isParent: Boolean(a.isParent),
smallThumbW: a.smallThumbW ?? null,
smallThumbH: a.smallThumbH ?? null,
smallThumbBinary: a.smallThumbBinary ?? null,

View File

@@ -153,7 +153,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error });
}
const propertiesInput = req.body?.properties ?? req.body;
const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) {
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 { emit } from '../sse.js';
import { applyZplCopies } from './zpljob.js';
import { interpolateString } from './article/shared.js';
import { interpolateString, interpolateObjects } from './article/shared.js';
// #region Helpers
@@ -96,10 +96,45 @@ export function register(app, basePath = '') {
try {
const templates = await db.list(req.user.id);
res.status(200).json({ labeltemplates: templates.map((t) => {
const { content, ...rest } = serializeTemplate(t);
return rest;
}) });
let article = null;
if (req.query?.sku) {
const skuStr = String(req.query.sku).trim();
const result = await articleDb.getBySku(req.user.id, skuStr);
if (result?.article) {
article = result.article;
}
}
const labeltemplates = await Promise.all(
templates.map(async (t) => {
const { content, ...rest } = serializeTemplate(t);
if (article) {
try {
const full = await db.getById(req.user.id, t.id);
const objects = Array.isArray(full?.content?.objects)
? full.content.objects
: Array.isArray(full?.content)
? full.content
: null;
if (objects && objects.length > 0) {
const interpolated = interpolateObjects(objects, article);
const thumbBuffer = await renderThumb({
width: t.width,
height: t.height,
objects: interpolated,
});
rest.thumb = thumbBuffer.toString('base64');
}
} catch (err) {
console.error(`Failed to render preview thumb for template ${t.id} with sku ${req.query.sku}:`, err);
}
}
return rest;
})
);
res.status(200).json({ labeltemplates });
} catch (error) {
const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('List label templates failed:', error);
@@ -173,7 +208,32 @@ export function register(app, basePath = '') {
if (!template) {
return res.status(404).json({ error: 'Label template not found' });
}
res.status(200).json({ labeltemplate: serializeTemplate(template) });
const serialized = serializeTemplate(template);
if (req.query?.sku) {
const skuStr = String(req.query.sku).trim();
const result = await articleDb.getBySku(req.user.id, skuStr);
if (result?.article) {
const objects = Array.isArray(serialized?.content?.objects)
? serialized.content.objects
: Array.isArray(serialized?.content)
? serialized.content
: null;
if (objects && objects.length > 0) {
try {
const interpolated = interpolateObjects(objects, result.article);
const thumbBuffer = await renderThumb({
width: template.width,
height: template.height,
objects: interpolated,
});
serialized.thumb = thumbBuffer.toString('base64');
} catch (err) {
console.error(`Failed to render preview thumb for template ${template.id} with sku ${req.query.sku}:`, err);
}
}
}
}
res.status(200).json({ labeltemplate: serialized });
} catch (error) {
const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('Get label template failed:', error);
@@ -590,16 +650,15 @@ export function register(app, basePath = '') {
// Resolve sample article for placeholder interpolation
let article = null;
if (req.body?.sku) {
try {
const result = await articleDb.getBySku(req.user.id, req.body.sku);
article = result?.article;
} catch {
// ignore
const skuStr = String(req.body.sku).trim();
const result = await articleDb.getBySku(req.user.id, skuStr);
if (!result?.article) {
return res.status(404).json({ error: 'Article not found' });
}
}
if (!article) {
article = result.article;
} else {
article = {
name: req.body?.name || template?.name || 'Musterartikel',
name: template?.name || 'Musterartikel',
sku: 'TEST-12345',
prices: [{ net: 19.99, taxRate: 19 }],
eans: ['4012345678901'],
@@ -625,16 +684,7 @@ export function register(app, basePath = '') {
objects = [];
}
const interpolatedObjects = objects.map((obj) => {
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 interpolatedObjects = interpolateObjects(objects, article);
const printPayload = await renderLabel({
width: width || 448,

View File

@@ -101,7 +101,7 @@ export function register(app, basePath = '') {
}
const manifestarticleId = Number(req.params.manifestarticleId);
const transactionType = req.query.transaction_type ?? req.query.transactionType;
const transactionType = req.query.transaction_type;
try {
const ledger = await db.listByManifestarticleId(
@@ -186,7 +186,7 @@ export function register(app, basePath = '') {
}
const sku = req.params.sku;
const transactionType = req.query.transaction_type ?? req.query.transactionType;
const transactionType = req.query.transaction_type;
try {
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 });
}
const rawManifestarticleId = req.query.manifestarticle_id ?? req.query.manifestarticleId;
const rawManifestarticleId = req.query.manifestarticle_id;
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())) {
return res.status(400).json({

View File

@@ -190,12 +190,8 @@ export function register(app, basePath = '') {
}
const pairingpin = String(randomInt(100000, 999999));
const rootCategoryId = req.body.rootCategoryId !== undefined
? req.body.rootCategoryId
: req.body.root_category_id;
const syncRootArticles = req.body.syncRootArticles !== undefined
? req.body.syncRootArticles
: req.body.sync_root_articles;
const rootCategoryId = req.body.rootCategoryId;
const syncRootArticles = req.body.syncRootArticles;
const sort = req.body.sort !== undefined ? Number(req.body.sort) : undefined;
if (sort !== undefined && !Number.isInteger(sort)) {
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();
}
if (req.body.rootCategoryId !== undefined || req.body.root_category_id !== undefined) {
const rawCat = req.body.rootCategoryId !== undefined ? req.body.rootCategoryId : req.body.root_category_id;
payload.rootCategoryId = rawCat != null ? Number(rawCat) : null;
if (req.body.rootCategoryId !== undefined) {
payload.rootCategoryId = req.body.rootCategoryId != null ? Number(req.body.rootCategoryId) : null;
}
if (req.body.syncRootArticles !== undefined || req.body.sync_root_articles !== undefined) {
const rawSync = req.body.syncRootArticles !== undefined ? req.body.syncRootArticles : req.body.sync_root_articles;
payload.syncRootArticles = Boolean(rawSync);
if (req.body.syncRootArticles !== undefined) {
payload.syncRootArticles = Boolean(req.body.syncRootArticles);
}
if (req.body.sort !== undefined) {

View File

@@ -221,7 +221,7 @@ export function register(app, basePath = '') {
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');
if (parsedApId.error) {
return res.status(400).json({ error: parsedApId.error });
@@ -302,7 +302,7 @@ export function register(app, basePath = '') {
}
try {
const rawApId = req.body?.agentprinterId ?? req.query?.agentprinterId;
const rawApId = req.body?.agentprinterId;
const agentprinterId = rawApId != null ? Number(rawApId) : undefined;
const allJobs = await printerjobDb.list(req.user.id, { agentprinterId });
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 });
}
const skus = parseSkus(req.query.skus ?? req.query.sku);
const skus = parseSkus(req.query.skus);
try {
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 });
}
const skus = parseSkus(req.body?.skus ?? req.body?.sku);
const skus = parseSkus(req.body?.skus);
try {
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) {
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);
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
@@ -787,8 +787,8 @@ export function register(app, basePath = '') {
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const rawOld = req.body?.oldPath || req.body?.from || req.body?.source;
const rawNew = req.body?.newPath || req.body?.to || req.body?.target;
const rawOld = req.body?.oldPath;
const rawNew = req.body?.newPath;
const parsedOld = parseVfsRef(rawOld);
if (parsedOld.error) {
return res.status(400).json({ error: `oldPath: ${parsedOld.error}` });

View File

@@ -87,7 +87,7 @@ export function buildStudioApp() {
// 5. Insert row
app.post('/api/tables/:table/row', async (req, res) => {
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);
} catch (err) {
res.status(400).json({ error: err.message });
@@ -175,7 +175,3 @@ export async function stopBackendMonitor() {
});
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);
}
try {
const provider = await getProvider();
if (provider?.db && typeof provider.db.backup === 'function') {
await provider.db.backup(targetFile);
const stats = fs.statSync(targetFile);
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`);
const provider = await getProvider();
if (provider?.db && typeof provider.db.backup === 'function') {
await provider.db.backup(targetFile);
const stats = fs.statSync(targetFile);
return { success: true, sizeBytes: stats.size };
}
// 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();
if (fs.existsSync(sqlitePath)) {
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 online = !isOffline;
const open = Boolean(live.open ?? live.headOpen ?? errors.headOpen ?? false);
const pause = Boolean(live.pause ?? live.paused ?? errors.pause ?? false);
const open = Boolean(live.open || errors.headOpen);
const pause = Boolean(live.pause || errors.pause);
const hasOtherError = Boolean(
live.error ||
errors.other ||
errors.error ||
Object.entries(errors).some(([key, val]) => !['headOpen', 'pause', 'unreachable'].includes(key) && Boolean(val))
);
const rawLabelLength = live.labelLength ?? live.labellength;
const labelLength = rawLabelLength != null && Number.isFinite(Number(rawLabelLength))
? Number(rawLabelLength)
const labelLength = live.labelLength != null && Number.isFinite(Number(live.labelLength))
? Number(live.labelLength)
: null;
return {

View File

@@ -4,7 +4,7 @@ import { optimizeImage } from 'wasm-image-optimization';
import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.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 { 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.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, '/');

View File

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

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath } from './vfs.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 { getStore } from '../db/store.js';
@@ -262,17 +262,7 @@ export function createPatchTool({ googleAuthId }) {
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 = {
success: true,

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter';
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';
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;
if (!result.totalPages) result.totalPages = images.length;

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { optimizeImage } from 'wasm-image-optimization';
import { defineTool, ToolError } from 'libllmorbiter';
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';
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 = {
query: term,

View File

@@ -257,14 +257,6 @@ export async function prepareVfs({ googleAuthId }) {
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
// #region Sandboxed VFS Factory

View File

@@ -13,7 +13,7 @@ const skeleCache = new Map();
export async function formatAttachment(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)) {
skeledash = skeleCache.get(file.hash);
@@ -36,7 +36,6 @@ export async function formatAttachment(file) {
return {
...rest,
skeledash,
skelehash: skeledash,
};
}