Show each farm's gateway status and what discovery found

When the fleet page is empty or a printer is "unknown", the reason is
almost always upstream: the gateway is not connected, or discovery never
reported. A strip per farm now says which - "Gateway online/offline/
checking" (no answer yet is not offline) - with a collapsible list of the
printers discovery found, each marked already added, same address as an
added printer, or not added yet, using the backend's serial/address match.
It also says plainly when discovery has not reported at all, which is the
default, as distinct from a scan that found nothing.

Read-only on purpose: adding a printer from the UI still needs the proxy's
offline-capable store and a credential pass-through that is never kept in
the cloud. Polled every 10s (gateway presence has no push channel); a
failed poll keeps the last answer instead of flipping to offline.

Checked in real Chrome against the live stack (Gateway online, discovery
not reported, no page errors); the static SPA still generates, now 6 routes.
This commit is contained in:
2026-10-29 14:41:26 +00:00
parent c7f85870db
commit 571a2c0ca7
4 changed files with 131 additions and 2 deletions
+32 -1
View File
@@ -1,5 +1,12 @@
<script setup lang="ts">
import { PrinterCard, type AppPrinterState, type PrinterCommand } from "@continuum/ui";
import {
FarmStatus,
PrinterCard,
type AppFarm,
type AppFarmDiscovery,
type AppPrinterState,
type PrinterCommand,
} from "@continuum/ui";
import { useTelemetryStore } from "~/stores/telemetry";
const telemetry = useTelemetryStore();
@@ -47,6 +54,29 @@ async function signalOffer(printerId: string, sdp: string): Promise<string> {
return answer.sdp;
}
// Which farms exist, and for each whether its gateway is connected and what
// discovery has found. Polled, because gateway presence has no push channel;
// ten seconds is quick enough to notice an unplugged box without hammering.
const { data: farms } = await useAsyncData("farms", () => apiFetch<AppFarm[]>("/farms"));
const farmStatus = ref<Record<string, AppFarmDiscovery>>({});
async function refreshFarmStatus() {
for (const farm of farms.value ?? []) {
try {
farmStatus.value[farm.id] = await apiFetch<AppFarmDiscovery>(`/farms/${farm.id}/discovered-printers`);
} catch {
// Keep the last answer rather than flip to "offline" over a failed poll.
}
}
}
let farmPoll: ReturnType<typeof setInterval> | undefined;
onMounted(() => {
void refreshFarmStatus();
farmPoll = setInterval(() => void refreshFarmStatus(), 10_000);
});
onBeforeUnmount(() => clearInterval(farmPoll));
/**
* Sends one command to a printer and turns a refusal into a sentence.
*
@@ -84,6 +114,7 @@ watch(
<template>
<div class="fleet">
<h1>Fleet overview</h1>
<FarmStatus v-for="farm in farms ?? []" :key="farm.id" :farm="farm" :status="farmStatus[farm.id] ?? null" />
<div class="fleet__grid">
<PrinterCard
v-for="printer in printers ?? []"