fix: address final review — cloud TLS, MAC guard, honest ISP metrics

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zacharyandClaude Opus 5 committed 2026-10-08 00:23:44 +02:00
1 parent 1b312b3103
commit 277fbb841e
9 files changed
+227 -64

No files matched your search

+59 -32
View File
@@ -7,6 +7,7 @@ import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { clientApi } from "@homarr/api/client";
import { useIntegrationConnected } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
@@ -15,6 +16,18 @@ dayjs.extend(relativeTime);
const EMPTY_VALUE = "—";
/**
* Le request-handler met les compteurs locaux en cache 60 s et le widget se
* rafraîchit toutes les 60 s : une donnée de moins de 2 min 30 est normale.
* Au-delà, la console n'a pas répondu et `fallbackToStaleOnError` sert la
* dernière valeur connue — ce que l'utilisateur doit voir, sans quoi la source
* périmée paraîtrait plus fraîche que le bloc cloud, qui affiche son âge.
*/
const LOCAL_STALE_AFTER_MS = 150_000;
/** Identité stable : évite de relancer l'effet du hook à chaque rendu. */
const NEVER_UPDATED = new Date(0);
const formatBitsPerSecond = (bps: number): string => {
const units = ["b/s", "Kb/s", "Mb/s", "Gb/s"];
let value = bps;
@@ -40,6 +53,8 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
const { data: results = [] } = clientApi.widget.unifiOs.summary.useQuery({ integrationIds });
const summary = useMemo(() => results[0]?.summary ?? null, [results]);
const updatedAt = results[0]?.updatedAt ?? NEVER_UPDATED;
const isLocalFresh = useIntegrationConnected(updatedAt, { timeout: LOCAL_STALE_AFTER_MS });
if (summary === null) {
return (
@@ -52,6 +67,7 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
}
const wanColor = summary.wan.state === "online" ? "green" : summary.wan.state === "offline" ? "red" : "gray";
const isp = summary.wan.isp;
return (
<Stack h="100%" p="sm" gap="xs" justify="space-between">
@@ -62,6 +78,17 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
<Text size="xs" c="dimmed" tt="uppercase" fw={600} truncate>
{summary.site.name}
</Text>
{/* Second rôle du badge de fraîcheur (§8) : une console injoignable est
servie depuis le cache périmé. Sans ce signal, les compteurs locaux
paraîtraient plus frais que le bloc cloud, qui affiche son âge.
Gris et discret : il ne doit pas concurrencer le bandeau WAN. */}
{!isLocalFresh && (
<Tooltip label={t("widget.unifiOs.card.stale")}>
<Badge size="xs" variant="light" color="gray" style={{ flexShrink: 0 }}>
{dayjs(updatedAt).fromNow()}
</Badge>
</Tooltip>
)}
</Group>
<Stack
@@ -83,40 +110,40 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
</Text>
</Group>
{options.showIsp && (
{/* Bloc ISP masqué quand il n'y a pas de donnée cloud (§8) : sans clé
Site Manager, cloud injoignable, ou aucun site correspondant au MAC
du gateway. Une absence que l'intégration a positivement établie ne
se déguise pas en valeur inconnue. */}
{options.showIsp && isp !== null && (
<Group justify="space-between" wrap="nowrap" gap="xs">
{summary.wan.isp === null ? (
<>
<Text size="xs" c="dimmed">
{t("widget.unifiOs.card.isp")}
<Text size="xs" c="dimmed" truncate>
{isp.name}
{isp.externalIp !== null ? ` · ${isp.externalIp}` : ""}
</Text>
<Group gap={6} wrap="nowrap">
{/* Le cloud omet la latence pour les périodes de coupure : afficher
un tiret plutôt qu'un « 0 ms » qui rassurerait à tort. */}
<Text size="xs">
{isp.latencyMs === null ? EMPTY_VALUE : t("widget.unifiOs.card.latency", { value: `${isp.latencyMs}` })}
</Text>
{isp.packetLossPct !== null && (
<Text size="xs" c={isp.packetLossPct > 0 ? "yellow" : undefined}>
{t("widget.unifiOs.card.packetLoss", { value: `${isp.packetLossPct}` })}
</Text>
<Text size="xs" c="dimmed">
{EMPTY_VALUE}
</Text>
</>
) : (
<>
<Text size="xs" c="dimmed" truncate>
{summary.wan.isp.name}
{summary.wan.isp.externalIp !== null ? ` · ${summary.wan.isp.externalIp}` : ""}
</Text>
<Group gap={6} wrap="nowrap">
<Text size="xs">{t("widget.unifiOs.card.latency", { value: `${summary.wan.isp.latencyMs}` })}</Text>
{/* The cloud metrics behind this badge lag ~27 minutes: it must
never look live, hence the relative-time badge instead of a
bare number. */}
<Tooltip
label={t("widget.unifiOs.card.measuredAt", {
time: dayjs(summary.wan.isp.measuredAt).format("HH:mm"),
})}
>
<Badge size="xs" variant="light" color="yellow">
{dayjs(summary.wan.isp.measuredAt).fromNow()}
</Badge>
</Tooltip>
</Group>
</>
)}
)}
{/* The cloud metrics behind this badge lag ~27 minutes: it must
never look live, hence the relative-time badge instead of a
bare number. */}
<Tooltip
label={t("widget.unifiOs.card.measuredAt", {
time: dayjs(isp.measuredAt).format("HH:mm"),
})}
>
<Badge size="xs" variant="light" color="yellow">
{dayjs(isp.measuredAt).fromNow()}
</Badge>
</Tooltip>
</Group>
</Group>
)}
</Stack>