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:
@@ -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": {
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
40
src/components/legal/usage-notice-banner.tsx
Normal file
40
src/components/legal/usage-notice-banner.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user