The console could only watch. A printer card now offers what
POST /printers/:id/commands accepts and a person should press: pause
while printing, resume while paused, and a fan percentage.
Stop is the one command with no undo (the printer reports FAILED), so it
sits apart from Pause and needs a second, named confirmation
("Cancel <job>? ... cannot be resumed"). Offline and never-reported
printers get no controls, since every command would be refused as
unreachable. A successful reply only means the printer accepted the
command, so the notice says to watch the status line rather than claiming
the state changed. The backend's own error message is shown for a 403, 409
or 502.
PrinterControls takes a `send` function like the card takes `signal`, so
@continuum/ui still knows nothing of the API; no function, no controls.
send_gcode_line and start_print are deliberately not offered here.
Verified in real Chrome against the live stack with the command requests
intercepted in the browser (the H2C was mid-print): controls render per
state, Stop shows its confirmation and Keep printing sends nothing, the
body is {action:"set_fan_speed",percent:100}, a 409 surfaces its message,
no page errors. pnpm -r typecheck is clean.
158 lines
5.1 KiB
Vue
158 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from "vue";
|
|
import type { AppPrinterState, PrinterCommand } from "../types";
|
|
import PrinterControls from "./PrinterControls.vue";
|
|
import TemperatureGraph from "./TemperatureGraph.vue";
|
|
import WebRTCStreamView from "./WebRTCStreamView.vue";
|
|
|
|
/**
|
|
* Where this printer's camera can be watched, from
|
|
* `GET /printers/:id/stream`.
|
|
*
|
|
* Passed in rather than fetched here, matching `printer`: this package is
|
|
* plain Vue with no knowledge of the API or of Nuxt, and keeping the fetching
|
|
* in the app is what lets these components be rendered from a test or the
|
|
* desktop shell without a server behind them.
|
|
*
|
|
* Undefined for most printers and that is expected - see
|
|
* `usePrinterStream`. The player is simply not rendered.
|
|
*/
|
|
interface PrinterStream {
|
|
cameraId: string;
|
|
iceServers: RTCIceServer[];
|
|
turnConfigured: boolean;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
/**
|
|
* The printer as it should be drawn right now. One prop, not a fetched row
|
|
* plus a `live` overlay: the REST row and the websocket push are the same
|
|
* shape by design, so the page replaces the one with the other before
|
|
* handing it down. That keeps this package ignorant of stores and the API,
|
|
* which is what lets it render from a test or the Tauri shell with no
|
|
* server - and means there is no second place to decide which of two
|
|
* values wins.
|
|
*/
|
|
printer: AppPrinterState;
|
|
stream?: PrinterStream | null;
|
|
/**
|
|
* Exchanges an offer SDP for an answer SDP, via the backend's relay.
|
|
*
|
|
* Passed in rather than called here, like `stream` itself: this package is
|
|
* plain Vue with no knowledge of the API or of Nuxt, which is what lets
|
|
* these components render from a test or the desktop shell with no server.
|
|
* No function means no player, rather than a player that cannot signal.
|
|
*/
|
|
signal?: (offerSdp: string) => Promise<string>;
|
|
/**
|
|
* Delivers a command to this printer. No function means no controls, so a
|
|
* read-only embedding of the card cannot offer buttons that do nothing.
|
|
*/
|
|
send?: (command: PrinterCommand) => Promise<void>;
|
|
}>();
|
|
|
|
// `status` is open-ended and null for a printer that has never reported.
|
|
// "unknown" is the honest word for that; the old fallback of "offline"
|
|
// asserted something nobody had observed.
|
|
const status = computed(() => props.printer.status ?? "unknown");
|
|
|
|
// Class-safe form of an arbitrary backend string.
|
|
const statusClass = computed(() => `is-${status.value.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`);
|
|
|
|
const hasTemperature = computed(
|
|
() => props.printer.nozzleTempC != null || props.printer.bedTempC != null,
|
|
);
|
|
|
|
// `lastSeenAt` is what separates "idle" from "nobody has heard from it in an
|
|
// hour", which `status` alone cannot say. Shown as an absolute time rather
|
|
// than "5 minutes ago": a relative label needs a ticking clock to stay true,
|
|
// and a card that says "just now" for an hour is worse than no label.
|
|
const lastSeen = computed(() => {
|
|
const at = props.printer.lastSeenAt;
|
|
return at ? new Date(at).toLocaleString() : null;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<article class="printer-card" :class="statusClass">
|
|
<header>
|
|
<h3>{{ printer.name }}</h3>
|
|
<span class="printer-card__model">{{ printer.vendor }}</span>
|
|
</header>
|
|
|
|
<p class="printer-card__state">
|
|
{{ status }}
|
|
<span v-if="printer.vendorState" class="printer-card__model">({{ printer.vendorState }})</span>
|
|
</p>
|
|
<p class="printer-card__model">
|
|
{{ lastSeen ? `Last heard from ${lastSeen}` : "Never reported" }}
|
|
</p>
|
|
|
|
<WebRTCStreamView
|
|
v-if="stream && signal"
|
|
:camera-id="stream.cameraId"
|
|
:ice-servers="stream.iceServers"
|
|
:turn-configured="stream.turnConfigured"
|
|
:signal="signal"
|
|
/>
|
|
|
|
<TemperatureGraph
|
|
v-if="hasTemperature"
|
|
:nozzle-temp-c="printer.nozzleTempC"
|
|
:bed-temp-c="printer.bedTempC"
|
|
/>
|
|
|
|
<!-- Omitted when null: an empty bar reads as 0% complete. -->
|
|
<progress v-if="printer.progressPercent != null" :value="printer.progressPercent" max="100" />
|
|
|
|
<PrinterControls v-if="send" :printer="printer" :send="send" />
|
|
</article>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.printer-card {
|
|
border: 1px solid #1f232c;
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
background: #12151c;
|
|
}
|
|
|
|
.printer-card__model {
|
|
opacity: 0.6;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/*
|
|
* One rule per value of the backend's `printer_status` enum - offline, idle,
|
|
* printing, paused, error, maintenance - plus the "unknown" this card
|
|
* substitutes for a null status. The enum was only two thirds covered, so a
|
|
* paused printer and one in maintenance drew in the default colour and read
|
|
* as ordinary. `status` is not a closed enum on the wire, so anything new
|
|
* still falls back to that default rather than breaking.
|
|
*/
|
|
.printer-card.is-printing .printer-card__state {
|
|
color: #35d07f;
|
|
}
|
|
|
|
.printer-card.is-error .printer-card__state {
|
|
color: #f2604c;
|
|
}
|
|
|
|
.printer-card.is-paused .printer-card__state {
|
|
color: #f0b429;
|
|
}
|
|
|
|
.printer-card.is-maintenance .printer-card__state {
|
|
color: #9f7aea;
|
|
}
|
|
|
|
.printer-card.is-offline .printer-card__state,
|
|
.printer-card.is-unknown .printer-card__state {
|
|
color: #6b7280;
|
|
}
|
|
|
|
progress {
|
|
width: 100%;
|
|
}
|
|
</style>
|