fix(calendar,todo): production readiness improvements

- Calendar: apply ThrottlerGuard globally (was registered but not used)
- Calendar: localize error page with i18n (5 languages)
- Calendar: add meta/OG tags and PWA meta improvements
- Todo: localize error page with i18n (5 languages)
- Todo: add meta/OG tags to root layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-24 09:57:24 +01:00
parent 52e9aa5889
commit 8bc52f4264
22 changed files with 172 additions and 8 deletions

View file

@ -65,7 +65,10 @@ import { HttpExceptionFilter } from './common/http-exception.filter';
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
// ThrottlerGuard registered via ThrottlerModule — use @UseGuards(ThrottlerGuard) on controllers
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}

View file

@ -0,0 +1,45 @@
#!/usr/bin/env node
/**
* Generate PWA icons from SVG favicon
* Run: node scripts/generate-icons.mjs
* Requires: sharp (available in workspace)
*/
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const staticDir = join(__dirname, '..', 'static');
const sizes = [
{ name: 'favicon.png', size: 32 },
{ name: 'pwa-192x192.png', size: 192 },
{ name: 'pwa-512x512.png', size: 512 },
{ name: 'apple-touch-icon.png', size: 180 },
];
async function generateIcons() {
try {
const sharp = (await import('sharp')).default;
const svgPath = join(staticDir, 'favicon.svg');
const svgBuffer = readFileSync(svgPath);
for (const { name, size } of sizes) {
const outputPath = join(staticDir, name);
await sharp(svgBuffer).resize(size, size).png().toFile(outputPath);
console.log(`Generated: ${name} (${size}x${size})`);
}
console.log('\nAll icons generated successfully!');
} catch (error) {
if (error.code === 'ERR_MODULE_NOT_FOUND') {
console.error('Sharp is not installed. Run: pnpm add -D sharp');
} else {
console.error('Error generating icons:', error);
}
process.exit(1);
}
}
generateIcons();

View file

@ -1,9 +1,25 @@
<!doctype html>
<html lang="en">
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#3b82f6" />
<meta name="application-name" content="Calendar" />
<meta name="description" content="Kalender und Terminverwaltung" />
<!-- PWA/iOS Meta Tags -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Calendar" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon.png" />
<title>Calendar</title>
%sveltekit.head%
</head>

View file

@ -129,5 +129,9 @@
"a11y": {
"createEventOn": "Termin erstellen am {date}",
"slotTime": "{day} {time}"
},
"error": {
"notFound": "Seite nicht gefunden",
"backToHome": "Zurück zur Startseite"
}
}

View file

@ -129,5 +129,9 @@
"a11y": {
"createEventOn": "Create event on {date}",
"slotTime": "{day} {time}"
},
"error": {
"notFound": "Page not found",
"backToHome": "Back to home"
}
}

View file

@ -97,5 +97,9 @@
"search": "Buscar",
"error": "Error",
"success": "Éxito"
},
"error": {
"notFound": "Página no encontrada",
"backToHome": "Volver al inicio"
}
}

View file

@ -97,5 +97,9 @@
"search": "Rechercher",
"error": "Erreur",
"success": "Succès"
},
"error": {
"notFound": "Page non trouvée",
"backToHome": "Retour à l'accueil"
}
}

View file

@ -97,5 +97,9 @@
"search": "Cerca",
"error": "Errore",
"success": "Successo"
},
"error": {
"notFound": "Pagina non trovata",
"backToHome": "Torna alla home"
}
}

View file

@ -1,9 +1,10 @@
<script lang="ts">
import { page } from '$app/stores';
import { _ } from 'svelte-i18n';
</script>
<div class="flex min-h-[60vh] flex-col items-center justify-center text-center">
<h1 class="text-6xl font-bold text-blue-600 mb-4">{$page.status}</h1>
<p class="text-xl text-muted-foreground mb-8">{$page.error?.message || 'Seite nicht gefunden'}</p>
<a href="/" class="btn btn-primary">Zurück zur Startseite</a>
<p class="text-xl text-muted-foreground mb-8">{$page.error?.message || $_('error.notFound')}</p>
<a href="/" class="btn btn-primary">{$_('error.backToHome')}</a>
</div>

View file

