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:
@@ -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 ?? []"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { AppFarm, AppFarmDiscovery } from "../types";
|
||||
|
||||
const props = defineProps<{ farm: AppFarm; status: AppFarmDiscovery | null }>();
|
||||
|
||||
// Three honest answers, not two: no answer yet is not "offline".
|
||||
const gateway = computed(() =>
|
||||
props.status === null ? "checking" : props.status.gatewayOnline ? "online" : "offline",
|
||||
);
|
||||
|
||||
const candidates = computed(() => props.status?.discovery?.printers ?? []);
|
||||
|
||||
const discoveryNote = computed(() => {
|
||||
if (!props.status) return "";
|
||||
if (props.status.discovery === null)
|
||||
return "Discovery has not reported - it is off by default on a gateway.";
|
||||
if (!candidates.value.length)
|
||||
return `Scanned ${new Date(props.status.discovery.scannedAt).toLocaleString()}: nothing found.`;
|
||||
return `Scanned ${new Date(props.status.discovery.scannedAt).toLocaleString()}.`;
|
||||
});
|
||||
|
||||
const label = (c: { name?: string | null; model?: string | null; vendor: string }) =>
|
||||
[c.name, c.model].filter(Boolean).join(" / ") || c.vendor;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="farm-status" :aria-label="`Farm ${farm.name}`">
|
||||
<span class="farm-status__name">{{ farm.name }}</span>
|
||||
<span class="farm-status__gateway" :class="`is-${gateway}`">Gateway {{ gateway }}</span>
|
||||
<details v-if="status" class="farm-status__discovery">
|
||||
<summary>Discovered printers ({{ candidates.length }})</summary>
|
||||
<p>{{ discoveryNote }}</p>
|
||||
<ul v-if="candidates.length">
|
||||
<li v-for="c in candidates" :key="c.address + (c.serial ?? '')">
|
||||
<strong>{{ label(c) }}</strong> - {{ c.vendor }} at {{ c.address }}
|
||||
<template v-if="c.matchedOn === 'serial'"> - already added</template>
|
||||
<template v-else-if="c.matchedOn === 'address'"> - same address as an added printer (check)</template>
|
||||
<template v-else> - not added yet</template>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.farm-status {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.farm-status__name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.farm-status__gateway.is-online {
|
||||
color: #35d07f;
|
||||
}
|
||||
|
||||
.farm-status__gateway.is-offline {
|
||||
color: #f2604c;
|
||||
}
|
||||
|
||||
.farm-status__gateway.is-checking {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.farm-status__discovery ul {
|
||||
margin: 0.25rem 0 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -4,4 +4,5 @@ export { default as TemperatureGraph } from "./components/TemperatureGraph.vue";
|
||||
export { default as WebRTCStreamView } from "./components/WebRTCStreamView.vue";
|
||||
export { default as JobList } from "./components/JobList.vue";
|
||||
export { default as JobUpload } from "./components/JobUpload.vue";
|
||||
export type { AppFarm, AppJob, AppPrinterState, AppTelemetryMessage, PrinterCommand } from "./types";
|
||||
export { default as FarmStatus } from "./components/FarmStatus.vue";
|
||||
export type { AppDiscoveredCandidate, AppFarm, AppFarmDiscovery, AppJob, AppPrinterState, AppTelemetryMessage, PrinterCommand } from "./types";
|
||||
|
||||
@@ -79,3 +79,25 @@ export interface AppJob {
|
||||
progressPct?: number | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* `GET /farms/:id/discovered-printers`. `discovery` is null when the gateway
|
||||
* has never reported a scan - almost always because discovery is off, which
|
||||
* is its default - and that is not the same as a report with no printers.
|
||||
*/
|
||||
export interface AppDiscoveredCandidate {
|
||||
address: string;
|
||||
vendor: string;
|
||||
name?: string | null;
|
||||
model?: string | null;
|
||||
serial?: string | null;
|
||||
/** The printer row this candidate looks like, and how sure that is. */
|
||||
existingPrinterId?: string | null;
|
||||
matchedOn?: "serial" | "address" | null;
|
||||
}
|
||||
|
||||
export interface AppFarmDiscovery {
|
||||
farmId: string;
|
||||
gatewayOnline: boolean;
|
||||
discovery: { scannedAt: string; printers: AppDiscoveredCandidate[] } | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user