From f77ddb6a4c7bae357fe0045275ec6858209b3631 Mon Sep 17 00:00:00 2001 From: Lucas Orth Date: Fri, 24 Jul 2026 09:05:09 +0200 Subject: [PATCH] Formate zentral aus Directus + Kuvert-/Agent-/Login-Fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Docker/directus/bootstrap/bootstrap.mjs | 83 ++++++++++++-- .../dist/index.js | 2 +- .../dist/index.js | 9 +- Docker/skrift-backend/src/lib/page-layout.js | 105 +++++++++--------- Docker/skrift-backend/src/server.js | 28 +++++ .../src/services/directus-service.js | 10 +- Produktion/skrift-agent/agent.py | 20 ++-- 7 files changed, 181 insertions(+), 76 deletions(-) diff --git a/Docker/directus/bootstrap/bootstrap.mjs b/Docker/directus/bootstrap/bootstrap.mjs index 89d3e6d..8f2b969 100644 --- a/Docker/directus/bootstrap/bootstrap.mjs +++ b/Docker/directus/bootstrap/bootstrap.mjs @@ -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). diff --git a/Docker/directus/extensions/directus-extension-skrift-orders/dist/index.js b/Docker/directus/extensions/directus-extension-skrift-orders/dist/index.js index 9d70cd8..1f6ce76 100644 --- a/Docker/directus/extensions/directus-extension-skrift-orders/dist/index.js +++ b/Docker/directus/extensions/directus-extension-skrift-orders/dist/index.js @@ -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) => { diff --git a/Docker/directus/extensions/directus-extension-skrift-produktion/dist/index.js b/Docker/directus/extensions/directus-extension-skrift-produktion/dist/index.js index 51157fb..b637741 100644 --- a/Docker/directus/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/Docker/directus/extensions/directus-extension-skrift-produktion/dist/index.js @@ -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; diff --git a/Docker/skrift-backend/src/lib/page-layout.js b/Docker/skrift-backend/src/lib/page-layout.js index ca19801..da2648c 100644 --- a/Docker/skrift-backend/src/lib/page-layout.js +++ b/Docker/skrift-backend/src/lib/page-layout.js @@ -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, }; diff --git a/Docker/skrift-backend/src/server.js b/Docker/skrift-backend/src/server.js index 3f8358b..8ca4bd9 100644 --- a/Docker/skrift-backend/src/server.js +++ b/Docker/skrift-backend/src/server.js @@ -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 diff --git a/Docker/skrift-backend/src/services/directus-service.js b/Docker/skrift-backend/src/services/directus-service.js index bd80b31..d1bf8d9 100644 --- a/Docker/skrift-backend/src/services/directus-service.js +++ b/Docker/skrift-backend/src/services/directus-service.js @@ -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 }; diff --git a/Produktion/skrift-agent/agent.py b/Produktion/skrift-agent/agent.py index e2bfde8..ecdd88f 100644 --- a/Produktion/skrift-agent/agent.py +++ b/Produktion/skrift-agent/agent.py @@ -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.")