u
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
import express from 'express';
|
||||
import crypto from 'node:crypto';
|
||||
import * as db from '../../db/printeragent.js';
|
||||
import * as agentprinterDb from '../../db/agentprinter.js';
|
||||
import { agentAuthMiddleware, extractAgentVersion } from '../middleware/agentAuth.js';
|
||||
import { emit } from '../sse.js';
|
||||
import { createLogger } from '../../util/logger.js';
|
||||
import {
|
||||
enrichAgentWithState,
|
||||
getAgentState,
|
||||
setAgentState,
|
||||
} from './stateStore.js';
|
||||
import {
|
||||
updateAgentPrinters,
|
||||
removeAgentPrinters,
|
||||
getCompanyPrinters,
|
||||
} from '../../services/companyPrinterService.js';
|
||||
|
||||
const log = createLogger('agent');
|
||||
|
||||
export const agentRouter = express.Router();
|
||||
|
||||
@@ -233,6 +247,15 @@ agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.flushHeaders?.();
|
||||
|
||||
const agentVersion = version || req.agent?.agentversion;
|
||||
const versionSuffix = agentVersion ? ` v${agentVersion}` : '';
|
||||
const agentName = req.agent?.name || 'unnamed';
|
||||
log.info(`Agent connected: id=${req.agent.id} ("${agentName}")${versionSuffix}`);
|
||||
|
||||
if (!getAgentState(req.agent.id)) {
|
||||
setAgentState(req.agent.id, 'online');
|
||||
}
|
||||
|
||||
res.write(`event: connected\ndata: ${JSON.stringify({ agentId: req.agent.id, name: req.agent.name })}\n\n`);
|
||||
|
||||
const keepAlive = setInterval(() => {
|
||||
@@ -241,8 +264,17 @@ agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
|
||||
db.touchLastseen(req.agent.id).catch(() => {});
|
||||
}, 15000);
|
||||
|
||||
let closed = false;
|
||||
const cleanup = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
clearInterval(keepAlive);
|
||||
log.info(`Agent disconnected: id=${req.agent.id} ("${agentName}")`);
|
||||
setAgentState(req.agent.id, 'offline');
|
||||
if (req.agent?.companyId) {
|
||||
removeAgentPrinters(req.agent.companyId, req.agent.id);
|
||||
emit('printeragent', { companyId: req.agent.companyId });
|
||||
}
|
||||
};
|
||||
|
||||
req.on('close', cleanup);
|
||||
@@ -287,29 +319,79 @@ 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 stateStr = rawState == null ? null : (typeof rawState === 'string' ? rawState : JSON.stringify(rawState));
|
||||
|
||||
const updated = await db.updateState(req.agent.id, stateStr);
|
||||
setAgentState(req.agent.id, rawState);
|
||||
|
||||
const version = extractAgentVersion(req);
|
||||
if (version) {
|
||||
await db.touchLastseen(req.agent.id, version);
|
||||
} else {
|
||||
await db.touchLastseen(req.agent.id);
|
||||
}
|
||||
|
||||
if (updated.companyId) {
|
||||
emit('printeragent', { companyId: updated.companyId });
|
||||
const rawPrinters = Array.isArray(rawState?.printers)
|
||||
? rawState.printers
|
||||
: (Array.isArray(req.body?.printers) ? req.body.printers : null);
|
||||
|
||||
let anyAgentPrinterCreated = false;
|
||||
if (rawPrinters && rawPrinters.length > 0) {
|
||||
for (const p of rawPrinters) {
|
||||
try {
|
||||
const result = await agentprinterDb.findOrCreate(req.agent.id, {
|
||||
type: p.type || 'zpl',
|
||||
model: p.model || null,
|
||||
serial: p.serial || null,
|
||||
});
|
||||
if (result?.created) {
|
||||
anyAgentPrinterCreated = true;
|
||||
}
|
||||
if (result?.agentprinter) {
|
||||
p.agentprinterId = result.agentprinter.id;
|
||||
p.enabled = result.agentprinter.enabled;
|
||||
p.sort = result.agentprinter.sort;
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(`Failed to record agentprinter for agent ${req.agent.id}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rawPrinters && req.agent.companyId) {
|
||||
updateAgentPrinters(req.agent.companyId, req.agent.id, req.agent.name, rawPrinters);
|
||||
}
|
||||
|
||||
if (anyAgentPrinterCreated && req.agent.companyId) {
|
||||
emit('agentprinter', { companyId: req.agent.companyId });
|
||||
}
|
||||
|
||||
if (req.agent.companyId) {
|
||||
emit('printeragent', { companyId: req.agent.companyId });
|
||||
}
|
||||
|
||||
const agent = enrichAgentWithState({
|
||||
...req.agent,
|
||||
agentversion: version || req.agent.agentversion,
|
||||
lastseenAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
agent: updated,
|
||||
agent,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Agent post state failed:', err);
|
||||
log.error('Agent post state failed:', err);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
agentRouter.post('/state', agentAuthMiddleware, handleState);
|
||||
agentRouter.put('/state', agentAuthMiddleware, handleState);
|
||||
agentRouter.get('/state', agentAuthMiddleware, (req, res) => {
|
||||
const memoryState = getAgentState(req.agent.id);
|
||||
const companyPrinters = req.agent.companyId ? getCompanyPrinters(req.agent.companyId) : [];
|
||||
const agentPrinters = companyPrinters.filter((p) => p.agentId === req.agent.id);
|
||||
res.status(200).json({
|
||||
state: memoryState ?? null,
|
||||
printers: agentPrinters,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
75
src/api/agent/stateStore.js
Normal file
75
src/api/agent/stateStore.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* In-memory state store for connected printer agents.
|
||||
* Keeps transient agent telemetry out of the database.
|
||||
*/
|
||||
|
||||
/** @type {Map<number, any>} */
|
||||
const agentStates = new Map();
|
||||
|
||||
/**
|
||||
* Sets the current telemetry/state for an agent.
|
||||
* @param {number} agentId
|
||||
* @param {any} state
|
||||
*/
|
||||
export function setAgentState(agentId, state) {
|
||||
if (agentId == null) return;
|
||||
agentStates.set(Number(agentId), state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw in-memory state for an agent.
|
||||
* @param {number} agentId
|
||||
* @returns {any}
|
||||
*/
|
||||
export function getAgentState(agentId) {
|
||||
if (agentId == null) return undefined;
|
||||
return agentStates.get(Number(agentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a state value into a concise clientstate string suitable for display (e.g. 'online', 'offline').
|
||||
* @param {any} state
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function formatClientState(state) {
|
||||
if (state == null) return null;
|
||||
if (typeof state === 'string') return state;
|
||||
if (typeof state === 'object') {
|
||||
if (typeof state.status === 'string' && state.status.trim()) {
|
||||
return state.status.trim();
|
||||
}
|
||||
if (typeof state.state === 'string' && state.state.trim()) {
|
||||
return state.state.trim();
|
||||
}
|
||||
}
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes in-memory state for an agent (e.g. when deleted).
|
||||
* @param {number} agentId
|
||||
*/
|
||||
export function removeAgentState(agentId) {
|
||||
if (agentId == null) return;
|
||||
agentStates.delete(Number(agentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches a printeragent object with the in-memory state.
|
||||
* @template {object} T
|
||||
* @param {T} agent
|
||||
* @returns {T}
|
||||
*/
|
||||
export function enrichAgentWithState(agent) {
|
||||
if (!agent || typeof agent !== 'object') return agent;
|
||||
const memoryState = getAgentState(agent.id);
|
||||
if (memoryState !== undefined) {
|
||||
return {
|
||||
...agent,
|
||||
state: memoryState,
|
||||
clientstate: formatClientState(memoryState),
|
||||
};
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -1735,15 +1735,6 @@
|
||||
* pairedAt:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* clientserial:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* clientmodel:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* clientstate:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* agentversion:
|
||||
* type: string
|
||||
* nullable: true
|
||||
@@ -1756,6 +1747,33 @@
|
||||
* type: integer
|
||||
* description: Display order (ascending)
|
||||
* default: 0
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* default: true
|
||||
* Agentprinter:
|
||||
* type: object
|
||||
* required: [id, printeragentId, type, enabled, sort]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* printeragentId:
|
||||
* type: integer
|
||||
* type:
|
||||
* type: string
|
||||
* description: Printer type ("zpl", "esc/pos", etc.)
|
||||
* model:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* serial:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* default: true
|
||||
* sort:
|
||||
* type: integer
|
||||
* description: Display order (ascending)
|
||||
* default: 0
|
||||
* LabelObject:
|
||||
* type: object
|
||||
* required: [id, type, x, y]
|
||||
|
||||
@@ -44,6 +44,7 @@ export const openApiOptions = {
|
||||
{ name: 'Posterminal', description: 'Company-scoped POS terminals' },
|
||||
{ name: 'Printer', description: 'Company-scoped receipt and label printers' },
|
||||
{ name: 'Printeragent', description: 'Company-scoped printer agents (create, rename, delete)' },
|
||||
{ name: 'Agentprinter', description: 'Company-scoped agent printers (enable, sort, delete)' },
|
||||
{ name: 'Agent', description: 'Printer agent device endpoints (pairing, SSE events, state)' },
|
||||
{ name: 'Ledger', description: 'Append-only ledger transactions' },
|
||||
{ name: 'Stock', description: 'Manual stock additions and removals' },
|
||||
|
||||
410
src/api/routes/agentprinter.js
Normal file
410
src/api/routes/agentprinter.js
Normal file
@@ -0,0 +1,410 @@
|
||||
import * as db from '../../db/agentprinter.js';
|
||||
import { emit } from '../sse.js';
|
||||
|
||||
// #region Helpers
|
||||
|
||||
function requireSelectedCompany(req) {
|
||||
const companyId = req.user?.selectedCompanyId;
|
||||
if (companyId == null) {
|
||||
return { error: 'No company selected' };
|
||||
}
|
||||
return { companyId };
|
||||
}
|
||||
|
||||
function parsePositiveInt(value, field) {
|
||||
const id = Number(value);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return { error: `${field} is required` };
|
||||
}
|
||||
return { id };
|
||||
}
|
||||
|
||||
function agentprinterErrorStatus(error) {
|
||||
if (error.message === 'No company selected') return 400;
|
||||
if (error.message === 'Not authorized') return 403;
|
||||
if (error.message === 'Company not found') return 404;
|
||||
if (error.message === 'Printeragent not found') return 404;
|
||||
if (error.message === 'Agentprinter not found') return 404;
|
||||
if (error.message === 'Invalid agentprinter') return 400;
|
||||
if (error.message === 'Invalid agentprinter order') return 400;
|
||||
return 500;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* Registers agentprinter routes for the authenticated user's selected company.
|
||||
* @param {import('express').Application} app - Express app
|
||||
* @param {string} [basePath=''] - Optional URL prefix for all routes in this module
|
||||
*/
|
||||
export function register(app, basePath = '') {
|
||||
/**
|
||||
* @openapi
|
||||
* /agentprinter:
|
||||
* get:
|
||||
* tags: [Agentprinter]
|
||||
* summary: List agent printers
|
||||
* description: |
|
||||
* Lists agent printers for the authenticated user's selected company,
|
||||
* optionally filtered by printer agent ID.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: printeragentId
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Optional printer agent ID filter
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of agent printers
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* agentprinters:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Agentprinter'
|
||||
* 400:
|
||||
* description: No company selected
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region GET /agentprinter
|
||||
|
||||
app.get(`${basePath}/agentprinter`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const printeragentId = req.query?.printeragentId ? Number(req.query.printeragentId) : undefined;
|
||||
const agentprinters = await db.list(req.user.id, printeragentId);
|
||||
res.status(200).json({ agentprinters });
|
||||
} catch (error) {
|
||||
const status = agentprinterErrorStatus(error);
|
||||
if (status === 500) console.error('List agentprinters failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /agentprinter/order:
|
||||
* put:
|
||||
* tags: [Agentprinter]
|
||||
* summary: Reorder agent printers
|
||||
* description: |
|
||||
* Reorders agent printers for a specific printer agent within the selected company.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-signals: [agentprinter]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [printeragentId, agentprinterIds]
|
||||
* properties:
|
||||
* printeragentId:
|
||||
* type: integer
|
||||
* agentprinterIds:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Applied order
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* agentprinterIds:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* 400:
|
||||
* description: Invalid order or no company selected
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region PUT /agentprinter/order
|
||||
|
||||
app.put(`${basePath}/agentprinter/order`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const { printeragentId, agentprinterIds } = req.body ?? {};
|
||||
|
||||
try {
|
||||
const result = await db.order(req.user.id, { printeragentId, agentprinterIds });
|
||||
emit('agentprinter', { companyId: selected.companyId });
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
const status = agentprinterErrorStatus(error);
|
||||
if (status === 500) console.error('Order agentprinters failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /agentprinter/{id}:
|
||||
* get:
|
||||
* tags: [Agentprinter]
|
||||
* summary: Get agent printer
|
||||
* description: Retrieves a single agent printer by ID.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Agent printer details
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* agentprinter:
|
||||
* $ref: '#/components/schemas/Agentprinter'
|
||||
* 400:
|
||||
* description: Invalid ID or no company selected
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 404:
|
||||
* description: Agent printer not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region GET /agentprinter/:id
|
||||
|
||||
app.get(`${basePath}/agentprinter/:id`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsed = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsed.error) {
|
||||
return res.status(400).json({ error: parsed.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const agentprinter = await db.getById(req.user.id, parsed.id);
|
||||
if (!agentprinter) {
|
||||
return res.status(404).json({ error: 'Agentprinter not found' });
|
||||
}
|
||||
res.status(200).json({ agentprinter });
|
||||
} catch (error) {
|
||||
const status = agentprinterErrorStatus(error);
|
||||
if (status === 500) console.error('Get agentprinter failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /agentprinter/{id}:
|
||||
* patch:
|
||||
* tags: [Agentprinter]
|
||||
* summary: Update agent printer
|
||||
* description: |
|
||||
* Updates enabled and/or display order (sort) of an agent printer.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-signals: [agentprinter]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* sort:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Updated agent printer
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* agentprinter:
|
||||
* $ref: '#/components/schemas/Agentprinter'
|
||||
* 400:
|
||||
* description: Invalid input or no company selected
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 404:
|
||||
* description: Agent printer not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region PATCH /agentprinter/:id
|
||||
|
||||
app.patch(`${basePath}/agentprinter/:id`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsed = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsed.error) {
|
||||
return res.status(400).json({ error: parsed.error });
|
||||
}
|
||||
|
||||
const payload = { id: parsed.id };
|
||||
if (req.body?.enabled !== undefined) payload.enabled = Boolean(req.body.enabled);
|
||||
if (req.body?.sort !== undefined) {
|
||||
const sort = Number(req.body.sort);
|
||||
if (!Number.isInteger(sort)) {
|
||||
return res.status(400).json({ error: 'sort must be an integer' });
|
||||
}
|
||||
payload.sort = sort;
|
||||
}
|
||||
|
||||
if (payload.enabled === undefined && payload.sort === undefined) {
|
||||
return res.status(400).json({ error: 'At least one field (enabled, sort) is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const agentprinter = await db.update(req.user.id, payload);
|
||||
emit('agentprinter', { companyId: selected.companyId });
|
||||
res.status(200).json({ agentprinter });
|
||||
} catch (error) {
|
||||
const status = agentprinterErrorStatus(error);
|
||||
if (status === 500) console.error('Update agentprinter failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /agentprinter/{id}:
|
||||
* delete:
|
||||
* tags: [Agentprinter]
|
||||
* summary: Delete agent printer
|
||||
* description: Deletes an agent printer from the selected company.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-signals: [agentprinter]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Agent printer deleted
|
||||
* 400:
|
||||
* description: Invalid ID or no company selected
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 404:
|
||||
* description: Agent printer not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region DELETE /agentprinter/:id
|
||||
|
||||
app.delete(`${basePath}/agentprinter/:id`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsed = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsed.error) {
|
||||
return res.status(400).json({ error: parsed.error });
|
||||
}
|
||||
|
||||
try {
|
||||
await db.remove(req.user.id, parsed.id);
|
||||
emit('agentprinter', { companyId: selected.companyId });
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
const status = agentprinterErrorStatus(error);
|
||||
if (status === 500) console.error('Delete agentprinter failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { emit } from '../sse.js';
|
||||
import { initCompanyVfs } from '../../tools/vfs.js';
|
||||
import { executeCompanyImport } from '../../services/companyImport.js';
|
||||
import { executeCompanyExport } from '../../services/companyExport.js';
|
||||
import { clearCompanyPrinters } from '../../services/companyPrinterService.js';
|
||||
|
||||
// #region Helpers
|
||||
|
||||
@@ -969,6 +970,7 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
await db.deleteCompany(parsedId.companyId);
|
||||
clearCompanyPrinters(parsedId.companyId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error('Delete company failed:', error);
|
||||
|
||||
@@ -55,6 +55,8 @@ export function register(app, basePath = '') {
|
||||
* | `propertyname` | `{}` | `POST /propertyname`, `PUT /propertyname/order`, `PATCH /propertyname/{id}`, `DELETE /propertyname/{id}` |
|
||||
* | `labeltemplate` | `{}` | `POST /labeltemplate`, `PUT /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `DELETE /labeltemplate/{id}` |
|
||||
* | `printer` | `{}` | `POST /printer`, `PUT /printer/{id}`, `PATCH /printer/{id}`, `DELETE /printer/{id}` |
|
||||
* | `printeragent` | `{}` | `PUT /printeragent`, `PATCH /printeragent/{id}`, `DELETE /printeragent/{id}`, printer agent connect/disconnect/state |
|
||||
* | `agentprinter` | `{}` | `PATCH /agentprinter/{id}`, `PUT /agentprinter/order`, `DELETE /agentprinter/{id}`, new printer discovery |
|
||||
* | `warehouse` | `{}` | `POST /warehouse`, `PUT /warehouse/{id}`, `PATCH /warehouse/{id}`, `DELETE /warehouse/{id}` |
|
||||
* | `supplier` | `{}` | `POST /supplier`, `PATCH /supplier/{id}`, `DELETE /supplier/{id}` |
|
||||
* | `vfs` | `{}` | VFS uploads and filesystem mutations such as `POST /vfs/upload`, `POST /vfs/mkdir`, `POST /vfs/rename`, `PUT /vfs/{*path}`, `DELETE /vfs/{*path}` |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as db from '../../db/printer.js';
|
||||
import { emit } from '../sse.js';
|
||||
import { getCompanyPrinters, getCompanyPrinter } from '../../services/companyPrinterService.js';
|
||||
|
||||
// #region Helpers
|
||||
|
||||
@@ -90,7 +91,17 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
const printers = await db.list(req.user.id);
|
||||
res.status(200).json({ printers });
|
||||
const livePrinters = getCompanyPrinters(selected.companyId);
|
||||
const enriched = printers.map((p) => {
|
||||
const live = livePrinters.find((lp) =>
|
||||
(p.serial && lp.serial && String(p.serial).toLowerCase() === String(lp.serial).toLowerCase()) ||
|
||||
(p.address && lp.ip && String(p.address).toLowerCase() === String(lp.ip).toLowerCase()) ||
|
||||
(p.address && lp.address && String(p.address).toLowerCase() === String(lp.address).toLowerCase()) ||
|
||||
(p.name && lp.name && String(p.name).toLowerCase() === String(lp.name).toLowerCase())
|
||||
);
|
||||
return live ? { ...p, state: live } : p;
|
||||
});
|
||||
res.status(200).json({ printers: enriched });
|
||||
} catch (error) {
|
||||
const status = printerErrorStatus(error);
|
||||
if (status === 500) console.error('List printers failed:', error);
|
||||
@@ -100,6 +111,186 @@ export function register(app, basePath = '') {
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /printer/state:
|
||||
* get:
|
||||
* tags: [Printer]
|
||||
* summary: Get live printer states
|
||||
* description: |
|
||||
* Retrieves real-time operational status and telemetry for all discovered printers
|
||||
* reported by active printer agents for the authenticated user's selected company.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: serial
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Optional serial number filter
|
||||
* - in: query
|
||||
* name: agentId
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Optional agent ID filter
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Live printer states
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* printers:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* 400:
|
||||
* description: No company selected
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region GET /printer/state
|
||||
|
||||
app.get(`${basePath}/printer/state`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
let printers = getCompanyPrinters(selected.companyId);
|
||||
|
||||
if (req.query?.serial) {
|
||||
const serialFilter = String(req.query.serial).trim().toLowerCase();
|
||||
printers = printers.filter((p) => p.serial && String(p.serial).toLowerCase() === serialFilter);
|
||||
}
|
||||
|
||||
if (req.query?.agentId) {
|
||||
const aId = Number(req.query.agentId);
|
||||
if (Number.isInteger(aId)) {
|
||||
printers = printers.filter((p) => p.agentId === aId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ printers });
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /printer/{id}/state:
|
||||
* get:
|
||||
* tags: [Printer]
|
||||
* summary: Get live state for a printer
|
||||
* description: |
|
||||
* Retrieves live operational status for a single printer by configured printer ID,
|
||||
* serial number, or device identifier for the authenticated user's selected company.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Configured printer ID, serial number, or device identifier
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Live printer state
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* printer:
|
||||
* type: object
|
||||
* 400:
|
||||
* description: No company selected or invalid ID
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 404:
|
||||
* description: Printer not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// #region GET /printer/:id/state
|
||||
|
||||
const handlePrinterStateById = async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const identifier = req.params.id;
|
||||
if (!identifier) {
|
||||
return res.status(400).json({ error: 'id is required' });
|
||||
}
|
||||
|
||||
let livePrinter = getCompanyPrinter(selected.companyId, identifier);
|
||||
|
||||
const numericId = Number(identifier);
|
||||
if (!livePrinter && Number.isInteger(numericId) && numericId > 0) {
|
||||
try {
|
||||
const dbPrinter = await db.getById(req.user.id, numericId);
|
||||
if (dbPrinter) {
|
||||
const allLive = getCompanyPrinters(selected.companyId);
|
||||
livePrinter = allLive.find((lp) =>
|
||||
(dbPrinter.serial && lp.serial && String(dbPrinter.serial).toLowerCase() === String(lp.serial).toLowerCase()) ||
|
||||
(dbPrinter.address && lp.ip && String(dbPrinter.address).toLowerCase() === String(lp.ip).toLowerCase()) ||
|
||||
(dbPrinter.address && lp.address && String(dbPrinter.address).toLowerCase() === String(lp.address).toLowerCase()) ||
|
||||
(dbPrinter.name && lp.name && String(dbPrinter.name).toLowerCase() === String(lp.name).toLowerCase())
|
||||
) || null;
|
||||
|
||||
if (!livePrinter) {
|
||||
return res.status(200).json({
|
||||
printer: {
|
||||
id: dbPrinter.id,
|
||||
companyId: dbPrinter.company_id,
|
||||
serial: dbPrinter.serial,
|
||||
address: dbPrinter.address,
|
||||
model: dbPrinter.model,
|
||||
status: 'offline',
|
||||
online: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore DB error, proceed to 404
|
||||
}
|
||||
}
|
||||
|
||||
if (!livePrinter) {
|
||||
return res.status(404).json({ error: 'Printer not found' });
|
||||
}
|
||||
|
||||
res.status(200).json({ printer: livePrinter });
|
||||
};
|
||||
|
||||
app.get(`${basePath}/printer/state/:id`, handlePrinterStateById);
|
||||
app.get(`${basePath}/printer/:id/state`, handlePrinterStateById);
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /printer/order:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { randomInt } from 'node:crypto';
|
||||
import * as db from '../../db/printeragent.js';
|
||||
import { emit } from '../sse.js';
|
||||
import { enrichAgentWithState, removeAgentState } from '../agent/stateStore.js';
|
||||
|
||||
// #region Helpers
|
||||
|
||||
@@ -92,7 +94,7 @@ export function register(app, basePath = '') {
|
||||
}
|
||||
|
||||
try {
|
||||
const printeragents = await db.list(req.user.id);
|
||||
const printeragents = (await db.list(req.user.id)).map(enrichAgentWithState);
|
||||
res.status(200).json({ printeragents });
|
||||
} catch (error) {
|
||||
const status = printeragentErrorStatus(error);
|
||||
@@ -126,6 +128,8 @@ export function register(app, basePath = '') {
|
||||
* type: string
|
||||
* sort:
|
||||
* type: integer
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* default: 0
|
||||
* example:
|
||||
* name: "Bar printer"
|
||||
@@ -193,7 +197,8 @@ export function register(app, basePath = '') {
|
||||
pairingpin,
|
||||
sort,
|
||||
});
|
||||
res.status(200).json({ printeragent });
|
||||
emit('printeragent', { companyId: selected.companyId });
|
||||
res.status(200).json({ printeragent: enrichAgentWithState(printeragent) });
|
||||
} catch (error) {
|
||||
const status = printeragentErrorStatus(error);
|
||||
if (status === 500) console.error('Create printeragent failed:', error);
|
||||
@@ -229,6 +234,8 @@ export function register(app, basePath = '') {
|
||||
* type: string
|
||||
* sort:
|
||||
* type: integer
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Updated printer agent
|
||||
@@ -278,8 +285,8 @@ export function register(app, basePath = '') {
|
||||
}
|
||||
|
||||
const { name } = req.body ?? {};
|
||||
if (name === undefined && req.body?.sort === undefined) {
|
||||
return res.status(400).json({ error: 'name or sort is required' });
|
||||
if (name === undefined && req.body?.sort === undefined && req.body?.enabled === undefined) {
|
||||
return res.status(400).json({ error: 'name, sort, or enabled is required' });
|
||||
}
|
||||
|
||||
if (name !== undefined && (typeof name !== 'string' || !name.trim())) {
|
||||
@@ -297,10 +304,12 @@ export function register(app, basePath = '') {
|
||||
const payload = { id: parsed.id };
|
||||
if (name !== undefined) payload.name = name.trim();
|
||||
if (sort !== undefined) payload.sort = sort;
|
||||
if (req.body?.enabled !== undefined) payload.enabled = Boolean(req.body.enabled);
|
||||
|
||||
try {
|
||||
const printeragent = await db.update(req.user.id, payload);
|
||||
res.status(200).json({ printeragent });
|
||||
emit('printeragent', { companyId: selected.companyId });
|
||||
res.status(200).json({ printeragent: enrichAgentWithState(printeragent) });
|
||||
} catch (error) {
|
||||
const status = printeragentErrorStatus(error);
|
||||
if (status === 500) console.error('Update printeragent failed:', error);
|
||||
@@ -367,6 +376,8 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
await db.remove(req.user.id, parsed.id);
|
||||
removeAgentState(parsed.id);
|
||||
emit('printeragent', { companyId: selected.companyId });
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
const status = printeragentErrorStatus(error);
|
||||
|
||||
Reference in New Issue
Block a user