Astro 7, and stop a print-only instance calling a route it has switched off
CI / check (push) Has been cancelled

The critical advisory in the dependency tree (GHSA-26w7-cxv4-gfx2, remote code
execution through AVIF image optimisation) has no fix inside Astro 5, so this
moves to Astro 7.3.2 with @astrojs/node 11 and current sharp. `npm audit` now
reports nothing. The only code the jump needed: `csp` graduated out of
`experimental`, and sharp's `Sharp` type is a named export now.

Also fixed, both found by testing with every module switched on rather than
only the print-only default:

- `src/pages/index.astro` passed `dict` to the studio island without ever
  defining it, so the studio page returned 500 on any instance with the AI
  module on. The print-only default redirects away from that page, which is
  why the earlier run never hit it.
- The print studio fetched `/api/delivery-targets` even with the delivery
  module off. The route answered 404 exactly as designed, but it left a red
  line in the console of every print-only instance, which reads like a fault.
  It now asks only when there is a module to ask, and hides the card otherwise.

Plus: the first-run box sizes itself to its content instead of miscounting
padding, and the README's counts are recounted rather than remembered.

Verified against a live instance on a fresh database, in both module sets:
sign-in, module gating, the German interface, an eight-up passport sheet and
its PDF. 124 tests, type check, build and secret scan all clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nh4A79bbqT9dcd2inzYUdg
This commit is contained in:
tillandClaude Opus 5 committed 2026-09-15 15:23:42 +00:00
1 parent 4ff9f8b7a0
commit 0798eb7438
13 files changed
+1681 -3890

No files matched your search

