Abhängigkeiten aktuell, und die Typprüfung ist zum ersten Mal grün

npm audit meldete sieben Lücken (sechs hoch), darunter nodemailer und
basic-ftp, die nur mit dem nächsten Hauptversionssprung behoben sind.
Jetzt: 0.

- nodemailer 10, basic-ftp 6, argon2 0.45, ssh2-sftp-client 12,
  @astrojs/react 7: keine Codeänderung. Alte argon2-Hashes werden weiter
  erkannt (gegengeprüft: Hash aus 0.41, Prüfung mit 0.45).
- archiver 8 kennt keinen Default-Export mehr, nur noch `ZipArchive`.
  Hätte sich erst beim ersten ZIP-Download gezeigt.
- basic-ftp 6 verbindet die Datenleitung nicht mehr still mit einer
  anderen Adresse, wenn ein FTP-Server hinter NAT seine interne IP meldet.
  Das ist Absicht der Bibliothek. Nach dem Deploy jedes FTPS-Ziel einmal
  testen.
- pg-boss bleibt auf 10: 12 kann das bestehende Warteschlangen-Schema
  nicht migrieren. Eigener Schritt mit Wartungsfenster.

`npm run typecheck` gibt es jetzt, und es meldet 0 statt 17 Fehler. Einer
davon war ein echter Fehler: Der Druckbogen aus dem Telegram-Bot bekam
seine Fußzeile als String statt als Objekt und kam deshalb ohne jede
Fußzeile — also ohne den Hinweis, bei 100 % zu drucken.
This commit is contained in:
till committed 2026-10-10 06:15:05 +00:00
1 parent 2c2ef9d458
commit ee887cc29c
10 files changed
+912 -1822

No files matched your search

