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:
10 files changed
+912
-1822
No files matched your search
Generated
+865
-1788
File diff suppressed because it is too large.
Load diff
+24
-20
@@ -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
@@ -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`);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user