+11 -4
View File
@@ -110,7 +110,8 @@ function shrink(dataUrl: string, maxEdge = 700): Promise<{ url: string; w: numbe
/* ========================================================================== */
export default function PrintApp({ locale, dict }: { locale?: Locale; dict?: Dict | null }) {
export default function PrintApp({ locale, dict, canDeliver = false }:
{ locale?: Locale; dict?: Dict | null; canDeliver?: boolean }) {
const t = useT(dict);
const [cells, setCells] = useState<Cell[]>([]);
const [paperId, setPaperId] = useState('A4');
@@ -298,8 +299,13 @@ export default function PrintApp({ locale, dict }: { locale?: Locale; dict?: Dic
useEffect(() => {
loadPresets();
fetch('/api/delivery-targets').then((r) => r.json()).then((j) => setTargets(j.targets || [])).catch(() => {});
}, []);
// Only ask when the delivery module is on. Asking anyway would work — the
// route answers 404 by design — but it puts a red line in the console of
// every print-only instance, which reads like a fault and is not one.
if (canDeliver) {
fetch('/api/delivery-targets').then((r) => r.json()).then((j) => setTargets(j.targets || [])).catch(() => {});
}
}, [canDeliver]);
const loadPresets = () => fetch('/api/print/presets').then((r) => r.json())
.then((j) => setPresets(j.presets || [])).catch(() => {});
@@ -706,6 +712,7 @@ export default function PrintApp({ locale, dict }: { locale?: Locale; dict?: Dic
)}
</Group>
{canDeliver && (
<Group closed title={t('Where to?')} hint={deliverTo ? t('deliver as well') : t('download only')}>
<select className="select" value={deliverTo} onChange={(e) => setDeliverTo(e.target.value)}
aria-label={t('Additional target')}>
@@ -718,7 +725,7 @@ export default function PrintApp({ locale, dict }: { locale?: Locale; dict?: Dic
<input className="input" style={{ marginTop: 7 }} value={gallery} onChange={(e) => setGallery(e.target.value)}
placeholder={t('Folder on the target (empty = default)')} aria-label={t('Folder on the target')} />
)}
</Group>
</Group>)}
</div>
</div>
</Card>
+2 -2
View File
@@ -1,7 +1,7 @@
// Rendering the print sheet: crop (sharp) + PDF assembly (pdf-lib).
// Deliberately without AI — plain geometry and scaling, so that the result is
// predictable and repeatable.
import sharp from 'sharp';
import sharp, { type Sharp } from 'sharp';
import { PDFDocument, StandardFonts, rgb, degrees } from 'pdf-lib';
import { mmToPt, mmToPx } from './paper.ts';
import type { Line, Page, SheetSpec } from './printlayout.ts';
@@ -112,7 +112,7 @@ export async function renderCell(
const ext: 'jpg' | 'png' = opt.ext === 'png' || hasAlpha ? 'png' : 'jpg';
if (hasAlpha && ext === 'jpg') img = img.flatten({ background: bg });
const encode = (pipe: sharp.Sharp) => (ext === 'png'
const encode = (pipe: Sharp) => (ext === 'png'
? pipe.withMetadata({ density: dpi }).png({ compressionLevel: 9 })
: pipe.withMetadata({ density: dpi }).jpeg({ quality: 94, mozjpeg: true }));
+10 -7
View File
@@ -44,14 +44,17 @@ async function seedAdmin(): Promise<void> {
} else {
// Printed once, on the first boot only. There is no way to recover it later;
// the reset flow (module "mail") or a fresh volume is the way back.
const lines = [
'Klarbild is set up. Sign in with:',
` user ${username}`,
` password ${password}`,
'Shown once. Change it under Account after you sign in.',
];
const w = Math.max(...lines.map((l) => l.length)) + 2;
console.log(
'\n' +
' ┌────────────────────────────────────────────────────────────┐\n' +
' │ Klarbild is set up. Sign in with: │\n' +
` │ user ${username.padEnd(46)}│\n` +
` │ password ${password.padEnd(46)}│\n` +
' │ This is shown once. Change it under Account after login. │\n' +
' └────────────────────────────────────────────────────────────┘\n',
'\n ┌' + '─'.repeat(w) + '┐\n' +
lines.map((l) => ` │ ${l.padEnd(w - 1)}│`).join('\n') +
'\n └' + '─'.repeat(w) + '┘\n',
);
}
}
+1
View File
@@ -789,6 +789,7 @@
"Storage & cleanup": "Speicher & Aufräumen",
"Stored encrypted, takes precedence over the environment variable.": "Verschlüsselt gespeichert, hat Vorrang vor der Umgebungsvariable.",
"Studio": "Studio",
"Studio · Klarbild": "Studio · Klarbild",
"Subject only, background transparent.": "Nur das Motiv, Hintergrund transparent.",
"Switch back on": "Wieder freigeben",
"Switch off": "Abschalten",
+4 -1
View File
@@ -3,13 +3,16 @@ import Base from '../layouts/Base.astro';
import StudioApp from '../components/StudioApp.tsx';
import { query } from '../lib/db';
import { has } from '../lib/modules';
import { tFor, dictFor } from '../lib/i18n.server';
// Without the AI module there is no studio, and the print studio is the product.
if (!has('ai')) return Astro.redirect('/print', 302);
const locale = Astro.locals.locale ?? 'en';
const t = tFor(locale);
const dict = dictFor(locale);
const recipes = await query('SELECT * FROM recipes ORDER BY is_default DESC, name').catch(() => []);
---
<Base title="Studio · Klarbild">
<Base title={t('Studio · Klarbild')}>
<StudioApp client:load recipes={recipes} locale={locale} dict={dict} />
</Base>
+2 -1
View File
@@ -2,6 +2,7 @@
import Base from '../layouts/Base.astro';
import PrintApp from '../components/PrintApp.tsx';
import { tFor, dictFor } from '../lib/i18n.server';
import { has } from '../lib/modules';
const locale = Astro.locals.locale ?? 'en';
const t = tFor(locale);
const dict = dictFor(locale);
@@ -11,7 +12,7 @@ const dict = dictFor(locale);
<p class="page-lead">
{t('No AI: bring images to exact sizes and place several of them at 100% size with crop marks on one sheet — from the biometric passport photo through the school photo set to the 30 × 40 poster.')}
</p>
<PrintApp client:load locale={locale} dict={dict} />
<PrintApp client:load locale={locale} dict={dict} canDeliver={has('delivery') || has('mirror')} />
<style>
.page-title{font-family:var(--font-display);font-size:1.6rem;margin:6px 0 4px;letter-spacing:-.02em;}
.page-lead{color:var(--soft);font-size:13.5px;margin:0 0 16px;max-width:64ch;line-height:1.55;}