From 235959101bb8583d19afdba3089ab3e86d4d55ed Mon Sep 17 00:00:00 2001 From: Pepe Ziberi Date: Thu, 23 Jul 2026 12:11:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(legal):=20In-App-Hinweise,=20Export-Discla?= =?UTF-8?q?imer=20&=20Web-H=C3=A4rtung=20(v1.7.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teil 4 des Compliance-Updates: - Export: rechtlicher Prüf-Hinweis in PDF-Footer und eingebrannt in PNG ("Angaben vor Verwendung fachlich prüfen · Offline-/Einsatzunterlagen bleiben massgebend") - Dashboard: dezenter, minimierbarer Hinweisbanner (unterstützendes Werkzeug ohne garantierte Verfügbarkeit → /verantwortungsvolle-nutzung); bleibt in den Rechtsseiten erhalten - Messpanel: expliziter Berechnungs-/Geodaten-Hinweis ("vor operativer Verwendung fachlich prüfen") - Middleware: private Routen (/app, /admin, /settings, /konto) mit X-Robots-Tag noindex + no-store Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- src/app/app/page.tsx | 3 ++ src/components/legal/usage-notice-banner.tsx | 40 ++++++++++++++++++++ src/components/map/measure-panel.tsx | 5 ++- src/hooks/use-map-export.ts | 21 ++++++++++ src/middleware.ts | 10 ++++- 6 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/components/legal/usage-notice-banner.tsx diff --git a/package.json b/package.json index e4a9a4f..1a63c56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lageplan", - "version": "1.7.2", + "version": "1.7.3", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "private": true, "scripts": { diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index 7c7f38e..68ca656 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -20,6 +20,7 @@ import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { JournalView } from '@/components/journal/journal-view' import { Lock, Eye, Pencil, AlertTriangle, WifiOff, GraduationCap, Map as MapIcon, ClipboardList, LayoutGrid } from 'lucide-react' +import { UsageNoticeBanner } from '@/components/legal/usage-notice-banner' import { CustomDragLayer } from '@/components/map/custom-drag-layer' import { OnboardingTour, resetOnboardingTour } from '@/components/onboarding/onboarding-tour' import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts' @@ -1035,6 +1036,8 @@ export default function AppPage() { editingLoading={editingLoading} /> + + {/* Kompakte Status-Zeile — eine Zeile mit Chips statt bis zu vier gestapelten Bannern. Zeigt nur, was gerade zutrifft; im Normalfall (Einsatz, online, verifiziert, niemand fremd am Zeichnen) erscheint sie gar nicht. */} diff --git a/src/components/legal/usage-notice-banner.tsx b/src/components/legal/usage-notice-banner.tsx new file mode 100644 index 0000000..96a8f99 --- /dev/null +++ b/src/components/legal/usage-notice-banner.tsx @@ -0,0 +1,40 @@ +'use client' + +import { useEffect, useState } from 'react' +import Link from 'next/link' +import { Info, X } from 'lucide-react' + +const KEY = 'lageplan-usage-notice-dismissed' + +/** + * Dezenter, minimierbarer Hinweis im Dashboard/App-Bereich. + * Nach Kenntnisnahme minimiert (localStorage) — verschwindet aber nicht aus den Rechtsseiten + * (/verantwortungsvolle-nutzung, Footer). + */ +export function UsageNoticeBanner() { + const [dismissed, setDismissed] = useState(true) + + useEffect(() => { + try { setDismissed(localStorage.getItem(KEY) === '1') } catch { setDismissed(false) } + }, []) + + if (dismissed) return null + + return ( +
+ +

+ Lageplan ist ein unterstützendes Werkzeug ohne garantierte Verfügbarkeit. Bitte haltet aktuelle + Offline- und Notfallunterlagen bereit.{' '} + Mehr erfahren. +

+ +
+ ) +} diff --git a/src/components/map/measure-panel.tsx b/src/components/map/measure-panel.tsx index 9fafb96..46a09f2 100644 --- a/src/components/map/measure-panel.tsx +++ b/src/components/map/measure-panel.tsx @@ -157,7 +157,10 @@ export function MeasurePanel({ measurement, hoseTypes, onClose }: MeasurePanelPr )}

- Länge inkl. 10% Verlegereserve · Höhendaten: {measurement.elevationSource === 'open-meteo' ? 'Open-Meteo DEM (~90 m Raster)' : measurement.elevationSource === 'open-elevation' ? 'Open-Elevation' : 'keine'} · Richtwerte, ersetzt keine Lagebeurteilung. + Länge inkl. 10% Verlegereserve · Höhendaten: {measurement.elevationSource === 'open-meteo' ? 'Open-Meteo DEM (~90 m Raster)' : measurement.elevationSource === 'open-elevation' ? 'Open-Elevation' : 'keine'}. +

+

+ Berechnungshilfe: Ergebnis vor der operativen Verwendung fachlich prüfen. Karten-/Geodaten können ungenau sein.

diff --git a/src/hooks/use-map-export.ts b/src/hooks/use-map-export.ts index 55aa3e0..ff8cd05 100644 --- a/src/hooks/use-map-export.ts +++ b/src/hooks/use-map-export.ts @@ -313,6 +313,19 @@ export function useMapExport({ } if (format === 'png') { + // Rechtlicher Prüf-Hinweis unten einbrennen + const disclaimer = 'Mit Lageplan erstellt · Angaben vor Verwendung fachlich prüfen · Offline-/Einsatzunterlagen der Organisation bleiben massgebend' + const barH = Math.max(18, Math.round(exportCanvas.width * 0.014)) + ctx.save() + ctx.fillStyle = 'rgba(0,0,0,0.55)' + ctx.fillRect(0, exportCanvas.height - barH, exportCanvas.width, barH) + ctx.fillStyle = '#ffffff' + ctx.font = `${Math.round(barH * 0.55)}px sans-serif` + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + ctx.fillText(disclaimer, exportCanvas.width / 2, exportCanvas.height - barH / 2, exportCanvas.width - 20) + ctx.restore() + const link = document.createElement('a') link.download = `${safeName}.png` link.href = exportCanvas.toDataURL('image/png') @@ -435,6 +448,14 @@ export function useMapExport({ // ── Footer ── const footerY = pageH - m - 4 + // Rechtlicher Prüf-Hinweis (eine Zeile über dem Meta-Footer) + pdf.setFontSize(6.5) + pdf.setFont('helvetica', 'italic') + pdf.setTextColor(120, 120, 120) + pdf.text( + 'Mit Lageplan erstellt. Angaben vor Verwendung fachlich prüfen. Die aktuellen Offline- und Einsatzunterlagen der zuständigen Organisation bleiben massgebend.', + m, footerY - 4 + ) pdf.setFontSize(7) pdf.setFont('helvetica', 'normal') pdf.setTextColor(156, 163, 175) // gray-400 diff --git a/src/middleware.ts b/src/middleware.ts index 8ce5a8f..3be3d87 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -95,7 +95,15 @@ export async function middleware(req: NextRequest) { return new NextResponse(null, { status: 404 }) } - return NextResponse.next() + const res = NextResponse.next() + + // Private/authentifizierte Bereiche: nicht indexieren und nicht cachen. + if (PROTECTED_ROUTES.some(r => pathname.startsWith(r))) { + res.headers.set('X-Robots-Tag', 'noindex, nofollow, noarchive') + res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate') + } + + return res } export const config = {