4fb34cdab8
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.
93 lines
5.3 KiB
TypeScript
93 lines
5.3 KiB
TypeScript
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='')`);
|
||
}
|