feat(print): sync capture diagnostics after desktop login
This commit is contained in:
parent
ec897c96b6
commit
15007a4893
33
apps/api/src/printTelemetry.ts
Normal file
33
apps/api/src/printTelemetry.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export const streamFormats = ['zpl', 'pdf', 'postscript', 'pjl', 'pcl', 'zip', 'unknown'] as const
|
||||
export const failureKinds = ['capture', 'forward', 'parser'] as const
|
||||
|
||||
export type PrintTelemetry = {
|
||||
id: string
|
||||
source: 'raw-proxy'
|
||||
streamFormat: (typeof streamFormats)[number]
|
||||
byteCount: number
|
||||
barcodeCount: number
|
||||
forwarded: boolean
|
||||
failureKind: (typeof failureKinds)[number] | null
|
||||
appVersion: string
|
||||
capturedAt: string
|
||||
}
|
||||
|
||||
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
export function parsePrintTelemetry(payload: unknown): PrintTelemetry | null {
|
||||
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) return null
|
||||
const value = payload as Record<string, unknown>
|
||||
const allowed = ['id', 'source', 'streamFormat', 'byteCount', 'barcodeCount', 'forwarded', 'failureKind', 'appVersion', 'capturedAt']
|
||||
if (Object.keys(value).some((key) => !allowed.includes(key))) return null
|
||||
if (typeof value.id !== 'string' || !uuid.test(value.id)) return null
|
||||
if (value.source !== 'raw-proxy') return null
|
||||
if (!streamFormats.includes(value.streamFormat as PrintTelemetry['streamFormat'])) return null
|
||||
if (!Number.isSafeInteger(value.byteCount) || (value.byteCount as number) < 0) return null
|
||||
if (!Number.isSafeInteger(value.barcodeCount) || (value.barcodeCount as number) < 0) return null
|
||||
if (typeof value.forwarded !== 'boolean') return null
|
||||
if (value.failureKind !== null && !failureKinds.includes(value.failureKind as (typeof failureKinds)[number])) return null
|
||||
if (typeof value.appVersion !== 'string' || value.appVersion.length > 40) return null
|
||||
if (typeof value.capturedAt !== 'string' || !Number.isFinite(Date.parse(value.capturedAt))) return null
|
||||
return value as PrintTelemetry
|
||||
}
|
||||
@ -278,6 +278,20 @@ app.post('/api/v1/sync', requireAccess('operator'), async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/v1/print-captures', requireAccess('operator'), async (req, res) => {
|
||||
const requested = Number(req.query.limit ?? 50)
|
||||
const limit = Number.isFinite(requested) ? Math.max(1, Math.min(200, Math.trunc(requested))) : 50
|
||||
const result = await pool.query(`
|
||||
SELECT id, device_id AS "deviceId", source, stream_format AS "streamFormat",
|
||||
byte_count AS "byteCount", barcode_count AS "barcodeCount", forwarded,
|
||||
failure_kind AS "failureKind", app_version AS "appVersion",
|
||||
captured_at AS "capturedAt", received_at AS "receivedAt"
|
||||
FROM print_capture_diagnostics WHERE tenant_id = $1
|
||||
ORDER BY captured_at DESC LIMIT $2
|
||||
`, [config.tenantId, limit])
|
||||
res.json({ captures: result.rows })
|
||||
})
|
||||
|
||||
app.get('/api/v1/scans', requireAccess('supervisor'), async (req, res) => {
|
||||
const limit = Math.max(1, Math.min(500, Number(req.query.limit ?? 100)))
|
||||
const result = await pool.query(`
|
||||
|
||||
@ -3,6 +3,25 @@ import type { PoolClient } from 'pg'
|
||||
import type { OutboxEvent, SyncAcknowledgement } from '@fieldlens/contracts'
|
||||
import type { AuthIdentity } from './auth.js'
|
||||
import type { DeviceContext } from './context.js'
|
||||
import { parsePrintTelemetry } from './printTelemetry.js'
|
||||
|
||||
async function insertPrintTelemetry(client: PoolClient, event: OutboxEvent, context: DeviceContext, identity: AuthIdentity) {
|
||||
const payload = parsePrintTelemetry(event.payload)
|
||||
if (!payload || payload.id !== event.aggregateId) throw new Error('invalid_print_capture_diagnostic')
|
||||
await client.query(`
|
||||
INSERT INTO print_capture_diagnostics (
|
||||
id, tenant_id, site_id, station_id, device_id, operator_subject,
|
||||
source, stream_format, byte_count, barcode_count, forwarded,
|
||||
failure_kind, app_version, captured_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [
|
||||
payload.id, context.tenantId, context.siteId, context.stationId, context.deviceId,
|
||||
identity.subject, payload.source, payload.streamFormat, payload.byteCount,
|
||||
payload.barcodeCount, payload.forwarded, payload.failureKind,
|
||||
payload.appVersion, payload.capturedAt,
|
||||
])
|
||||
}
|
||||
|
||||
async function insertScan(client: PoolClient, event: OutboxEvent, context: DeviceContext, identity: AuthIdentity) {
|
||||
const payload = event.payload as Record<string, unknown>
|
||||
@ -123,6 +142,9 @@ async function completePallet(client: PoolClient, event: OutboxEvent) {
|
||||
|
||||
async function applyDomainEvent(client: PoolClient, event: OutboxEvent, context: DeviceContext, identity: AuthIdentity) {
|
||||
switch (event.eventType) {
|
||||
case 'print.capture.diagnostic':
|
||||
await insertPrintTelemetry(client, event, context, identity)
|
||||
return
|
||||
case 'scan.captured':
|
||||
await insertScan(client, event, context, identity)
|
||||
return
|
||||
|
||||
20
migrations/005_print_capture_diagnostics.sql
Normal file
20
migrations/005_print_capture_diagnostics.sql
Normal file
@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS print_capture_diagnostics (
|
||||
id uuid PRIMARY KEY,
|
||||
tenant_id text NOT NULL REFERENCES tenants(id),
|
||||
site_id uuid NOT NULL REFERENCES sites(id),
|
||||
station_id uuid NOT NULL REFERENCES stations(id),
|
||||
device_id uuid NOT NULL REFERENCES devices(id),
|
||||
operator_subject text NOT NULL,
|
||||
source text NOT NULL,
|
||||
stream_format text NOT NULL,
|
||||
byte_count bigint NOT NULL CHECK (byte_count >= 0),
|
||||
barcode_count integer NOT NULL CHECK (barcode_count >= 0),
|
||||
forwarded boolean NOT NULL,
|
||||
failure_kind text,
|
||||
app_version text NOT NULL,
|
||||
captured_at timestamptz NOT NULL,
|
||||
received_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_print_capture_diagnostics_tenant_time
|
||||
ON print_capture_diagnostics(tenant_id, captured_at DESC);
|
||||
@ -56,6 +56,7 @@ export type OutboxEvent = {
|
||||
| 'pallet.session.started'
|
||||
| 'pallet.observation.captured'
|
||||
| 'pallet.session.completed'
|
||||
| 'print.capture.diagnostic'
|
||||
schemaVersion: SchemaVersion
|
||||
payload: unknown
|
||||
createdAt: string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user