Hinweise optimiert
Test and publish container images / test (push) Successful in 2m27s
Test and publish container images / publish (push) Failing after 4s

This commit is contained in:
BuTzZ
2026-07-27 13:04:32 +02:00
parent 9813a1fafb
commit b98ec8bc6f
34 changed files with 500 additions and 399 deletions
+9 -3
View File
@@ -44,6 +44,7 @@ import { useGeolocation } from "./hooks/useGeolocation";
import { useMarineData } from "./hooks/useMarineData";
import type { OfflineVoyage } from "./lib/offline-route";
import { boatCategoryLabel, toVesselProfile } from "./lib/boat-profile";
import { isOpenRouteWarning } from "./lib/route-warnings";
import {
upcomingRouteEvents,
type RouteEventEtaBasis,
@@ -300,7 +301,7 @@ export function App() {
}, [route]);
const routeWarningCount = useMemo(
() => route?.warnings.filter((warning) => warning.severity !== "info").length ?? 0,
() => route?.warnings.filter(isOpenRouteWarning).length ?? 0,
[route]
);
const handleMapReady = useCallback(() => setMapReady(true), []);
@@ -487,9 +488,13 @@ export function App() {
: route
? "active"
: "idle",
route: guidanceAlarm || route?.warnings.some((warning) => warning.severity === "critical")
route: guidanceAlarm || route?.warnings.some(
(warning) => isOpenRouteWarning(warning) && warning.severity === "critical"
)
? "alarm"
: route?.warnings.some((warning) => warning.severity === "caution")
: route?.warnings.some(
(warning) => isOpenRouteWarning(warning) && warning.severity === "caution"
)
? "caution"
: courseAssistant.active || route
? "active"
@@ -1006,6 +1011,7 @@ export function App() {
headingSource={guidanceHeadingSource}
accuracyM={gps.accuracyM}
fixStale={courseAssistant.fixStale}
routeWarnings={route?.warnings ?? []}
onStop={courseAssistant.stop}
/>
</LazyContent>
+2 -2
View File
@@ -190,7 +190,7 @@ export function BoatProfilePanel({
<legend>Abmessungen und Fahrtwerte</legend>
<p>
Bitte die Werte für den normalen Fahrzustand eintragen. Sie
beeinflussen Durchfahrt, Tiefenprüfung und Fahrtdauer.
beeinflussen bekannte Durchfahrtsbeschränkungen und Fahrtdauer.
</p>
<div className="boat-profile-field-grid">
@@ -232,7 +232,7 @@ export function BoatProfilePanel({
/>
<NumberField
label="Sicherheitsreserve"
help="Zusätzlicher Abstand zum Grund beim Routing."
help="Wird im Bootsprofil gespeichert; eine automatische Routentiefenprüfung findet derzeit nicht statt."
unit="m"
min={0}
max={10}
@@ -1,5 +1,6 @@
import { AlertTriangle, Navigation, Square } from "lucide-react";
import type { RouteGuidanceResult } from "@watermaps/shared";
import type { RouteGuidanceResult, RouteWarning } from "@watermaps/shared";
import { RouteWarnings } from "./RouteWarnings";
import "./CourseAssistantPanel.css";
export type CourseAssistantPanelProps = {
@@ -9,6 +10,7 @@ export type CourseAssistantPanelProps = {
headingSource: "COG" | "HDG" | "--";
accuracyM: number | null;
fixStale: boolean;
routeWarnings?: RouteWarning[];
onStop: () => void;
};
@@ -19,6 +21,7 @@ export function CourseAssistantPanel({
headingSource,
accuracyM,
fixStale,
routeWarnings = [],
onStop
}: CourseAssistantPanelProps) {
const waitingForGps = !guidance && !fixStale;
@@ -47,6 +50,8 @@ export function CourseAssistantPanel({
</button>
</header>
<RouteWarnings warnings={routeWarnings} />
<div className="course-assistant-main">
<span
className="course-assistant-arrow"
+14 -54
View File
@@ -38,9 +38,11 @@ import {
DEFAULT_BOAT_PROFILE,
toVesselProfile
} from "../lib/boat-profile";
import { isOpenRouteWarning } from "../lib/route-warnings";
import type { RouteLock } from "../voyageHarbours";
import { LazyContent } from "./LazyContent";
import { NavigationDataPanel } from "./NavigationDataPanel";
import { RouteWarnings } from "./RouteWarnings";
import { RouteTidePanel, type RouteTidePlan } from "./RouteTidePanel";
import { VoyagePlan } from "./VoyagePlan";
@@ -173,10 +175,18 @@ export function RoutePlanner({
if (!result) {
return "idle";
}
if (result.warnings.some((warning) => warning.severity === "critical")) {
if (
result.warnings.some(
(warning) => isOpenRouteWarning(warning) && warning.severity === "critical"
)
) {
return "critical";
}
if (result.warnings.some((warning) => warning.severity === "caution")) {
if (
result.warnings.some(
(warning) => isOpenRouteWarning(warning) && warning.severity === "caution"
)
) {
return "caution";
}
return "ok";
@@ -207,9 +217,6 @@ export function RoutePlanner({
}
return new Date(timestamp + routeLocks.length * lockDelayMinutes * 60_000).toISOString();
}, [lockDelayMinutes, result?.eta, routeLocks.length, weatherReport?.adjustedEta]);
const criticalWarnings = result?.warnings.filter((warning) => warning.severity === "critical") ?? [];
const otherWarnings = result?.warnings.filter((warning) => warning.severity !== "critical") ?? [];
useEffect(() => {
if (!result) {
setPlannerView("input");
@@ -554,9 +561,7 @@ export function RoutePlanner({
<span>
{result.routingMode === "fairway"
? "Fahrwasser"
: result.unknownDepthRatio > 0
? `${Math.round(result.unknownDepthRatio * 100)}% unbekannt`
: "Tiefe OK"}
: "Direkte Wegpunkte"}
</span>
{(weatherReport?.adjustedEta ?? result.eta) && (
<span>
@@ -578,52 +583,7 @@ export function RoutePlanner({
</section>
)}
{(error || criticalWarnings.length > 0) && (
<section
className="warning-list route-priority-warnings"
aria-label="Kritische Hinweise"
role="alert"
>
<strong>Kritische Hinweise</strong>
{error && (
<p>
<AlertTriangle size={14} aria-hidden="true" />
{error}
</p>
)}
{criticalWarnings.map((warning, index) => (
<p key={`${warning.code}-${index}`}>
<AlertTriangle size={14} aria-hidden="true" />
{warning.message}
</p>
))}
</section>
)}
{otherWarnings.length > 0 && (
<section className="warning-list" aria-label="Weitere Hinweise">
<strong>Weitere Hinweise</strong>
{otherWarnings.slice(0, 3).map((warning, index) => (
<p key={`${warning.code}-${index}`}>
<AlertTriangle size={14} aria-hidden="true" />
{warning.message}
</p>
))}
{otherWarnings.length > 3 && (
<details className="voyage-tools-disclosure">
<summary>{otherWarnings.length - 3} weitere Hinweise</summary>
<div className="warning-list">
{otherWarnings.slice(3).map((warning, index) => (
<p key={`${warning.code}-${index + 3}`}>
<AlertTriangle size={14} aria-hidden="true" />
{warning.message}
</p>
))}
</div>
</details>
)}
</section>
)}
<RouteWarnings warnings={result.warnings} error={error} />
{routeOptions.length > 1 && (
<section className="route-options" aria-label="Routenalternativen">
+4 -1
View File
@@ -37,7 +37,10 @@ export function RouteTidePanel({ plan, loading, error }: RouteTidePanelProps) {
<TideLocation label="Ziel" summary={plan.destination} />
</div>
)}
<small>Stationsabstand und Bezugsnull beachten; Wasserstände ersetzen keine amtliche Tiefenprüfung.</small>
<small>
Stationsabstand und Bezugsnull beachten; angezeigte Wasserstände geben keine befahrbare
Wassertiefe an.
</small>
</section>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { AlertTriangle, Info } from "lucide-react";
import type { RouteWarning } from "@watermaps/shared";
import { isOpenRouteWarning, isRetainedRouteWarning } from "../lib/route-warnings";
export type RouteWarningsProps = {
warnings: RouteWarning[];
error?: string | null;
};
export function RouteWarnings({ warnings, error = null }: RouteWarningsProps) {
const openWarnings = warnings.filter(isOpenRouteWarning);
const criticalWarnings = openWarnings.filter((warning) => warning.severity === "critical");
const cautionWarnings = openWarnings.filter((warning) => warning.severity === "caution");
const informationalWarnings = warnings.filter(
(warning) => isRetainedRouteWarning(warning) && warning.severity === "info"
);
return (
<>
{(error || openWarnings.length > 0) && (
<section className="warning-list" aria-label="Routing-Hinweise">
{openWarnings.length > 0 && <strong>Hinweise ({openWarnings.length})</strong>}
{(error || criticalWarnings.length > 0) && (
<div
className="warning-list route-priority-warnings"
aria-label="Kritische Hinweise"
role="alert"
>
<strong>Kritische Hinweise</strong>
{error && (
<p>
<AlertTriangle size={14} aria-hidden="true" />
{error}
</p>
)}
{criticalWarnings.map((warning, index) => (
<p key={`${warning.code}-${index}`}>
<AlertTriangle size={14} aria-hidden="true" />
{warning.message}
</p>
))}
</div>
)}
{cautionWarnings.map((warning, index) => (
<p key={`${warning.code}-${index}`}>
<AlertTriangle size={14} aria-hidden="true" />
{warning.message}
</p>
))}
</section>
)}
{informationalWarnings.length > 0 && (
<details className="voyage-tools-disclosure">
<summary>Routingdetails ({informationalWarnings.length})</summary>
<div className="warning-list">
{informationalWarnings.map((warning, index) => (
<p key={`${warning.code}-${index}`}>
<Info size={14} aria-hidden="true" />
{warning.message}
</p>
))}
</div>
</details>
)}
</>
);
}
+5 -1
View File
@@ -1,4 +1,5 @@
import type { RouteResult } from "@watermaps/shared";
import { isRetainedRouteWarning } from "./route-warnings";
const DEFAULT_CREATOR = "Watermaps";
@@ -25,7 +26,10 @@ export function createRouteGpx(route: RouteResult, options: GpxExportOptions = {
const createdAt = normalizeDate(options.createdAt);
const source = cleanText(route.dataSources.join(", ") || DEFAULT_CREATOR, 500);
const warningSummary = cleanText(
route.warnings.map((warning) => warning.message).join(" · ") || "Nicht amtliche Routenplanung",
route.warnings
.filter(isRetainedRouteWarning)
.map((warning) => warning.message)
.join(" · ") || "Nicht amtliche Routenplanung",
1_000
);
const bounds = routeBounds(coordinates);
+20 -21
View File
@@ -5,6 +5,7 @@ import type {
RouteWarning,
VesselProfile
} from "@watermaps/shared";
import { isRetainedRouteWarning } from "./route-warnings";
export const OFFLINE_VOYAGES_STORAGE_KEY = "watermaps.offline-voyages.v1";
export const LEGACY_OFFLINE_VOYAGES_STORAGE_KEY = "seacompass.offline-voyages.v1";
@@ -211,8 +212,6 @@ function normalizeRoute(value: unknown): RouteResult {
const coordinates = normalizeLineCoordinates(value.geometry.coordinates);
const distanceNm = finiteNumber(value.distanceNm, 0, 100_000);
const eta = value.eta === null ? null : limitedString(value.eta, 100);
const minKnownDepthM = value.minKnownDepthM === null ? null : finiteNumber(value.minKnownDepthM, 0, 20_000);
const unknownDepthRatio = finiteNumber(value.unknownDepthRatio, 0, 1);
const warnings = normalizeWarnings(value.warnings);
const dataSources = normalizeStrings(value.dataSources, 100, 500);
const id = value.id === undefined ? undefined : limitedString(value.id, 160);
@@ -232,8 +231,6 @@ function normalizeRoute(value: unknown): RouteResult {
distanceNm,
eta,
warnings,
minKnownDepthM,
unknownDepthRatio,
dataSources,
...(departureTime ? { departureTime } : {}),
...(durationMinutes !== undefined ? { durationMinutes } : {}),
@@ -320,23 +317,25 @@ function normalizeWarnings(value: unknown): RouteWarning[] {
if (!Array.isArray(value) || value.length > 500) {
throw new OfflineVoyageStorageError("Die Routenwarnungen sind ungültig.");
}
return value.map((warning) => {
if (!isRecord(warning)) {
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
}
const code = limitedString(warning.code, 100);
const message = limitedString(warning.message, 1_000);
const severity = warning.severity;
if (!code || !message || (severity !== "info" && severity !== "caution" && severity !== "critical")) {
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
}
return {
code,
message,
severity,
...(warning.coordinate === undefined ? {} : { coordinate: normalizeCoordinate(warning.coordinate) })
};
});
return value
.map((warning): RouteWarning => {
if (!isRecord(warning)) {
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
}
const code = limitedString(warning.code, 100);
const message = limitedString(warning.message, 1_000);
const severity = warning.severity;
if (!code || !message || (severity !== "info" && severity !== "caution" && severity !== "critical")) {
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
}
return {
code,
message,
severity,
...(warning.coordinate === undefined ? {} : { coordinate: normalizeCoordinate(warning.coordinate) })
};
})
.filter(isRetainedRouteWarning);
}
function normalizeStrings(value: unknown, maxItems: number, maxLength: number): string[] {
+18
View File
@@ -0,0 +1,18 @@
import type { RouteWarning } from "@watermaps/shared";
const REMOVED_ROUTE_WARNING_CODES = new Set([
"FAIRWAY_ROUTE",
"FAIRWAY_DATA_NOT_OFFICIAL",
"DEPTH_UNKNOWN",
"DEPTH_PARTIAL",
"NO_KNOWN_DEPTH",
"DEPTH_TOO_SHALLOW"
]);
export function isRetainedRouteWarning(warning: RouteWarning): boolean {
return !REMOVED_ROUTE_WARNING_CODES.has(warning.code);
}
export function isOpenRouteWarning(warning: RouteWarning): boolean {
return isRetainedRouteWarning(warning) && warning.severity !== "info";
}