Compare commits
3 Commits
e6c15dd053
...
689f12ba69
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
689f12ba69 | ||
|
|
bf081f875d | ||
|
|
8f9c941699 |
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}` });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, '/');
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user