diff --git a/.allmystuff-rev b/.allmystuff-rev index 0c1b4f0..eef44dc 100644 --- a/.allmystuff-rev +++ b/.allmystuff-rev @@ -1 +1 @@ -v0.2.45 +v0.2.46 diff --git a/gui/src-tauri/Cargo.toml b/gui/src-tauri/Cargo.toml index e607083..3939bab 100644 --- a/gui/src-tauri/Cargo.toml +++ b/gui/src-tauri/Cargo.toml @@ -51,11 +51,11 @@ tauri-plugin-single-instance = "2" # media planes, and the node control socket the client drives. Reused in "CEC # client (customer) mode". Git deps on the sibling branch that adds the CEC # node commands; not yet published, so this won't resolve in this sandbox. -allmystuff-node = { git = "https://github.com/mrjeeves/AllMyStuff", tag = "v0.2.45" } +allmystuff-node = { git = "https://github.com/mrjeeves/AllMyStuff", tag = "v0.2.46" } # The CEC wire contract + Support-ID derivation, and the three-choice consent # store — the customer's per-technician approvals. -allmystuff-cec-protocol = { git = "https://github.com/mrjeeves/AllMyStuff", tag = "v0.2.45" } -allmystuff-cec-consent = { git = "https://github.com/mrjeeves/AllMyStuff", tag = "v0.2.45" } +allmystuff-cec-protocol = { git = "https://github.com/mrjeeves/AllMyStuff", tag = "v0.2.46" } +allmystuff-cec-consent = { git = "https://github.com/mrjeeves/AllMyStuff", tag = "v0.2.46" } # The client's OWN OS-service installer (never AllMyStuff's), so installing the # background service never clobbers an existing AllMyStuff install. diff --git a/gui/src-tauri/src/main.rs b/gui/src-tauri/src/main.rs index 331f409..c1d99c2 100644 --- a/gui/src-tauri/src/main.rs +++ b/gui/src-tauri/src/main.rs @@ -386,6 +386,20 @@ async fn cec_grants(state: State<'_, AppState>) -> Result { Ok(v) } +/// What each connected technician is actually doing right now — the node's +/// `cec_viewing` projection `{ techs: { : { screen, control } } }`, +/// derived from live routes rather than session state. The pull half of the +/// `cec://viewing` event (which the generic pump already relays), so the +/// access list paints the chip correctly on a mid-session app start. +#[tauri::command] +async fn cec_viewing(state: State<'_, AppState>) -> Result { + state + .node + .request("cec_viewing", json!({})) + .await + .map_err(|e| e.to_string()) +} + /// Set this computer's friendly name (shown to the technician on the mesh). A /// convenience beyond the core dial/approve contract. #[tauri::command] @@ -960,6 +974,7 @@ fn run_gui() -> ExitCode { cec_revoke, cec_forget_node, cec_grants, + cec_viewing, cec_set_label, cec_chat_send, cec_chat_history, diff --git a/gui/src/store.svelte.ts b/gui/src/store.svelte.ts index 414ace4..041c577 100644 --- a/gui/src/store.svelte.ts +++ b/gui/src/store.svelte.ts @@ -25,6 +25,7 @@ import { cecDeny, cecForgetNode, cecGrants, + cecViewing, cecPending, cecRevoke, cecSetLabel, @@ -42,6 +43,7 @@ import { onCecHelp, onCecRequest, onCecSession, + onCecViewing, serviceInstall, serviceStatus, serviceStop, @@ -146,6 +148,12 @@ class CecStore { pending = $state([]); /** Live sessions keyed by session id. */ sessions = $state>({}); + /** What each technician's live routes actually carry right now (canonical + * tech id → screen/control), pushed by the node on every change. This is + * the truth the "Viewing/Controlling your screen" chip renders: a session + * outlives the console (chat rides it), so session state alone would keep + * the chip lit after the technician closed the console. */ + viewing = $state>({}); /** Standing approvals ("who can reach me"). */ grants = $state([]); service = $state(null); @@ -347,6 +355,7 @@ class CecStore { agent_name: g.agent_name || "A CEC technician", grant: g, live: live.find((s) => canonicalTech(s.tech) === key) ?? null, + viewing: this.viewing[key] ?? null, }; }); for (const s of live) { @@ -361,6 +370,7 @@ class CecStore { agent_name: s.agent_name || "A CEC technician", grant: null, live: s, + viewing: this.viewing[key] ?? null, }); } return rows; @@ -397,6 +407,7 @@ class CecStore { this.unlisteners.push(await onCecRequest((r) => this.onRequest(r))); this.unlisteners.push(await onCecSession((s) => this.onSession(s))); this.unlisteners.push(await onCecGrants((g) => (this.grants = g))); + this.unlisteners.push(await onCecViewing((v) => (this.viewing = v))); this.unlisteners.push( await onCecChat((e) => this.appendChat(e.peer, e.message)), ); @@ -520,6 +531,9 @@ class CecStore { this.status = await cecStatus(); this.pending = await cecPending(); this.grants = await cecGrants(); + // Pull the live viewing map too, so an app that starts (or reconnects) + // mid-session paints the chip without waiting for the next transition. + this.viewing = await cecViewing(); // The node is the truth for the ask (it withdraws it itself on approval, // and a restart drops it) — mirror it whenever the status lands, but never // mid-request: an in-flight ask/cancel (busy) owns the flag, so a status diff --git a/gui/src/tauri.ts b/gui/src/tauri.ts index 4efe279..25dc6a0 100644 --- a/gui/src/tauri.ts +++ b/gui/src/tauri.ts @@ -213,6 +213,17 @@ export async function cecGrants(): Promise { return Array.isArray(r) ? r : []; } +/** What each technician's live routes actually carry right now — screen + * (viewing) and control (driving input) — keyed by canonical tech id. This, + * not session state, is what the "Viewing your screen" chip keys on: a + * session outlives the console (chat rides it), so session state over-claims. + * Empty in web mode / on error. */ +export type ViewingMap = Record; +export async function cecViewing(): Promise { + const r = await tryInvoke<{ techs?: ViewingMap }>("cec_viewing"); + return r?.techs ?? {}; +} + /** Set this computer's friendly name (shown to the technician on the mesh). * A convenience beyond the core dial/approve contract; see README. */ export function cecSetLabel(label: string): Promise { @@ -333,6 +344,19 @@ export async function onCecGrants( ); } +/** What technicians' live routes carry changed (console opened/closed, control + * granted route came up/down) — repaint the Viewing/Controlling chip. The + * node's consent sweep pushes the whole map on every transition. */ +export async function onCecViewing( + cb: (viewing: ViewingMap) => void, +): Promise<() => void> { + if (!isTauri()) return () => {}; + const { listen } = await import("@tauri-apps/api/event"); + return listen<{ techs?: ViewingMap }>("cec://viewing", (e) => + cb(e.payload.techs ?? {}), + ); +} + /** A chat line landed on some technician's transcript — a received line, or the * echo of one we just sent. `peer` is the technician's canonical device id (the * thread key); `message` is the line. Drives the chat panel live. */ diff --git a/gui/src/types.ts b/gui/src/types.ts index e690fe1..3211695 100644 --- a/gui/src/types.ts +++ b/gui/src/types.ts @@ -159,6 +159,10 @@ export interface AccessRow { grant: Grant | null; /** The technician's live session right now, or null when not connected. */ live: LiveSession | null; + /** What their live routes actually carry right now — the chip's truth. A + * connected session with `viewing` null/false means chat only (or the + * console was closed): no "Viewing your screen". */ + viewing: { screen: boolean; control: boolean } | null; } /** The OS background-service status, from the `cec-support-service` crate. */ diff --git a/gui/src/ui/AccessList.svelte b/gui/src/ui/AccessList.svelte index 5f720c2..79ac3cf 100644 --- a/gui/src/ui/AccessList.svelte +++ b/gui/src/ui/AccessList.svelte @@ -95,9 +95,14 @@ {#if r.grant && remaining(r.grant)} {remaining(r.grant)} {/if} - {#if dot === "live"} + {#if r.viewing?.screen} + - {r.live?.want_control ? "Controlling your screen" : "Viewing your screen"} + {r.viewing.control ? "Controlling your screen" : "Viewing your screen"} {/if}