Astro 7, and stop a print-only instance calling a route it has switched off
CI / check (push) Has been cancelled
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:
13 files changed
+1670
-3879
No files matched your search
+3
-1
@@ -11,7 +11,8 @@ private repository as a personal tool.
|
||||
default is the print studio alone — no image model, no object store, no bot
|
||||
token, no account anywhere. See `src/lib/modules.ts`.
|
||||
- **English interface**, with German as a full second language. The English
|
||||
sentence is the translation key; 1,038 strings.
|
||||
sentence is the translation key; 1,067 entries, covering every translatable
|
||||
string in the source.
|
||||
- **First-run setup.** No built-in accounts. The first boot creates one
|
||||
administrator from `ADMIN_USERNAME` / `ADMIN_PASSWORD`, or generates a password
|
||||
and prints it once to the log.
|
||||
@@ -23,6 +24,7 @@ private repository as a personal tool.
|
||||
- English routes: `/print`, `/library`, `/queue`, `/shares`, `/help`, `/account`,
|
||||
`/tour`, `/password`.
|
||||
- AGPL-3.0, CI (build, tests, type check, secret scan), `CONTRIBUTING`, `SECURITY`.
|
||||
- Astro 7 and current `sharp`: `npm audit` reports no known vulnerability.
|
||||
|
||||
**Carried over from the private tool**
|
||||
|
||||
|
||||
@@ -112,10 +112,11 @@ Astro 5 (SSR, Node standalone) with React islands · PostgreSQL · `sharp` ·
|
||||
`pdf-lib` · `pg-boss` · OKLCH design tokens, no Tailwind. Files go to a volume by
|
||||
default; S3/MinIO is available but not required.
|
||||
|
||||
About 13,600 lines across 116 files, 124 tests, 16 forward-only idempotent
|
||||
migrations that run themselves on boot. English and German interface, 1,038
|
||||
translated strings; the English sentence is the translation key, so there is no key
|
||||
file to keep in sync.
|
||||
About 13,700 lines across 117 files, 124 tests, 16 forward-only idempotent
|
||||
migrations that run themselves on boot. English and German interface — 1,067
|
||||
entries, covering every translatable string in the source. The English sentence is
|
||||
the translation key, so there is no key file to keep in sync and a missing entry
|
||||
degrades to readable English rather than to `print.sheet.title`.
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
+1
-2
@@ -14,7 +14,7 @@ export default defineConfig({
|
||||
// Content-Security-Policy with hashes instead of 'unsafe-inline': Astro computes
|
||||
// the hash of every inline script and style it emits, at build time. The remaining
|
||||
// directives are set by the middleware (src/middleware.ts).
|
||||
experimental: {
|
||||
// Graduated out of `experimental` in Astro 7.
|
||||
csp: {
|
||||
directives: [
|
||||
"default-src 'self'",
|
||||
@@ -27,7 +27,6 @@ export default defineConfig({
|
||||
"frame-ancestors 'none'",
|
||||
],
|
||||
},
|
||||
},
|
||||
server: { host: true, port: Number(process.env.PORT) || 4321 },
|
||||
vite: {
|
||||
ssr: {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 233 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 168 KiB |
Generated
+1628
-3854
File diff suppressed because it is too large.
Load diff
+4
-4
@@ -38,14 +38,14 @@
|
||||
"scan": "node scripts/secret-scan.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.1.3",
|
||||
"@astrojs/react": "^4.2.1",
|
||||
"@astrojs/node": "^11.1.5",
|
||||
"@astrojs/react": "^6.0.5",
|
||||
"@aws-sdk/client-s3": "^3.700.0",
|
||||
"@fontsource/space-grotesk": "^5.1.0",
|
||||
"@fontsource/space-mono": "^5.1.0",
|
||||
"archiver": "^7.0.1",
|
||||
"argon2": "^0.41.1",
|
||||
"astro": "^5.5.0",
|
||||
"astro": "^7.3.2",
|
||||
"basic-ftp": "^5.0.5",
|
||||
"grammy": "^1.30.0",
|
||||
"heic-convert": "^2.1.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"pg-boss": "^10.1.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"sharp": "^0.35.4",
|
||||
"ssh2-sftp-client": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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();
|
||||
// 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>
|
||||
|
||||
@@ -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
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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;}
|
||||
|
||||
Reference in new issue
Block a user