@ -26,6 +26,19 @@
});
</script>
<svelte:head>
<meta
name="description"
content="Termine verwalten, Kalender teilen und den Überblick behalten mit Calendar von ManaCore."
/>
<meta property="og:title" content="Calendar - Kalender" />
<meta
property="og:description"
content="Termine verwalten, Kalender teilen und den Überblick behalten."
/>
<meta property="og:type" content="website" />
</svelte:head>
{#if !appReady}
<AppLoadingSkeleton />
{:else}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#2563eb"/>
</linearGradient>
</defs>
<!-- Rounded rectangle background -->
<rect width="512" height="512" rx="96" ry="96" fill="url(#bg)"/>
<!-- Calendar top bar -->
<rect x="80" y="120" width="352" height="48" rx="8" fill="rgba(255,255,255,0.3)"/>
<!-- Calendar rings -->
<rect x="160" y="96" width="24" height="56" rx="12" fill="white"/>
<rect x="328" y="96" width="24" height="56" rx="12" fill="white"/>
<!-- Calendar body -->
<rect x="80" y="168" width="352" height="248" rx="0 0 16 16" fill="rgba(255,255,255,0.15)"/>
<!-- Grid lines horizontal -->
<line x1="80" y1="230" x2="432" y2="230" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<line x1="80" y1="292" x2="432" y2="292" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<line x1="80" y1="354" x2="432" y2="354" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<!-- Grid lines vertical -->
<line x1="130" y1="168" x2="130" y2="416" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<line x1="180" y1="168" x2="180" y2="416" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<line x1="230" y1="168" x2="230" y2="416" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<line x1="280" y1="168" x2="280" y2="416" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<line x1="330" y1="168" x2="330" y2="416" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<line x1="380" y1="168" x2="380" y2="416" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<!-- Today highlight -->
<rect x="232" y="294" width="46" height="58" rx="8" fill="white" opacity="0.9"/>
<text x="255" y="333" font-family="system-ui, -apple-system, sans-serif" font-size="32" font-weight="700" fill="#2563eb" text-anchor="middle">24</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -93,6 +93,10 @@
"loadProjects": "Projekte konnten nicht geladen werden",
"loadLabels": "Labels konnten nicht geladen werden"
},
"error": {
"notFound": "Seite nicht gefunden",
"backToHome": "Zurück zur Startseite"
},
"success": {
"taskCreated": "Aufgabe erstellt",
"taskUpdated": "Aufgabe aktualisiert",

View file

@ -93,6 +93,10 @@
"loadProjects": "Failed to load projects",
"loadLabels": "Failed to load labels"
},
"error": {
"notFound": "Page not found",
"backToHome": "Back to home"
},
"success": {
"taskCreated": "Task created",
"taskUpdated": "Task updated",

View file

@ -93,6 +93,10 @@
"loadProjects": "No se pudieron cargar los proyectos",
"loadLabels": "No se pudieron cargar las etiquetas"
},
"error": {
"notFound": "Página no encontrada",
"backToHome": "Volver al inicio"
},
"success": {
"taskCreated": "Tarea creada",
"taskUpdated": "Tarea actualizada",

View file

@ -93,6 +93,10 @@
"loadProjects": "Impossible de charger les projets",
"loadLabels": "Impossible de charger les labels"
},
"error": {
"notFound": "Page non trouvée",
"backToHome": "Retour à l'accueil"
},
"success": {
"taskCreated": "Tâche créée",
"taskUpdated": "Tâche mise à jour",

View file

@ -93,6 +93,10 @@
"loadProjects": "Impossibile caricare i progetti",
"loadLabels": "Impossibile caricare le etichette"
},
"error": {
"notFound": "Pagina non trovata",
"backToHome": "Torna alla home"
},
"success": {
"taskCreated": "Attività creata",
"taskUpdated": "Attività aggiornata",

View file

@ -1,9 +1,10 @@
<script lang="ts">
import { page } from '$app/stores';
import { _ } from 'svelte-i18n';
</script>
<div class="flex min-h-[60vh] flex-col items-center justify-center text-center">
<h1 class="text-6xl font-bold text-emerald-600 mb-4">{$page.status}</h1>
<p class="text-xl text-muted-foreground mb-8">{$page.error?.message || 'Seite nicht gefunden'}</p>
<a href="/" class="btn btn-primary">Zurück zur Startseite</a>
<p class="text-xl text-muted-foreground mb-8">{$page.error?.message || $_('error.notFound')}</p>
<a href="/" class="btn btn-primary">{$_('error.backToHome')}</a>
</div>

View file

@ -10,6 +10,7 @@
let { children } = $props();
let loading = $state(true);
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
@ -23,6 +24,19 @@
});
</script>
<svelte:head>
<meta
name="description"
content="Aufgaben verwalten, Projekte organisieren und Produktivität steigern mit Todo von ManaCore."
/>
<meta property="og:title" content="Todo - Aufgabenverwaltung" />
<meta
property="og:description"
content="Aufgaben verwalten, Projekte organisieren und Produktivität steigern."
/>
<meta property="og:type" content="website" />
</svelte:head>
{#if !appReady}
<AppLoadingSkeleton layout="tasks" listItemCount={4} />
{:else}