GPS Geschwindigkeitsanzeige
Test and publish container images / test (push) Successful in 2m25s
Test and publish container images / publish (push) Failing after 4s

This commit is contained in:
BuTzZ
2026-07-28 16:39:35 +02:00
parent 593dbd5f85
commit 2e84f4eae4
5 changed files with 70 additions and 45 deletions
+51 -21
View File
@@ -18,6 +18,13 @@ type StatusBarProps = {
} | null;
};
type StatusItemData = {
icon?: ReactNode;
label: string;
value: string;
speed?: boolean;
};
export function StatusBar({
gps,
forecast,
@@ -39,7 +46,7 @@ export function StatusBar({
? "Position aktiv"
: `±${gps.accuracyM} m`
: gpsStatusLabel(gps.status);
const speedValue = gps.speedKn === null ? "-- kn" : `${gps.speedKn.toFixed(1)} kn`;
const speedValue = formatGpsSpeed(gps.speedKn);
const tideValue = tide?.nextHigh
? `HW ${new Date(tide.nextHigh.time).toLocaleTimeString("de-DE", {
hour: "2-digit",
@@ -48,7 +55,7 @@ export function StatusBar({
: "Keine Daten";
const conditionsValue = formatConditions(forecast);
const items =
const items: StatusItemData[] =
resolvedMode === "anchor"
? [
{
@@ -95,9 +102,9 @@ export function StatusBar({
value: `${routeWarningCount} offen`
}
: {
icon: <Activity size={15} aria-hidden="true" />,
label: "SOG",
value: speedValue
icon: <Navigation2 size={15} aria-hidden="true" />,
label: "GPS",
value: gpsValue
}
]
: resolvedMode === "route"
@@ -115,13 +122,7 @@ export function StatusBar({
label: "GPS",
value: gpsValue
},
gps.position
? {
icon: <Activity size={15} aria-hidden="true" />,
label: "SOG",
value: speedValue
}
: {
{
icon: <Waves size={15} aria-hidden="true" />,
label: "Tide",
value: tideValue
@@ -144,23 +145,44 @@ export function StatusBar({
value: tideValue
}
];
const statusItems: StatusItemData[] = [
...items,
{
icon: <Activity size={15} aria-hidden="true" />,
label: "GPS-Geschwindigkeit",
value: speedValue,
speed: true
}
];
return (
<footer
className="status-bar"
aria-label="Navigationsstatus"
style={{ gridTemplateColumns: "repeat(3, minmax(0, 1fr))" }}
>
{items.map((item) => (
<StatusItem key={item.label} icon={item.icon} label={item.label} value={item.value} />
<footer className="status-bar" aria-label="Navigationsstatus">
{statusItems.map((item) => (
<StatusItem
key={item.label}
icon={item.icon}
label={item.label}
value={item.value}
speed={item.speed}
/>
))}
</footer>
);
}
function StatusItem({ icon, label, value }: { icon?: ReactNode; label: string; value: string }) {
function StatusItem({
icon,
label,
value,
speed = false
}: {
icon?: ReactNode;
label: string;
value: string;
speed?: boolean;
}) {
return (
<span className="status-item">
<span className="status-item" data-kind={speed ? "speed" : undefined}>
{icon}
<span className="status-label">{label}</span>
<strong>{value}</strong>
@@ -168,6 +190,14 @@ function StatusItem({ icon, label, value }: { icon?: ReactNode; label: string; v
);
}
function formatGpsSpeed(speedKn: number | null): string {
if (speedKn === null || !Number.isFinite(speedKn) || speedKn < 0) {
return "-- km/h · -- kn";
}
const format = (value: number) => value.toFixed(1).replace(".", ",");
return `${format(speedKn * 1.852)} km/h · ${format(speedKn)} kn`;
}
function gpsStatusLabel(status: GpsState["status"]): string {
switch (status) {
case "idle":
+7 -2
View File
@@ -1128,7 +1128,7 @@ button:disabled {
.route-weather-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(118px, 1.35fr);
gap: 6px;
}
@@ -1681,6 +1681,11 @@ button:disabled {
white-space: nowrap;
}
.status-item[data-kind="speed"] strong {
overflow: visible;
text-overflow: clip;
}
.status-label {
grid-column: 2;
font-size: 10px;
@@ -1778,7 +1783,7 @@ button:disabled {
}
.status-bar {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(118px, 1.35fr);
}
}
+5 -2
View File
@@ -18,7 +18,7 @@ const idleGps: GpsState = {
};
describe("StatusBar", () => {
it("shows only three planning values without claiming that a route is safe", () => {
it("always includes GPS speed in planning mode without claiming that a route is safe", () => {
const forecast = {
windSpeed: 12,
waveHeightM: 0.8
@@ -34,8 +34,9 @@ describe("StatusBar", () => {
/>
);
expect(container.querySelectorAll(".status-item")).toHaveLength(3);
expect(container.querySelectorAll(".status-item")).toHaveLength(4);
expect(screen.getByText("12 kn · 0.8 m")).toBeVisible();
expect(screen.getByText("-- km/h · -- kn")).toBeVisible();
expect(screen.queryByText("Route OK")).not.toBeInTheDocument();
});
@@ -60,6 +61,7 @@ describe("StatusBar", () => {
expect(screen.getByText("087°T")).toBeVisible();
expect(screen.getByText("24 m Stb")).toBeVisible();
expect(screen.getByText("2 offen")).toBeVisible();
expect(screen.getByText("11,5 km/h · 6,2 kn")).toBeVisible();
});
it("shows anchor drift, GPS and the expected tide rise in anchor mode", () => {
@@ -82,5 +84,6 @@ describe("StatusBar", () => {
expect(screen.getByText("12 / 40 m")).toBeVisible();
expect(screen.getByText("±5 m")).toBeVisible();
expect(screen.getByText("+1.3 m")).toBeVisible();
expect(screen.getByText("-- km/h · -- kn")).toBeVisible();
});
});
+1 -14
View File
@@ -372,20 +372,7 @@ function buildFairwayRouteCandidate(
const departureTimestamp = requestedDepartureTimestamp(request.departureTime);
const durationMinutes = Math.round((distanceNm / speedKn) * 60);
const eta = new Date(departureTimestamp + durationMinutes * 60 * 1000).toISOString();
const warnings: RouteWarning[] = [
...(searchTruncated
? [
{
code: "ROUTE_SEARCH_LIMITED",
severity: "caution" as const,
message:
"Die Snap-Suche wurde wegen eines dichten Fahrwassernetzes begrenzt. " +
"Die Route ist befahrbar verbunden, aber möglicherweise nicht die kürzeste Option."
}
]
: []),
...routeSnapWarnings(routeSnaps)
];
const warnings: RouteWarning[] = routeSnapWarnings(routeSnaps);
const route: RouteResult = {
geometry: {
+2 -2
View File
@@ -499,7 +499,7 @@ describe("route building", () => {
expect(route?.dataSources).not.toContain("long-detour");
});
it("reports bounded snap-search truncation instead of silently claiming no route", () => {
it("still routes when a bounded snap search found a connected path", () => {
const start = { lat: 52.00035, lon: 7.0005 };
const mainStart = { lat: 52, lon: 7.0005 };
const destination = { lat: 52, lon: 7.04 };
@@ -552,7 +552,7 @@ describe("route building", () => {
expect(boundedRoute).not.toBeNull();
expect(
boundedRoute?.warnings.some((warning) => warning.code === "ROUTE_SEARCH_LIMITED")
).toBe(true);
).toBe(false);
});
it("does not fall back to a misleading straight line when no fairway graph matches", () => {