6 Commits

Author SHA1 Message Date
s4luorth
bf3352d82c Nur Kuverts: kein Schriftstueck erzeugen, wenn kein Text hinterlegt ist
All checks were successful
Build & Deploy / deploy (push) Successful in 11s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-07 07:52:40 +02:00
s4luorth
832204c3d8 Formate: neue render_formats ohne Backend-Neustart aktiv (Miss-Nachladen entklemmt)
All checks were successful
Build & Deploy / deploy (push) Successful in 13s
signaledMisses-Sperre entfernt (blockierte das Nachladen, wenn die Drossel griff),
Nachlade-Drossel 30s -> 5s. Neue Formate greifen so binnen Sekunden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-07 07:22:15 +02:00
s4luorth
3dfcdcd3b5 Kuvert-Absender: groesser + prozentuale Groesse (envelope_sender_scale)
All checks were successful
Build & Deploy / deploy (push) Successful in 19s
Absender-Grundgroesse = Empfaenger-Grundschrift (war 0.62x = zu klein), zusaetzlich
ueber envelope_sender_scale (50-200%) einstellbar. Durchgereicht via Generierung + Vorschau.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 11:04:23 +02:00
s4luorth
bffb7a1085 Kuvert: Empfaenger weiter nach rechts (60% statt 52% Breite)
All checks were successful
Build & Deploy / deploy (push) Successful in 20s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:43:14 +02:00
s4luorth
13f8394512 Kuvert: Absender oben links, Empfaenger unten rechts sauber positioniert
All checks were successful
Build & Deploy / deploy (push) Successful in 20s
Positionierung ueber Hoehen-/Breiten-Anteile statt ueber den grossen Kuvert-Rand
(marginTop bis ~50% der Hoehe): Absender ~14% oben/6% links, Empfaenger ~65-80%
unten/rechts. Vorher klebten beide im mittleren Band.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:30:40 +02:00
s4luorth
42a0d2d8b1 Kuvertbeschriftung: Absender oben links + Empfaenger unten rechts (opt-in)
All checks were successful
Build & Deploy / deploy (push) Successful in 22s
generateEnvelopeSVG rendert bei gesetztem Absender den Absender klein oben links und
den Empfaenger unten rechts (sonst unveraendert). Absender wird ueber die Auftrags-/
Vorschau-Pipeline durchgereicht (metadata.envelopeSender, envelope_sender), fetchOrder
laedt das Feld, Kuvert wird bei Absender frisch gerendert statt 1:1 aus der Vorschau.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:01:07 +02:00
7 changed files with 56 additions and 19 deletions

View File

