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:
1 parent
1b312b3103
commit
277fbb841e
9 files changed
+227
-64
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user