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",
|
"name": "lageplan",
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
import { JournalView } from '@/components/journal/journal-view'
|
import { JournalView } from '@/components/journal/journal-view'
|
||||||
import { Lock, Eye, Pencil, AlertTriangle, WifiOff, GraduationCap, Map as MapIcon, ClipboardList, LayoutGrid } from 'lucide-react'
|
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 { CustomDragLayer } from '@/components/map/custom-drag-layer'
|
||||||
import { OnboardingTour, resetOnboardingTour } from '@/components/onboarding/onboarding-tour'
|
import { OnboardingTour, resetOnboardingTour } from '@/components/onboarding/onboarding-tour'
|
||||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
|
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
|
||||||
@@ -1035,6 +1036,8 @@ export default function AppPage() {
|
|||||||
editingLoading={editingLoading}
|
editingLoading={editingLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UsageNoticeBanner />
|
||||||
|
|
||||||
{/* Kompakte Status-Zeile — eine Zeile mit Chips statt bis zu vier gestapelten Bannern.
|
{/* Kompakte Status-Zeile — eine Zeile mit Chips statt bis zu vier gestapelten Bannern.
|
||||||
Zeigt nur, was gerade zutrifft; im Normalfall (Einsatz, online, verifiziert,
|
Zeigt nur, was gerade zutrifft; im Normalfall (Einsatz, online, verifiziert,
|
||||||
niemand fremd am Zeichnen) erscheint sie gar nicht. */}
|
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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -313,6 +313,19 @@ export function useMapExport({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (format === 'png') {
|
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')
|
const link = document.createElement('a')
|
||||||
link.download = `${safeName}.png`
|
link.download = `${safeName}.png`
|
||||||
link.href = exportCanvas.toDataURL('image/png')
|
link.href = exportCanvas.toDataURL('image/png')
|
||||||
@@ -435,6 +448,14 @@ export function useMapExport({
|
|||||||
|
|
||||||
// ── Footer ──
|
// ── Footer ──
|
||||||
const footerY = pageH - m - 4
|
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.setFontSize(7)
|
||||||
pdf.setFont('helvetica', 'normal')
|
pdf.setFont('helvetica', 'normal')
|
||||||
pdf.setTextColor(156, 163, 175) // gray-400
|
pdf.setTextColor(156, 163, 175) // gray-400
|
||||||
|
|||||||
@@ -95,7 +95,15 @@ export async function middleware(req: NextRequest) {
|
|||||||
return new NextResponse(null, { status: 404 })
|
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 = {
|
export const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user