feat(legal): In-App-Hinweise, Export-Disclaimer & Web-Härtung (v1.7.3)

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) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 12:11:43 +02:00
parent 6ab2fd9a6b
commit 235959101b
6 changed files with 78 additions and 3 deletions

View File

@@ -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": {

View File

@@ -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}
/>
<UsageNoticeBanner />
{/* 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. */}

View File

@@ -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 (
<div className="flex items-start gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-950/30 border-b border-amber-200 dark:border-amber-900 text-xs text-amber-800 dark:text-amber-300 print:hidden">
<Info className="w-4 h-4 mt-0.5 shrink-0" />
<p className="flex-1 leading-relaxed">
Lageplan ist ein unterstützendes Werkzeug ohne garantierte Verfügbarkeit. Bitte haltet aktuelle
Offline- und Notfallunterlagen bereit.{' '}
<Link href="/verantwortungsvolle-nutzung" className="underline font-medium">Mehr erfahren</Link>.
</p>
<button
onClick={() => { try { localStorage.setItem(KEY, '1') } catch {}; setDismissed(true) }}
aria-label="Hinweis minimieren"
className="shrink-0 rounded p-0.5 hover:bg-amber-100 dark:hover:bg-amber-900/50"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
)
}

View File

@@ -157,7 +157,10 @@ export function MeasurePanel({ measurement, hoseTypes, onClose }: MeasurePanelPr
)}
<p className="text-[10px] text-muted-foreground leading-snug">
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'}.
</p>
<p className="text-[10px] text-amber-600 dark:text-amber-400 leading-snug font-medium">
Berechnungshilfe: Ergebnis vor der operativen Verwendung fachlich prüfen. Karten-/Geodaten können ungenau sein.
</p>
</div>
</div>

View File

@@ -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

View File

@@ -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 = {