@@ -78,9 +78,10 @@ function buildDocuments(order, entries) {
// Briefnummer 1 gehört. Vorschau nummeriert identisch (siehe skrift-orders). // Briefnummer 1 gehört. Vorschau nummeriert identisch (siehe skrift-orders).
const nr = Number(entry.letter_number) || (i + 1); const nr = Number(entry.letter_number) || (i + 1);
if (!istDatei && !istKuvertOnly) { // Abweichender Text je Schriftstück hat Vorrang; sonst Standardtext.
// Abweichender Text je Schriftstück (#5) hat Vorrang; sonst Standardtext. const briefText = (entry.text && String(entry.text).trim()) ? entry.text : (order.text_template || '');
const briefText = (entry.text && String(entry.text).trim()) ? entry.text : (order.text_template || ''); // Kein Schriftstück erzeugen, wenn kein Text hinterlegt ist (z. B. „nur Kuverts drucken").
if (!istDatei && !istKuvertOnly && String(briefText).trim()) {
documents.push({ documents.push({
index: nr, index: nr,
type: 'letter', type: 'letter',
@@ -167,6 +168,9 @@ async function generateFromDirectus(req, res, next) {
previewSession: order.preview_session || null, previewSession: order.preview_session || null,
// Eingegebener Brieftext → als brieftext.txt im Ausgabeordner ablegen. // Eingegebener Brieftext → als brieftext.txt im Ausgabeordner ablegen.
rawText: order.text_template || null, rawText: order.text_template || null,
// Absender fürs Kuvert (manuelle Aufträge): oben links, Empfänger unten rechts.
envelopeSender: order.envelope_sender || null,
envelopeSenderScale: Number(order.envelope_sender_scale) || 1,
}, },
}); });

View File

@@ -311,8 +311,9 @@ async function runGeneration({ orderNumber, letters, metadata }) {
const filepath = isEnv ? path.join(envelopesDir, filename) : path.join(outputDir, filename); const filepath = isEnv ? path.join(envelopesDir, filename) : path.join(outputDir, filename);
// 1:1 aus der Vorschau übernehmen, wenn für dieses Dokument eine // 1:1 aus der Vorschau übernehmen, wenn für dieses Dokument eine
// Vorschau-Datei existiert. // Vorschau-Datei existiert. Ausnahme: Kuvert mit Absender – das wird frisch
if (previewSession) { // gerendert, damit der Absender (oben links) sicher drauf ist.
if (previewSession && !(isEnv && metadata.envelopeSender)) {
const cached = path.join(previewsRoot, previewSession, filename); const cached = path.join(previewsRoot, previewSession, filename);
try { try {
await fs.access(cached); await fs.access(cached);
@@ -329,7 +330,7 @@ async function runGeneration({ orderNumber, letters, metadata }) {
if (isEnv) { if (isEnv) {
svgResult = generateEnvelopeSVG( svgResult = generateEnvelopeSVG(
scriptalizedText, letter.format, letter.envelopeType, scriptalizedText, letter.format, letter.envelopeType,
{ font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale } { font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale, sender: metadata.envelopeSender || '', senderScale: metadata.envelopeSenderScale || 1 }
); );
} else { } else {
svgResult = generateLetterSVG( svgResult = generateLetterSVG(

View File

@@ -167,7 +167,7 @@ async function generateBatch(req, res, next) {
if (letter.type === "envelope") { if (letter.type === "envelope") {
const svgResult = generateEnvelopeSVG( const svgResult = generateEnvelopeSVG(
scriptalizedText, letter.format, letter.envelopeType, scriptalizedText, letter.format, letter.envelopeType,
{ font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale } { font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale, sender: letter.sender || '', senderScale: letter.senderScale || 1 }
); );
if (svgResult.overflow) hasOverflow = true; if (svgResult.overflow) hasOverflow = true;
const filename = `envelope_${pad}.svg`; const filename = `envelope_${pad}.svg`;

View File

@@ -57,7 +57,6 @@ function applyRenderFormats(rows) {
PAGE_FORMATS = pf; PAGE_FORMATS = pf;
FORMAT_LAYOUTS = fl; FORMAT_LAYOUTS = fl;
LINE_LIMITS = ll; LINE_LIMITS = ll;
signaledMisses.clear();
} }
// mm in Pixel umrechnen // mm in Pixel umrechnen
@@ -72,12 +71,12 @@ function mmToPx(mm) {
// stündliche Aktualisierung abzuwarten. Für DIESEN Aufruf greift noch der // stündliche Aktualisierung abzuwarten. Für DIESEN Aufruf greift noch der
// Default – der nächste Aufruf hat das Format. // Default – der nächste Aufruf hat das Format.
let missHandler = null; let missHandler = null;
const signaledMisses = new Set();
function setFormatMissHandler(fn) { missHandler = fn; } function setFormatMissHandler(fn) { missHandler = fn; }
function signalMiss(key) { function signalMiss(key) {
const k = String(key || '').toLowerCase(); const k = String(key || '').toLowerCase();
if (!missHandler || !k || signaledMisses.has(k)) return; if (!missHandler || !k) return;
signaledMisses.add(k); // Kein „schon gemeldet"-Merker mehr: sonst blieb ein Key hängen, wenn die
// Nachlade-Drossel gerade griff. Die Drossel im Handler verhindert Dauerfeuer.
try { missHandler(k); } catch { /* egal */ } try { missHandler(k); } catch { /* egal */ }
} }

View File

@@ -294,8 +294,10 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
startX = 0; // Dummy-Wert, wird für jede Zeile überschrieben startX = 0; // Dummy-Wert, wird für jede Zeile überschrieben
} else { } else {
// type === 'recipient' / 'free': Links, max. 60% der Umschlagsbreite // type === 'recipient' / 'free'. Ist ein Absender gesetzt, sitzt der
const maxWidthPx = Math.min(widthPx - marginLeftPx - 20, widthPx * 0.6); // Empfänger UNTEN RECHTS (klassisches Kuvert), sonst wie bisher oben links.
const hatAbsender = !!String(options.sender || '').trim();
const maxWidthPx = Math.min(widthPx - marginLeftPx - 20, widthPx * (hatAbsender ? 0.36 : 0.6));
lines = wrapTextToLinesByWidth({ lines = wrapTextToLinesByWidth({
text: String(text || ''), text: String(text || ''),
@@ -304,8 +306,16 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
fontSizePx, fontSizePx,
}); });
startX = marginLeftPx; if (hatAbsender) {
startY = marginTopPx + fontSizePx; // Empfänger unten rechts – bewusst über Höhen-/Breiten-Anteile, damit der
// große Kuvert-Rand (marginTop bis ~50 % der Höhe) nicht alles nach oben zieht.
startX = Math.round(widthPx * 0.60);
const untenBaseline = heightPx * 0.80; // unterste Zeile ~20 % über der Unterkante
startY = Math.round(untenBaseline - Math.max(0, lines.length - 1) * lineHeightPx);
} else {
startX = marginLeftPx;
startY = marginTopPx + fontSizePx;
}
} }
let paths = []; let paths = [];
@@ -347,6 +357,28 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
}); });
} }
// Absender oben links – nur wenn gesetzt und kein zentrierter Freitext.
const absender = String(options.sender || '').trim();
if (absender && type !== 'custom') {
// Eigene Absender-Größe (Prozent, 1 = 100%). Grundgröße = Empfänger-Grundschrift
// (früher fix 0.62 → zu klein). Über options.senderScale prozentual einstellbar.
const senderScale = Math.max(0.5, Math.min(2, Number(options.senderScale) || 1));
const absSize = FONT_SIZE_PX * senderScale;
const absLH = lineHeightPx * (absSize / fontSizePx);
const absLines = absender.split('\n').map((l) => l.replace(/\s+$/, ''));
const absPaths = layoutTextToPaths({
lines: absLines,
fontFileName: fontConfig.file,
fontSizePx: absSize,
// Echt oben links – unabhängig vom großen Kuvert-Rand.
startX: Math.round(widthPx * 0.06),
startY: Math.round(heightPx * 0.14),
lineHeightPx: absLH,
realisticHandwriting: options.realisticHandwriting ?? false,
});
paths = [...paths, ...absPaths];
}
const pathElements = paths const pathElements = paths
.map( .map(
(p) => `<path d="${escapeXml(p.d)}" (p) => `<path d="${escapeXml(p.d)}"

View File

@@ -27,10 +27,11 @@ async function refreshRenderFormats() {
} }
} }
// Unbekanntes Format angefragt → sofort nachladen (max. alle 30 s, damit ein // Unbekanntes Format angefragt → sofort nachladen (max. alle 5 s, damit ein
// dauerhaft unbekanntes Format kein Dauerfeuer auslöst). // dauerhaft unbekanntes Format kein Dauerfeuer auslöst). Neu in Directus angelegte
// Formate sind damit ohne Backend-Neustart binnen weniger Sekunden aktiv.
setFormatMissHandler((key) => { setFormatMissHandler((key) => {
if (Date.now() - lastFormatRefresh < 30 * 1000) return; if (Date.now() - lastFormatRefresh < 5 * 1000) return;
console.log(`[Formate] Unbekanntes Format "${key}" angefragt – lade Formate nach.`); console.log(`[Formate] Unbekanntes Format "${key}" angefragt – lade Formate nach.`);
refreshRenderFormats(); refreshRenderFormats();
}); });

View File

@@ -41,7 +41,7 @@ async function directusFetch(path, options = {}) {
async function fetchOrder(orderId) { async function fetchOrder(orderId) {
const fields = [ const fields = [
'id', 'order_number', 'customer_email', 'font', 'status', 'id', 'order_number', 'customer_email', 'font', 'status',
'needs_envelope', 'envelope_labeling', 'envelope_text', 'envelope_format', 'needs_envelope', 'envelope_labeling', 'envelope_text', 'envelope_format', 'envelope_sender', 'envelope_sender_scale',
'text_template', 'motif_mode', 'own_cards', 'entries_count', 'preview_session', 'text_template', 'motif_mode', 'own_cards', 'entries_count', 'preview_session',
'source_file', 'product.key', 'product.type', 'product.input_mode', 'format.key', 'source_file', 'product.key', 'product.type', 'product.input_mode', 'format.key',
'font_scale_letter', 'font_scale_envelope', 'signatures_manual', 'multi_page', 'realistic', 'font_scale_letter', 'font_scale_envelope', 'signatures_manual', 'multi_page', 'realistic',