feat: implement backend label print agent system for remote label rendering and job management
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s

This commit is contained in:
2026-05-07 22:46:29 +02:00
parent 0c94e7b999
commit 80f862a0c0
15 changed files with 995 additions and 15 deletions
@@ -0,0 +1,164 @@
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThan, IsNull, Or } from 'typeorm';
import { LabelPrintJob } from '../database/entities/label-print-job.entity';
import { BarcodeTemplate } from '../database/entities/barcode-template.entity';
import { LabelRendererService } from './label-renderer.service';
function applyVars(template: string, vars: Record<string, string>): string {
return template.replace(/\{([^}]+)\}/g, (_, key) => vars[key] ?? '');
}
function lockExpiry(): Date {
const d = new Date();
d.setMinutes(d.getMinutes() - 5);
return d;
}
@Injectable()
export class LabelPrintAgentService {
private readonly logger = new Logger(LabelPrintAgentService.name);
constructor(
@InjectRepository(LabelPrintJob)
private readonly jobRepo: Repository<LabelPrintJob>,
@InjectRepository(BarcodeTemplate)
private readonly templateRepo: Repository<BarcodeTemplate>,
private readonly renderer: LabelRendererService,
) {}
async createJob(
templateId: number,
fieldValues: Record<string, string>,
): Promise<LabelPrintJob> {
const template = await this.templateRepo.findOne({ where: { Id: templateId } });
if (!template) throw new NotFoundException('Template nicht gefunden');
if (!template.LabelEnabled) throw new BadRequestException('Etikett-Druck für dieses Template nicht aktiviert');
// Variablen aufbauen
const vars: Record<string, string> = { ...fieldValues };
// Datum-Felder: year/month/day separat ablegen
for (const field of template.LabelInputFields ?? []) {
if (field.type === 'date' && fieldValues[field.name]) {
const raw = fieldValues[field.name]; // erwartet YYYY-MM-DD
const parts = raw.split('-');
if (parts.length === 3) {
vars[`${field.name}.year`] = parts[0];
vars[`${field.name}.month`] = parts[1];
vars[`${field.name}.day`] = parts[2];
}
}
}
// GET-URL aufrufen → {number}
if (template.LabelGetUrl) {
const url = applyVars(template.LabelGetUrl, vars);
try {
const res = await fetch(url);
const text = (await res.text()).trim();
vars['number'] = text;
} catch (err: any) {
this.logger.warn(`GET-URL fehlgeschlagen (${url}): ${err.message}`);
}
}
const job = this.jobRepo.create({
Status: 'pending',
LabelImageData: null,
LabelWidthMm: template.LabelWidthMm ?? 57,
LabelHeightMm: template.LabelHeightMm ?? 32,
BarcodeTemplateId: template.Id,
LabelVariables: vars,
LockedAt: null,
LockedByAgent: null,
});
return this.jobRepo.save(job);
}
async claimNextJob(agentId: string): Promise<LabelPrintJob | null> {
// Kandidat: pending und kein Lock oder Lock abgelaufen (> 5 Min)
const candidate = await this.jobRepo.findOne({
where: [
{ Status: 'pending', LockedAt: IsNull() },
{ Status: 'pending', LockedAt: LessThan(lockExpiry()) },
],
order: { CreatedAt: 'ASC' },
});
if (!candidate) return null;
// Lock setzen
candidate.LockedAt = new Date();
candidate.LockedByAgent = agentId;
await this.jobRepo.save(candidate);
// Lazy render
if (!candidate.LabelImageData) {
const template = await this.templateRepo.findOne({ where: { Id: candidate.BarcodeTemplateId ?? undefined } });
if (template?.LabelLayout?.length) {
try {
candidate.LabelImageData = await this.renderer.render(
template.LabelLayout,
candidate.LabelWidthMm,
candidate.LabelHeightMm,
candidate.LabelVariables ?? {},
);
await this.jobRepo.save(candidate);
} catch (err: any) {
this.logger.error(`Label-Rendering fehlgeschlagen für Job ${candidate.Id}: ${err.message}`);
}
}
}
return candidate;
}
async getJobImage(jobId: number): Promise<Buffer | null> {
const job = await this.jobRepo.findOne({ where: { Id: jobId } });
return job?.LabelImageData ?? null;
}
async markPrinted(jobId: number, agentId: string, printerName: string): Promise<void> {
const job = await this.jobRepo.findOne({ where: { Id: jobId } });
if (!job) throw new NotFoundException('Job nicht gefunden');
job.Status = 'printed';
job.PrintedAt = new Date();
job.PrintedByAgent = agentId;
job.PrinterName = printerName;
await this.jobRepo.save(job);
await this.callUrl('PRINTED', job);
}
async markError(jobId: number, agentId: string, printerName: string, errorMessage: string): Promise<void> {
const job = await this.jobRepo.findOne({ where: { Id: jobId } });
if (!job) throw new NotFoundException('Job nicht gefunden');
job.Status = 'error';
job.PrintedByAgent = agentId;
job.PrinterName = printerName;
job.ErrorMessage = errorMessage;
await this.jobRepo.save(job);
await this.callUrl('RELEASE', job);
}
private async callUrl(type: 'PRINTED' | 'RELEASE', job: LabelPrintJob): Promise<void> {
const template = job.BarcodeTemplateId
? await this.templateRepo.findOne({ where: { Id: job.BarcodeTemplateId } })
: null;
if (!template) return;
const urlTemplate = type === 'PRINTED' ? template.LabelPrintedUrl : template.LabelReleaseUrl;
if (!urlTemplate) return;
const url = applyVars(urlTemplate, job.LabelVariables ?? {});
try {
await fetch(url, { method: 'POST' });
} catch (err: any) {
this.logger.warn(`${type}-URL fehlgeschlagen (${url}): ${err.message}`);
}
}
}