Formate zentral aus Directus + Kuvert-/Agent-/Login-Fixes
Formate zentralisiert (Phase 1)
- Neue Collection render_formats (Seitengröße, Ränder, Zeilenhöhe/-limit +
Plotter-Template) als einzige Quelle. Backend liest sie gecacht (Start +
stündlich, synchroner Zugriff → keine Latenz) mit eingebauten Defaults als
Fallback; manueller Trigger via POST /api/formats/refresh.
- Ermöglicht frei definierbare Formate ohne Codeänderung.
Kuvert-Templates
- Werte 1:1 aus dem alten Job-Manager übernommen (falsche/vertauschte Maße
korrigiert, per Upsert) und je Format die zweite Variante ergänzt
(individuelle Beschriftung). Druck-Popup schlägt bei freiem Text Variante 2 vor.
Produktions-Agent
- Datei-Endpunkt liefert {files} ohne data-Hülle → eigener get_raw statt des
data-auspackenden get; Dokumentauswahl positionsbasiert (robust gegen 0-/1-
basierte Dateinamen).
Bootstrap
- Ungültiger DIRECTUS_TOKEN wird erkannt und fällt auf ADMIN_EMAIL/PASSWORD
zurück, statt mit einem 401 abzubrechen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,10 +56,27 @@ async function api(method, path, body) {
|
||||
return data?.data ?? data;
|
||||
}
|
||||
|
||||
/** Prüft, ob der gesetzte Token wirklich gültig ist. */
|
||||
async function tokenGueltig() {
|
||||
try {
|
||||
const res = await fetch(DIRECTUS_URL + '/users/me?fields=id', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return res.ok;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (token) return;
|
||||
// Ein mitgegebener DIRECTUS_TOKEN (z. B. versehentlich aus der .env
|
||||
// übernommen) wird geprüft – sonst schlägt erst der nächste Aufruf mit einem
|
||||
// wenig hilfreichen 401 fehl.
|
||||
if (token) {
|
||||
if (await tokenGueltig()) return;
|
||||
console.warn(' DIRECTUS_TOKEN ist ungültig – versuche Login mit ADMIN_EMAIL/ADMIN_PASSWORD.');
|
||||
token = '';
|
||||
}
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
console.error('Weder DIRECTUS_TOKEN noch ADMIN_EMAIL/ADMIN_PASSWORD gesetzt.');
|
||||
console.error('DIRECTUS_TOKEN ungültig bzw. nicht gesetzt und ADMIN_EMAIL/ADMIN_PASSWORD fehlen.');
|
||||
process.exit(1);
|
||||
}
|
||||
const res = await fetch(DIRECTUS_URL + '/auth/login', {
|
||||
@@ -349,6 +366,25 @@ const collections = [
|
||||
f('rotation', 'integer', { interface: 'input', default: 0 }),
|
||||
] },
|
||||
|
||||
// Zentrale Rendering-Formate: Seitengröße, Ränder, Zeilenhöhe/-limit. Das
|
||||
// Backend liest diese (gecacht) statt hartkodierter Werte – so lassen sich
|
||||
// eigene Formate anlegen, ohne Code zu ändern (eine Quelle für App + Backend).
|
||||
{ collection: 'render_formats', meta: { icon: 'crop_free', note: 'Rendering-Formate (Seitengröße, Ränder, Zeilenlimit) – zentral fürs Backend.' }, fields: [
|
||||
f('key', 'string', { interface: 'input', unique: true, required: true, note: 'Technischer Schlüssel, z. B. a4, a6p, din_lang, oder ein eigener.' }),
|
||||
f('name', 'string', { interface: 'input', required: true }),
|
||||
dropdown('kind', ['brief', 'umschlag'], { default: 'brief' }),
|
||||
f('page_width_mm', 'float', { interface: 'input', required: true, note: 'Seitenbreite in mm.' }),
|
||||
f('page_height_mm', 'float', { interface: 'input', required: true, note: 'Seitenhöhe in mm.' }),
|
||||
f('margin_top_mm', 'float', { interface: 'input', default: 25 }),
|
||||
f('margin_left_mm', 'float', { interface: 'input', default: 20 }),
|
||||
f('margin_right_mm', 'float', { interface: 'input', note: 'Optional (v. a. Querformat).' }),
|
||||
f('line_height_factor', 'float', { interface: 'input', default: 1.35, note: 'Zeilenhöhe = Schriftgröße × Faktor.' }),
|
||||
f('line_limit', 'integer', { interface: 'input', note: 'Max. Zeilen. Leer = automatisch aus Höhe.' }),
|
||||
// Zugehöriges Plotter-Druck-Template (Platzierung an der Maschine).
|
||||
m2o('print_template', 'format_templates'),
|
||||
f('active', 'boolean', { interface: 'boolean', default: true }),
|
||||
] },
|
||||
|
||||
{ collection: 'jobs', meta: { icon: 'print', note: 'Produktions-Job (Druck-Warteschlange je Maschine).' }, fields: [
|
||||
m2o('order', 'orders', { onDelete: 'CASCADE' }),
|
||||
f('output_ref', 'string', { interface: 'input', note: 'Ausgabe-Ordner ohne Auftrag (z. B. Testdruck).' }),
|
||||
@@ -664,15 +700,38 @@ async function applySeed() {
|
||||
];
|
||||
for (const t of templates) await seedItem('email_templates', 'key', { enabled: true, ...t });
|
||||
|
||||
// Druck-Templates (aus der bisherigen config.json der Produktions-App).
|
||||
// Druck-Templates – Werte 1:1 aus der config.json des alten Job-Managers.
|
||||
// Je Format gibt es zwei Varianten (v1/v2); die zweite ist die zusätzliche
|
||||
// Platzierung (u. a. für individuelle Kuvert-Beschriftung). Werte werden per
|
||||
// Upsert gesetzt, damit falsche Altwerte beim Bootstrap korrigiert werden.
|
||||
const formatTemplates = [
|
||||
{ key: 'a4_single', name: 'A4 Brief – Einzeln', type: 'brief', width: 8.27, height: 11.69, xpos: 0.0, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v1', name: 'A6 Brief – Hochformat', type: 'brief', width: 4.1, height: 5.8, xpos: -0.3, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v2', name: 'A6 Brief – Querformat', type: 'brief', width: 4.1, height: 5.8, xpos: 4.0, ypos: -0.3, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_a_v1', name: 'Kuvert DIN Lang', type: 'umschlag', width: 4.49, height: 6.38, xpos: 4.2, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_b_v1', name: 'Kuvert C6', type: 'umschlag', width: 3.9, height: 8.27, xpos: 4.49, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'a4_single', name: 'A4 Brief', type: 'brief', width: 8.27, height: 11.69, xpos: 0.0, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v1', name: 'A6 Hochformat – Variante 1', type: 'brief', width: 4.1, height: 5.8, xpos: -0.3, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v2', name: 'A6 Hochformat – Variante 2', type: 'brief', width: 4.1, height: 5.8, xpos: 4.0, ypos: -0.3, scale: 1.0, rotation: 90 },
|
||||
{ key: 'a6l_v1', name: 'A6 Querformat – Variante 1', type: 'brief', width: 5.8, height: 4.1, xpos: -0.3, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6l_v2', name: 'A6 Querformat – Variante 2', type: 'brief', width: 5.8, height: 4.1, xpos: 4.0, ypos: -0.3, scale: 1.0, rotation: 90 },
|
||||
// DIN Lang – v1 = Empfängeradresse, v2 = individuelle Beschriftung.
|
||||
{ key: 'kuvert_a_v1', name: 'Kuvert DIN Lang – Variante 1', type: 'umschlag', width: 4.33, height: 8.27, xpos: 4.0, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_a_v2', name: 'Kuvert DIN Lang – Variante 2', type: 'umschlag', width: 3.9, height: 8.27, xpos: 4.49, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
// C6 – v1 = Empfängeradresse, v2 = individuelle Beschriftung.
|
||||
{ key: 'kuvert_b_v1', name: 'Kuvert C6 – Variante 1', type: 'umschlag', width: 4.49, height: 6.38, xpos: 4.2, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_b_v2', name: 'Kuvert C6 – Variante 2', type: 'umschlag', width: 4.33, height: 6.38, xpos: 4.0, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
];
|
||||
for (const t of formatTemplates) await seedItem('format_templates', 'key', t);
|
||||
for (const t of formatTemplates) {
|
||||
const vorhanden = await api('GET', `/items/format_templates?filter[key][_eq]=${encodeURIComponent(t.key)}&limit=1&fields=id`);
|
||||
if (vorhanden && vorhanden.length) await api('PATCH', `/items/format_templates/${vorhanden[0].id}`, t);
|
||||
else { await api('POST', '/items/format_templates', t); log('seed + format_templates.' + t.key); }
|
||||
}
|
||||
|
||||
// Rendering-Formate (zentral fürs Backend). Werte = bisherige Defaults.
|
||||
const renderFormats = [
|
||||
{ key: 'a4', name: 'A4', kind: 'brief', page_width_mm: 210, page_height_mm: 297, margin_top_mm: 25, margin_left_mm: 20, line_height_factor: 1.35, line_limit: 27, active: true },
|
||||
{ key: 'a6p', name: 'A6 Hochformat', kind: 'brief', page_width_mm: 105, page_height_mm: 148, margin_top_mm: 8, margin_left_mm: 8, line_height_factor: 1.3, line_limit: 14, active: true },
|
||||
{ key: 'a6l', name: 'A6 Querformat', kind: 'brief', page_width_mm: 148, page_height_mm: 105, margin_top_mm: 8, margin_left_mm: 8, margin_right_mm: 14, line_height_factor: 1.2, line_limit: 11, active: true },
|
||||
{ key: 'din_lang', name: 'DIN Lang Kuvert', kind: 'umschlag', page_width_mm: 220, page_height_mm: 110, margin_top_mm: 50, margin_left_mm: 10, line_height_factor: 1.2, active: true },
|
||||
{ key: 'c6', name: 'C6 Kuvert', kind: 'umschlag', page_width_mm: 162, page_height_mm: 114, margin_top_mm: 60, margin_left_mm: 10, line_height_factor: 1.2, active: true },
|
||||
];
|
||||
for (const rf of renderFormats) await seedItem('render_formats', 'key', rf);
|
||||
|
||||
// email_settings (Singleton) – Signatur nur setzen, wenn leer.
|
||||
const es = await api('GET', '/items/email_settings');
|
||||
@@ -702,7 +761,7 @@ async function applySeed() {
|
||||
const BIZ = ['products', 'formats', 'motifs', 'price_items', 'pricing_settings',
|
||||
'vouchers', 'voucher_redemptions', 'orders', 'order_entries', 'order_addons', 'jobs', 'status_history',
|
||||
'offer_requests', 'signatures', 'customer_discounts', 'customer_products',
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'test_prints'];
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'render_formats', 'test_prints'];
|
||||
// Kunde = Login-Konto: Aufträge hängen direkt am Nutzer.
|
||||
const OWN_ORDER = { customer: { _eq: '$CURRENT_USER' } };
|
||||
const OWN_VIA_ORDER = { order: { customer: { _eq: '$CURRENT_USER' } } };
|
||||
@@ -817,7 +876,7 @@ async function applyPermissions() {
|
||||
const svcPol = await ensurePolicy('Service', { app: false });
|
||||
await ensureAccess(svcRole.id, svcPol.id);
|
||||
for (const col of ['orders', 'order_entries', 'order_addons', 'jobs', 'products', 'formats',
|
||||
'motifs', 'price_items', 'machines', 'format_templates'])
|
||||
'motifs', 'price_items', 'machines', 'format_templates', 'render_formats'])
|
||||
await ensurePermission(svcPol.id, col, 'read');
|
||||
await ensurePermission(svcPol.id, 'orders', 'update',
|
||||
['status', 'artifact_path', 'payment_status', 'payment_ref', 'production_status', 'brief_gedruckt', 'kuvert_gedruckt']);
|
||||
@@ -888,7 +947,7 @@ async function organizeNav() {
|
||||
await patch(col, { group: null, hidden: false });
|
||||
// Unter „Einstellungen"
|
||||
for (const col of ['products', 'formats', 'motifs', 'price_items', 'pricing_settings', 'offer_requests',
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'test_prints'])
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'render_formats', 'test_prints'])
|
||||
await patch(col, { group: FOLDER, hidden: false });
|
||||
// Reine Detail-/Technik-Collections aus der Navigation nehmen (Signaturen und
|
||||
// Rabatte werden direkt am Kundenkonto gepflegt, nicht als eigener Menüpunkt).
|
||||
|
||||
@@ -630,7 +630,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
// ── Druckauftrag einreihen (manueller Trigger aus dem Admin) ───────────────
|
||||
// Legt pro Typ (Schriftstück / Umschlag) einen Job mit aufgelöstem Template
|
||||
// für die gewählte Maschine an. Kein Statuswechsel am Auftrag (bewusst).
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6_v2' };
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6l_v1' };
|
||||
const ENVELOPE_TPL = { dinlang: 'kuvert_a_v1', c6: 'kuvert_b_v1' };
|
||||
|
||||
router.post('/enqueue', async (req, res) => {
|
||||
|
||||
@@ -23,7 +23,9 @@ import { defineComponent, ref, reactive, computed, onMounted, h, Teleport, resol
|
||||
import { useApi } from '@directus/extensions-sdk';
|
||||
|
||||
// Auftragsformat → Standard-Template-Schlüssel (wie im Backend /enqueue).
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6_v2' };
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6l_v1' };
|
||||
// v1 = Empfängeradresse (Standardvorschlag). Für individuelle Beschriftung
|
||||
// wählt der Operator im Popup die Variante 2 (kuvert_a_v2 / kuvert_b_v2).
|
||||
const ENVELOPE_TPL = { dinlang: 'kuvert_a_v1', c6: 'kuvert_b_v1' };
|
||||
const INLAND = /^(de|deutschland|germany)$/i;
|
||||
|
||||
@@ -256,7 +258,10 @@ const Board = defineComponent({
|
||||
function openDruck(order) {
|
||||
const bt = tplByKey(LETTER_TPL[order.format?.key] || 'a4_single', 'brief');
|
||||
const wantsEnv = order.needs_envelope && order.envelope_labeling && order.envelope_labeling !== 'keine';
|
||||
const et = wantsEnv ? tplByKey(ENVELOPE_TPL[order.envelope_format] || 'kuvert_a_v1', 'umschlag') : null;
|
||||
// Bei individuellem Text die Variante 2 des Kuverts vorschlagen (v1 = Empfängeradresse).
|
||||
let envKey = ENVELOPE_TPL[order.envelope_format] || 'kuvert_a_v1';
|
||||
if (wantsEnv && order.envelope_labeling === 'freitext') envKey = envKey.replace('_v1', '_v2');
|
||||
const et = wantsEnv ? (tplByKey(envKey, 'umschlag') || tplByKey(ENVELOPE_TPL[order.envelope_format], 'umschlag')) : null;
|
||||
const n = Math.max(1, order.entries_count || 1);
|
||||
druck.order = order;
|
||||
druck.machine = aktiveMaschinen.value[0]?.id ?? null;
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
const PX_PER_MM = 3.78; // 96dpi
|
||||
const FONT_SIZE_PX = 26;
|
||||
|
||||
// Maximale Zeilenanzahl pro Format (für Textvalidierung)
|
||||
const LINE_LIMITS = {
|
||||
a4: 27,
|
||||
a6p: 14,
|
||||
a6l: 11,
|
||||
// ── Formate: Defaults (Fallback) + datengetriebener Cache ───────────────────
|
||||
// Die Werte kommen zentral aus Directus (Collection render_formats) und werden
|
||||
// im Speicher gehalten (stündlich / manuell aufgefrischt via applyRenderFormats).
|
||||
// Ist Directus nicht erreichbar oder ein Format nicht hinterlegt, greifen die
|
||||
// eingebauten Defaults – die Generierung läuft also immer.
|
||||
const DEFAULT_LINE_LIMITS = { a4: 27, a6p: 14, a6l: 11 };
|
||||
const DEFAULT_FORMAT_LAYOUTS = {
|
||||
a4: { marginTopMm: 25, marginLeftMm: 20, lineHeightFactor: 1.35 },
|
||||
a6p: { marginTopMm: 8, marginLeftMm: 8, lineHeightFactor: 1.3 },
|
||||
a6l: { marginTopMm: 8, marginLeftMm: 8, marginRightMm: 14, lineHeightFactor: 1.2 },
|
||||
din_lang: { marginTopMm: 50, marginLeftMm: 10, lineHeightFactor: 1.2 },
|
||||
c6: { marginTopMm: 60, marginLeftMm: 10, lineHeightFactor: 1.2 },
|
||||
};
|
||||
|
||||
// Layout Konfiguration pro Format + Orientation
|
||||
const FORMAT_LAYOUTS = {
|
||||
// A4 Hochformat
|
||||
a4: {
|
||||
marginTopMm: 25,
|
||||
marginLeftMm: 20,
|
||||
lineHeightFactor: 1.35,
|
||||
},
|
||||
|
||||
// A6 Hochformat
|
||||
a6p: {
|
||||
marginTopMm: 8,
|
||||
marginLeftMm: 8,
|
||||
lineHeightFactor: 1.3,
|
||||
},
|
||||
|
||||
// A6 Querformat (landscape)
|
||||
a6l: {
|
||||
marginTopMm: 8,
|
||||
marginLeftMm: 8,
|
||||
marginRightMm: 14,
|
||||
lineHeightFactor: 1.2,
|
||||
},
|
||||
|
||||
// DIN Lang Kuvert (Querformat)
|
||||
din_lang: {
|
||||
marginTopMm: 50,
|
||||
marginLeftMm: 10, // Links unten in Ecke
|
||||
lineHeightFactor: 1.2,
|
||||
},
|
||||
|
||||
// C6 Kuvert (Querformat)
|
||||
c6: {
|
||||
marginTopMm: 60,
|
||||
marginLeftMm: 10, // Links unten in Ecke
|
||||
lineHeightFactor: 1.2,
|
||||
},
|
||||
};
|
||||
|
||||
// Seiten-Dimensionen
|
||||
const PAGE_FORMATS = {
|
||||
const DEFAULT_PAGE_FORMATS = {
|
||||
a4: { widthMm: 210, heightMm: 297 },
|
||||
a6p: { widthMm: 105, heightMm: 148 },
|
||||
a6l: { widthMm: 148, heightMm: 105 }, // Querformat: getauscht
|
||||
a6l: { widthMm: 148, heightMm: 105 },
|
||||
din_lang: { widthMm: 220, heightMm: 110 },
|
||||
c6: { widthMm: 162, heightMm: 114 },
|
||||
};
|
||||
|
||||
// Live-Cache (wird von applyRenderFormats ersetzt). Start = Defaults.
|
||||
let LINE_LIMITS = { ...DEFAULT_LINE_LIMITS };
|
||||
let FORMAT_LAYOUTS = { ...DEFAULT_FORMAT_LAYOUTS };
|
||||
let PAGE_FORMATS = { ...DEFAULT_PAGE_FORMATS };
|
||||
|
||||
/**
|
||||
* Übernimmt Format-Definitionen aus Directus (render_formats) in den Cache.
|
||||
* Baut immer auf den Defaults auf, damit Teilangaben nichts kaputt machen.
|
||||
* rows: [{ key, page_width_mm, page_height_mm, margin_top_mm, margin_left_mm,
|
||||
* margin_right_mm, line_height_factor, line_limit }]
|
||||
*/
|
||||
function applyRenderFormats(rows) {
|
||||
const pf = { ...DEFAULT_PAGE_FORMATS };
|
||||
const fl = { ...DEFAULT_FORMAT_LAYOUTS };
|
||||
const ll = { ...DEFAULT_LINE_LIMITS };
|
||||
for (const r of Array.isArray(rows) ? rows : []) {
|
||||
const k = String(r.key || '').toLowerCase();
|
||||
if (!k) continue;
|
||||
if (r.page_width_mm && r.page_height_mm) {
|
||||
pf[k] = { widthMm: Number(r.page_width_mm), heightMm: Number(r.page_height_mm) };
|
||||
}
|
||||
const alt = fl[k] || {};
|
||||
const layout = {
|
||||
marginTopMm: Number(r.margin_top_mm ?? alt.marginTopMm ?? 25),
|
||||
marginLeftMm: Number(r.margin_left_mm ?? alt.marginLeftMm ?? 20),
|
||||
lineHeightFactor: Number(r.line_height_factor ?? alt.lineHeightFactor ?? 1.35),
|
||||
};
|
||||
const mr = r.margin_right_mm ?? alt.marginRightMm;
|
||||
if (mr != null && mr !== '') layout.marginRightMm = Number(mr);
|
||||
fl[k] = layout;
|
||||
if (r.line_limit != null && r.line_limit !== '') ll[k] = Number(r.line_limit);
|
||||
}
|
||||
PAGE_FORMATS = pf;
|
||||
FORMAT_LAYOUTS = fl;
|
||||
LINE_LIMITS = ll;
|
||||
}
|
||||
|
||||
// mm in Pixel umrechnen
|
||||
function mmToPx(mm) {
|
||||
return mm * PX_PER_MM;
|
||||
@@ -113,12 +116,10 @@ function getLineLimit(format) {
|
||||
module.exports = {
|
||||
PX_PER_MM,
|
||||
FONT_SIZE_PX,
|
||||
FORMAT_LAYOUTS,
|
||||
PAGE_FORMATS,
|
||||
LINE_LIMITS,
|
||||
mmToPx,
|
||||
getLayoutForFormat,
|
||||
getPageDimensions,
|
||||
getLineLimit,
|
||||
escapeXml
|
||||
escapeXml,
|
||||
applyRenderFormats,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,21 @@ const healthRoutes = require('./api/routes/health-routes');
|
||||
const errorHandler = require('./api/middleware/error-handler');
|
||||
const requestLogger = require('./api/middleware/request-logger');
|
||||
|
||||
const { applyRenderFormats } = require('./lib/page-layout');
|
||||
const { fetchRenderFormats } = require('./services/directus-service');
|
||||
|
||||
// Zentrale Format-Definitionen aus Directus in den Cache holen. Läuft beim Start
|
||||
// und stündlich; Fehler sind unkritisch (die Defaults im page-layout greifen).
|
||||
async function refreshRenderFormats() {
|
||||
try {
|
||||
const rows = await fetchRenderFormats();
|
||||
applyRenderFormats(rows);
|
||||
console.log(`[Formate] ${Array.isArray(rows) ? rows.length : 0} Format(e) aus Directus geladen.`);
|
||||
} catch (e) {
|
||||
console.warn(`[Formate] Konnte Directus-Formate nicht laden (nutze Defaults): ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
@@ -22,6 +37,16 @@ app.use('/health', healthRoutes);
|
||||
app.use('/api/preview', previewRoutes);
|
||||
app.use('/api/order', orderRoutes);
|
||||
|
||||
// Format-Cache manuell auffrischen (Token-geschützt) – für „sofort übernehmen".
|
||||
app.post('/api/formats/refresh', async (req, res) => {
|
||||
const token = config.auth && config.auth.apiToken;
|
||||
if (token && req.headers['x-api-token'] !== token) {
|
||||
return res.status(401).json({ error: 'API token required' });
|
||||
}
|
||||
await refreshRenderFormats();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use(errorHandler);
|
||||
|
||||
@@ -39,6 +64,9 @@ app.listen(config.port, () => {
|
||||
console.log('════════════════════════════════════════════════════════════');
|
||||
console.log(`Server running at http://localhost:${config.port}`);
|
||||
console.log('════════════════════════════════════════════════════════════\n');
|
||||
// Formate initial laden und stündlich auffrischen (zentral aus Directus).
|
||||
refreshRenderFormats();
|
||||
setInterval(refreshRenderFormats, 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
|
||||
@@ -77,4 +77,12 @@ async function addStatusHistory(orderId, status, note) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { fetchOrder, updateOrder, addStatusHistory };
|
||||
/** Zentrale Format-Definitionen (Seitengröße, Ränder, Zeilenlimit) aus Directus. */
|
||||
async function fetchRenderFormats() {
|
||||
return directusFetch(
|
||||
'/items/render_formats?filter[active][_eq]=true&limit=-1' +
|
||||
'&fields=key,page_width_mm,page_height_mm,margin_top_mm,margin_left_mm,margin_right_mm,line_height_factor,line_limit',
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { fetchOrder, updateOrder, addStatusHistory, fetchRenderFormats };
|
||||
|
||||
@@ -46,10 +46,17 @@ class Directus:
|
||||
self.s.headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
def get(self, path, **params):
|
||||
"""Für /items/… – Directus verpackt die Nutzdaten in `data`."""
|
||||
r = self.s.get(self.base + path, params=params, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json().get("data")
|
||||
|
||||
def get_raw(self, path, **params):
|
||||
"""Für Custom-Endpunkte (z. B. /skrift-orders/files) ohne `data`-Hülle."""
|
||||
r = self.s.get(self.base + path, params=params, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def patch(self, path, body):
|
||||
r = self.s.patch(self.base + path, json=body, timeout=30)
|
||||
r.raise_for_status()
|
||||
@@ -152,12 +159,6 @@ def parse_nummern(spec):
|
||||
return treffer or None
|
||||
|
||||
|
||||
def datei_nummer(name):
|
||||
"""Zahl aus einem Dateinamen ziehen (letter_003.svg → 3)."""
|
||||
ziffern = "".join(ch for ch in os.path.splitext(os.path.basename(name))[0] if ch.isdigit())
|
||||
return int(ziffern) if ziffern else None
|
||||
|
||||
|
||||
def main():
|
||||
cfg = lade_config()
|
||||
dx = Directus(cfg["directus_url"], cfg["directus_token"])
|
||||
@@ -248,15 +249,18 @@ def _druck(dx, plotter, job, KIND):
|
||||
try:
|
||||
if not order_nr:
|
||||
raise RuntimeError("Auftragsnummer fehlt am Job.")
|
||||
liste = dx.get(f"/skrift-orders/files/{order_nr}") or {}
|
||||
liste = dx.get_raw(f"/skrift-orders/files/{order_nr}") or {}
|
||||
gewuenscht = KIND.get(typ, "schriftstueck")
|
||||
passende = sorted(
|
||||
[f for f in (liste.get("files") or []) if f.get("kind") == gewuenscht],
|
||||
key=lambda f: f.get("name", ""),
|
||||
)
|
||||
# Nur bestimmte Briefnummern drucken, falls im Job angegeben.
|
||||
# Positionsbasiert (1 = erstes Dokument), NICHT über die Zahl im
|
||||
# Dateinamen: Alt-Aufträge sind 0-basiert (letter_000.svg), neue
|
||||
# 1-basiert (letter_001.svg). Die Position stimmt in beiden Fällen.
|
||||
if nummern is not None:
|
||||
passende = [f for f in passende if datei_nummer(f.get("name")) in nummern]
|
||||
passende = [f for i, f in enumerate(passende, start=1) if i in nummern]
|
||||
if not passende:
|
||||
raise RuntimeError(f"Keine Dateien vom Typ '{gewuenscht}' für {order_nr} gefunden.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user