Files
klarbild/src/lib/seed.ts
T
till 4fb34cdab8 feat: sheet templates, margin profiles and delivering the sheet
Seeds the four sheet setups Till actually prints (Kita set, small school set,
full passport sheet, two 10x7,5 on 10x15 paper). A multi-format template
applied to a single image clones it into every format, so one click yields the
whole set. Margin gets one-click borderless / 5 mm / 10 mm profiles, and the
finished PDF can be pushed to Picdrop, the NAS or any extra target.
2026-08-18 06:40:31 +00:00

93 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { one, query } from './db';
import { hashPassword } from './auth';
/** Legt beim Erststart Nutzer, Standard-Rezepte, Einstellungen und Modelle an. */
export async function seed(): Promise<void> {
// Einstellungen (eine Zeile)
await query(`INSERT INTO settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING`);
// Standard-Galerie vorbelegen (wie initial gebrieft), falls leer
await query(`UPDATE settings SET picdrop_default_gallery='POSTER LEA' WHERE id=1 AND picdrop_default_gallery IS NULL`);
// Modelle vorbelegen (freundliche Namen, keine rohen IDs in der UI)
const mCount = await one<{ n: string }>(`SELECT count(*)::text AS n FROM models`);
if (mCount && Number(mCount.n) === 0) {
await query(`INSERT INTO models (model_id, label, description, active, is_default, supports_alpha, sort) VALUES
('google/gemini-3.1-flash-image','Schnell','In wenigen Sekunden fertig. Für den Alltag.',true,true,true,0),
('google/gemini-3-pro-image','Beste Qualität','Dauert länger, trifft Schrift und Details genauer.',true,false,true,1)`);
}
// Open-Source-Option (FLUX.2, offene Gewichte) nachrüsten — auch für bestehende Installationen.
await query(`INSERT INTO models (model_id, label, description, active, is_default, supports_alpha, sort)
SELECT 'black-forest-labs/flux.2-flex','Open Source (FLUX.2)','Offene Gewichte, gute Qualität. Kein transparentes Freistellen.',true,false,false,2
WHERE NOT EXISTS (SELECT 1 FROM models WHERE model_id='black-forest-labs/flux.2-flex')`);
// Nutzer
const till = await one(`SELECT id FROM users WHERE username='till'`);
if (!till) {
await query(
`INSERT INTO users (username, role, display_name, password_hash) VALUES ($1,'admin','Till',$2)`,
['till', await hashPassword(process.env.SEED_TILL_PASSWORD || 'klarbild-till')]);
}
const lea = await one(`SELECT id FROM users WHERE username='lea'`);
if (!lea) {
await query(
`INSERT INTO users (username, role, display_name, password_hash) VALUES ($1,'user','Lea',$2)`,
['lea', await hashPassword(process.env.SEED_LEA_PASSWORD || 'klarbild-lea')]);
}
// Standard-Rezepte
const count = await one<{ n: string }>(`SELECT count(*)::text AS n FROM recipes`);
if (count && Number(count.n) === 0) {
const admin = await one<{ id: string }>(`SELECT id FROM users WHERE username='till'`);
const by = admin?.id ?? null;
const R = (name: string, tasks: string[], fmt: string, orient: string, extra: Record<string, unknown> = {}) =>
query(
`INSERT INTO recipes (name, tasks, output_format, orientation, crop_mode, dpi, delivery, is_default, created_by, contour_mm)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[name, JSON.stringify(tasks), fmt, orient, (extra.crop_mode as string) || 'crop', 300,
(extra.delivery as string) || 'library', !!extra.is_default, by, (extra.contour_mm as number) ?? null]);
await R('Nur bereinigen', ['clean'], 'keep', 'landscape', { is_default: true });
await R('Poster 30×40', ['clean', 'format'], '30x40', 'portrait');
await R('The Frame', ['clean', 'format'], 'theframe', 'landscape',
{ delivery: 'both', picdrop_gallery: 'TheFrame-Backgrounds' });
await R('Sticker 5 cm', ['clean', 'cutout', 'format', 'contour'], 'sticker5', 'landscape',
{ contour_mm: 3 });
}
// Druck-Vorlagen (Bogen ohne KI) — idempotent über den Namen, auch für bestehende Installationen.
{
const admin = await one<{ id: string }>(`SELECT id FROM users WHERE username='till'`);
const by = admin?.id ?? null;
const P = async (name: string, config: Record<string, unknown>) => {
const exists = await one(`SELECT id FROM print_presets WHERE name=$1`, [name]);
if (exists) return;
await query(`INSERT INTO print_presets (name, created_by, config) VALUES ($1,$2,$3)`,
[name, by, JSON.stringify(config)]);
};
const sheet = {
paperId: 'A4', paperCustom: '', paperLandscape: false, marginMm: 5,
autoGap: true, gapMm: 14, bleedMm: 0,
marks: 'corner', markLen: 4, markOff: 3, dpi: 300, ext: 'jpg', center: true, footer: true,
};
const F = (sizeId: string, count: number, landscape = false) =>
({ sizeId, customSize: '', landscape, count, allowRotate: true });
// Der klassische Kita-/Schulfoto-Satz aus einem Bild.
await P('Kita-Satz (A4)', { ...sheet, formats: [F('S13x18', 1), F('S9x13', 2), F('P35x45', 8)] });
// Nur die kleinen Abzüge, wenn der große schon gedruckt ist.
await P('Schulsatz klein (A4)', { ...sheet, formats: [F('S9x13', 4), F('K30x40', 8)] });
// Voller Passbildbogen — durchgehende Linien, weil alles gleich groß ist.
await P('Passbildbogen 35×45 (A4)', { ...sheet, marks: 'grid', autoGap: false, gapMm: 4, formats: [F('P35x45', 20)] });
// Zwei Bilder in Originalgröße auf ein Blatt 10×15-Fotopapier.
await P('2 × 10×7,5 auf Fotopapier 10×15', {
...sheet, paperId: 'F10x15', marginMm: 0, marks: 'grid', autoGap: false, gapMm: 0,
formats: [{ sizeId: 'custom', customSize: '10x7,5', landscape: false, count: 2, allowRotate: true }],
});
}
// The-Frame-Rezept auf die eigene Galerie verdrahten (auch für bestehende Installationen).
await query(
`UPDATE recipes SET picdrop_gallery='TheFrame-Backgrounds', delivery='both'
WHERE output_format='theframe' AND (picdrop_gallery IS NULL OR picdrop_gallery='')`);
}