+865 -1788
View File
File diff suppressed because it is too large. Load diff
+24 -20
View File
@@ -4,7 +4,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=22.6" "node": ">=22.12"
}, },
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
@@ -12,34 +12,38 @@
"start": "node ./dist/server/entry.mjs", "start": "node ./dist/server/entry.mjs",
"migrate": "node --loader tsx ./scripts/migrate.mjs", "migrate": "node --loader tsx ./scripts/migrate.mjs",
"astro": "astro", "astro": "astro",
"test": "node --experimental-strip-types --test tests/*.test.ts" "test": "node --experimental-strip-types --test tests/*.test.ts",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^11.1.5", "@astrojs/node": "^11.1.7",
"@astrojs/react": "^6.0.5", "@astrojs/react": "^7.0.1",
"@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/client-s3": "^3.1149.0",
"@aws-sdk/s3-request-presigner": "^3.700.0", "@aws-sdk/s3-request-presigner": "^3.1149.0",
"@fontsource/space-grotesk": "^5.1.0", "@fontsource/space-grotesk": "^5.1.0",
"@fontsource/space-mono": "^5.1.0", "@fontsource/space-mono": "^5.1.0",
"archiver": "^7.0.1", "archiver": "^8.0.0",
"argon2": "^0.41.1", "argon2": "^0.45.1",
"astro": "^7.3.2", "astro": "^7.3.8",
"basic-ftp": "^5.0.5", "basic-ftp": "^6.2.3",
"grammy": "^1.30.0", "grammy": "^1.46.0",
"heic-convert": "^2.1.0", "heic-convert": "^2.1.0",
"nodemailer": "^9.1.0", "nodemailer": "^10.0.16",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pg": "^8.13.1", "pg": "^8.23.1",
"pg-boss": "^10.1.5", "pg-boss": "^10.1.5",
"react": "^19.0.0", "react": "^19.3.0",
"react-dom": "^19.0.0", "react-dom": "^19.3.0",
"sharp": "^0.35.4", "sharp": "^0.35.5",
"ssh2-sftp-client": "^11.0.0" "ssh2-sftp-client": "^12.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/pg": "^8.11.10", "@types/archiver": "^8.0.0",
"@types/react": "^19.0.0", "@types/node": "^26.6.5",
"@types/react-dom": "^19.0.0", "@types/pg": "^8.23.1",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@types/ssh2-sftp-client": "^9.0.6",
"typescript": "^5.7.2" "typescript": "^5.7.2"
} }
} }
+6 -1
View File
@@ -310,7 +310,12 @@ async function dispatchPrint(chatId: number, draftId: string, sizeId: string, co
const label = labelMm(size.w, size.h); const label = labelMm(size.w, size.h);
const bytes = await buildSheetPdf({ const bytes = await buildSheetPdf({
sheet, pages, marksPerPage: marks, images, title: `Klarbild Druckbogen ${label}`, sheet, pages, marksPerPage: marks, images, title: `Klarbild Druckbogen ${label}`,
footer: `Klarbild · DIN A4 · ${label} · 300 dpi · Druck bei 100 % (nicht „an Seite anpassen")`, // Muss ein FooterSpec sein, kein String: Ein String hat kein `.text`, und der
// Bogen kam bisher ohne jede Fußzeile — also ohne den Hinweis auf 100 %.
footer: {
text: `Klarbild · DIN A4 · ${label} · 300 dpi · Druck bei 100 % (nicht „an Seite anpassen")`,
position: 'unten-links', sizePt: 6,
},
}); });
const file = new InputFile(Buffer.from(bytes), `klarbild-druckbogen-${label.replace(/[^0-9]+/g, 'x')}.pdf`); const file = new InputFile(Buffer.from(bytes), `klarbild-druckbogen-${label.replace(/[^0-9]+/g, 'x')}.pdf`);
+1 -1
View File
@@ -43,7 +43,7 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
try { buf = await sharp(buf, { failOn: 'none' }).jpeg({ quality: 90, mozjpeg: true }).toBuffer(); contentType = 'image/jpeg'; } try { buf = await sharp(buf, { failOn: 'none' }).jpeg({ quality: 90, mozjpeg: true }).toBuffer(); contentType = 'image/jpeg'; }
catch { /* Fallback: Original */ } catch { /* Fallback: Original */ }
} }
return new Response(buf, { return new Response(new Uint8Array(buf), {
headers: { headers: {
'Content-Type': contentType, 'Content-Type': contentType,
'Cache-Control': 'private, max-age=600', 'Cache-Control': 'private, max-age=600',
+2 -2
View File
@@ -1,6 +1,6 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { buildAuswahlZipName } from '../../../lib/naming'; import { buildAuswahlZipName } from '../../../lib/naming';
import archiver from 'archiver'; import { ZipArchive } from 'archiver';
import { query } from '../../../lib/db'; import { query } from '../../../lib/db';
import { getObject } from '../../../lib/storage'; import { getObject } from '../../../lib/storage';
@@ -17,7 +17,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
`SELECT id, result_path, filename FROM items WHERE id = ANY($1) AND result_path IS NOT NULL`, [ids]); `SELECT id, result_path, filename FROM items WHERE id = ANY($1) AND result_path IS NOT NULL`, [ids]);
if (!rows.length) return new Response('Keine Ergebnisse.', { status: 404 }); if (!rows.length) return new Response('Keine Ergebnisse.', { status: 404 });
const archive = archiver('zip', { zlib: { level: 6 } }); const archive = new ZipArchive({ zlib: { level: 6 } });
const seen: Record<string, number> = {}; const seen: Record<string, number> = {};
for (const r of rows) { for (const r of rows) {
try { try {
+1 -1
View File
@@ -42,7 +42,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
const cropW = (crop?.w ?? 1) * out.srcPx[0]; const cropW = (crop?.w ?? 1) * out.srcPx[0];
const check = dpiCheck(cropW, size.w, dpi); const check = dpiCheck(cropW, size.w, dpi);
return new Response(out.buffer, { return new Response(new Uint8Array(out.buffer), {
headers: { headers: {
'Content-Type': out.ext === 'png' ? 'image/png' : 'image/jpeg', 'Content-Type': out.ext === 'png' ? 'image/png' : 'image/jpeg',
'Content-Disposition': `attachment; filename="${buildSingleFilename({ 'Content-Disposition': `attachment; filename="${buildSingleFilename({
+2 -2
View File
@@ -1,5 +1,5 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import archiver from 'archiver'; import { ZipArchive } from 'archiver';
import { Readable } from 'node:stream'; import { Readable } from 'node:stream';
import { once } from 'node:events'; import { once } from 'node:events';
import { query } from '../../../../lib/db'; import { query } from '../../../../lib/db';
@@ -64,7 +64,7 @@ export const GET: APIRoute = async ({ params, request, clientAddress }) => {
[bilder.map((b) => b.id)]); [bilder.map((b) => b.id)]);
const nachId = new Map(pfade.map((p) => [p.id, p])); const nachId = new Map(pfade.map((p) => [p.id, p]));
const archive = archiver('zip', { zlib: { level: 6 } }); const archive = new ZipArchive({ zlib: { level: 6 } });
// Fehler dürfen den Prozess nicht mitnehmen; der Strom bricht dann ab. // Fehler dürfen den Prozess nicht mitnehmen; der Strom bricht dann ab.
archive.on('error', (e) => console.error('[share/zip]', e?.message || e)); archive.on('error', (e) => console.error('[share/zip]', e?.message || e));
archive.on('warning', (e) => console.error('[share/zip] Warnung:', e?.message || e)); archive.on('warning', (e) => console.error('[share/zip] Warnung:', e?.message || e));
+1 -1
View File
@@ -54,7 +54,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
return json({ error: 'Dieser Link ist abgelaufen — erst die Gültigkeit verlängern.' }, 409); return json({ error: 'Dieser Link ist abgelaufen — erst die Gültigkeit verlängern.' }, 409);
const url = `${basis()}/s/${share.slug}`; const url = `${basis()}/s/${share.slug}`;
const absender = locals.user.name || locals.user.username || 'Jemand'; const absender = locals.user.name || 'Jemand';
const titel = share.title || (share.anzahl > 1 ? `${share.anzahl} Bilder` : 'Ein Bild'); const titel = share.title || (share.anzahl > 1 ? `${share.anzahl} Bilder` : 'Ein Bild');
const nachricht = String(b.message || '').trim().slice(0, 1000); const nachricht = String(b.message || '').trim().slice(0, 1000);
const gueltig = share.expires_at const gueltig = share.expires_at
+1 -1
View File
@@ -22,7 +22,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
if (!isImg) { out.push({ filename: file.name, error: 'Nicht unterstütztes Format.' }); continue; } if (!isImg) { out.push({ filename: file.name, error: 'Nicht unterstütztes Format.' }); continue; }
try { try {
let buf = Buffer.from(await file.arrayBuffer()); let buf: Buffer = Buffer.from(await file.arrayBuffer());
let ext = 'png'; let ext = 'png';
const heic = /heic|heif/i.test(file.type) || /\.(heic|heif)$/i.test(file.name); const heic = /heic|heif/i.test(file.type) || /\.(heic|heif)$/i.test(file.name);
if (heic) buf = await heicToPng(buf); if (heic) buf = await heicToPng(buf);
+9 -5
View File
@@ -1,10 +1,14 @@
{ {
"extends": "astro/tsconfigs/strict", "extends": "astro/tsconfigs/strict",
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx"
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
}, },
"include": ["src", "scripts"], "include": [
"exclude": ["dist", "node_modules"] "src",
"scripts"
],
"exclude": [
"dist",
"node_modules"
]
} }