From 524139705edb33e3397411e9c25fdaecb6060e42 Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Sat, 2 May 2026 21:44:28 -0400 Subject: [PATCH] refactor(web): redesign ConnectingOverlay as hero card + fix early visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes: Visibility fix — overlay now reads `savedConnections` for any entry whose status is "connecting" or "configuring". The previous code keyed off `device.connectionPhase` from the active deviceStore entry, but that entry doesn't exist until partway through `setupMeshDevice` — the overlay was hidden during transport-open + storage-open (often the slowest part of the connect flow). Visual redo (option C — hero card with radial pings): - Center: transport-type icon (Globe / Bluetooth / Cable) on a gradient disc with two concentric ping rings. - Below: device name + phase label with small spinner. - 2x2 grid of stat chips (Identity / Metadata / Channels / Nodes) that flip from gray (dashed circle) to emerald (check or count) as the SDK reports each piece arriving. - Slate-to-black gradient panel; non-dismissable while active. - i18n strings under `connections.overlay.*` updated to match. --- .../public/i18n/locales/en/connections.json | 22 +- apps/web/src/components/ConnectingOverlay.tsx | 297 +++++++----------- 2 files changed, 117 insertions(+), 202 deletions(-) diff --git a/apps/web/public/i18n/locales/en/connections.json b/apps/web/public/i18n/locales/en/connections.json index 375acee0..70274363 100644 --- a/apps/web/public/i18n/locales/en/connections.json +++ b/apps/web/public/i18n/locales/en/connections.json @@ -56,18 +56,16 @@ "updatedByName": "{{name}} has been updated." }, "overlay": { - "log": { - "header": "meshtastic — connect", - "transportOpening": "Opening transport…", - "transportReady": "Transport ready", - "requestingConfig": "Requesting configuration", - "configReceived": "Received config section ({{n}})", - "moduleReceived": "Received module config ({{n}})", - "channelReceived": "Received channel ({{n}})", - "nodeReceived": "Received node info ({{n}})", - "myInfo": "Identity received", - "metadata": "Metadata received", - "configured": "Configuration complete" + "srTitle": "Connecting to device", + "phase": { + "connecting": "Opening transport…", + "configuring": "Streaming configuration…" + }, + "chip": { + "identity": "Identity", + "metadata": "Metadata", + "channels": "Channels", + "nodes": "Nodes" } } } diff --git a/apps/web/src/components/ConnectingOverlay.tsx b/apps/web/src/components/ConnectingOverlay.tsx index 83f10689..84aad887 100644 --- a/apps/web/src/components/ConnectingOverlay.tsx +++ b/apps/web/src/components/ConnectingOverlay.tsx @@ -1,219 +1,136 @@ import { Dialog, DialogContent, DialogTitle } from "@components/UI/Dialog.tsx"; -import { useAppStore, useDeviceStore } from "@core/stores"; -import type { ConnectionProgressCounters } from "@meshtastic/sdk"; +import { useDeviceStore } from "@core/stores"; +import { cn } from "@core/utils/cn.ts"; import { useConnectionProgress } from "@meshtastic/sdk-react"; -import type { ReactElement } from "react"; -import { useEffect, useRef, useState } from "react"; +import { Bluetooth, Cable, CheckCircle2, Globe, Loader2 } from "lucide-react"; +import type { ComponentType, ReactElement } from "react"; import { useTranslation } from "react-i18next"; -type LineKind = "ok" | "active" | "info"; -interface LogLine { - id: number; - ts: number; - kind: LineKind; - text: string; -} - -const SYMBOL: Record = { - ok: "✓", - active: "→", - info: "•", -}; - -const COLOR: Record = { - ok: "text-emerald-400", - active: "text-sky-300", - info: "text-zinc-400", +const TRANSPORT_ICON: Record< + "http" | "bluetooth" | "serial", + ComponentType<{ className?: string }> +> = { + http: Globe, + bluetooth: Bluetooth, + serial: Cable, }; /** - * Full-screen terminal-style log overlay shown while the active device's - * connection phase is "connecting" or "configuring". Lines are derived - * from `device.connectionPhase` transitions and `MeshClient.progress` - * counter bumps — pure consumer of domain signals. + * Hero-card connect overlay. Visible while any saved connection's status + * is "connecting" or "configuring" — driven from the saved-connection + * status field rather than `device.connectionPhase` so the overlay shows + * during transport-open + storage-open (the device entry doesn't exist + * yet during those phases). * - * Auto-scrolls to the latest line. Auto-dismisses once phase flips to - * "configured" / "disconnected" / "error". + * Center: device name + transport icon with an animated radial pulse. + * Surrounding: 4 stat chips (Identity / Channels / Nodes / Metadata) + * that flip from gray to green as the SDK reports each piece arriving. + * Auto-dismisses on "configured" (with a brief success state) or + * "disconnected" / "error". */ export const ConnectingOverlay = (): ReactElement | null => { - const { selectedDeviceId } = useAppStore(); - const { getDevice } = useDeviceStore(); - const device = getDevice(selectedDeviceId); - const phase = device?.connectionPhase ?? "disconnected"; + const savedConnections = useDeviceStore((s) => s.savedConnections); const progress = useConnectionProgress(); const { t } = useTranslation("connections"); - const [lines, setLines] = useState([]); - const idRef = useRef(0); - const lastPhaseRef = useRef("disconnected"); - const lastCountersRef = useRef(undefined); - const scrollRef = useRef(null); + const active = savedConnections.find( + (c) => c.status === "connecting" || c.status === "configuring", + ); - const visible = phase === "connecting" || phase === "configuring"; + if (!active) return null; - // Reset the log when a fresh connect cycle starts (idle → connecting). - useEffect(() => { - if (phase === "connecting" && lastPhaseRef.current !== "connecting") { - setLines([]); - idRef.current = 0; - lastCountersRef.current = undefined; - } - lastPhaseRef.current = phase; - }, [phase]); + const Icon = TRANSPORT_ICON[active.type]; + const counters = + progress.phase === "configuring" || progress.phase === "configured" + ? progress.received + : { config: 0, modules: 0, channels: 0, nodes: 0, myInfo: false, metadata: false }; - // High-level phase transitions → log lines. - useEffect(() => { - if (phase === "connecting") { - append("active", t("overlay.log.transportOpening", { defaultValue: "Opening transport…" })); - } else if (phase === "configuring") { - append("ok", t("overlay.log.transportReady", { defaultValue: "Transport ready" })); - append( - "active", - t("overlay.log.requestingConfig", { defaultValue: "Requesting configuration" }), - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [phase]); - - // Counter bumps → per-event lines. - useEffect(() => { - if (progress.phase !== "configuring") { - if (progress.phase === "configured") { - append("ok", t("overlay.log.configured", { defaultValue: "Configuration complete" })); - } - return; - } - const cur = progress.received; - const prev = lastCountersRef.current ?? { - config: 0, - modules: 0, - channels: 0, - nodes: 0, - myInfo: false, - metadata: false, - }; - if (cur.config > prev.config) { - append( - "info", - t("overlay.log.configReceived", { - n: cur.config, - defaultValue: "Received config section ({{n}})", - }), - ); - } - if (cur.modules > prev.modules) { - append( - "info", - t("overlay.log.moduleReceived", { - n: cur.modules, - defaultValue: "Received module config ({{n}})", - }), - ); - } - if (cur.channels > prev.channels) { - append( - "info", - t("overlay.log.channelReceived", { - n: cur.channels, - defaultValue: "Received channel ({{n}})", - }), - ); - } - if (cur.nodes > prev.nodes) { - append( - "info", - t("overlay.log.nodeReceived", { - n: cur.nodes, - defaultValue: "Received node info ({{n}})", - }), - ); - } - if (cur.myInfo && !prev.myInfo) { - append("ok", t("overlay.log.myInfo", { defaultValue: "Identity received" })); - } - if (cur.metadata && !prev.metadata) { - append("ok", t("overlay.log.metadata", { defaultValue: "Metadata received" })); - } - lastCountersRef.current = cur; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [progress]); - - // Auto-scroll to bottom on new line. - useEffect(() => { - const el = scrollRef.current; - if (el) el.scrollTop = el.scrollHeight; - }, [lines]); - - function append(kind: LineKind, text: string): void { - setLines((prev) => [...prev, { id: ++idRef.current, ts: Date.now(), kind, text }]); - } - - if (!visible) return null; - - const start = lines[0]?.ts ?? Date.now(); + const isConnecting = active.status === "connecting"; + const phaseLabel = isConnecting + ? t("overlay.phase.connecting", { defaultValue: "Opening transport…" }) + : t("overlay.phase.configuring", { defaultValue: "Streaming configuration…" }); return ( e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} > -
-
- - - -
- - {t("overlay.log.header", { defaultValue: "meshtastic — connect" })} - - - - {phase} - -
+ + {t("overlay.srTitle", { defaultValue: "Connecting to device" })} + -
- {lines.length === 0 ? ( -
- ) : ( - lines.map((line, i) => { - const isLast = i === lines.length - 1; - const symbol = isLast && line.kind === "active" ? "→" : SYMBOL[line.kind]; - return ( -
- - {String(((line.ts - start) / 1000).toFixed(2)).padStart(6, " ")}s - - {symbol} - - {line.text} - {isLast && line.kind === "active" && ( - - )} - -
- ); - }) - )} +
+ {/* Hero icon with radial pings */} +
+ + + + +
+ + {/* Name + phase */} +
+
{active.name}
+
+ + {phaseLabel} +
+
+ + {/* Stat chips — 2x2 grid */} +
+ + + 0 && progress.phase === "configured"} + /> + 0 && progress.phase === "configured"} + /> +
); }; + +interface ChipProps { + label: string; + count?: number; + done: boolean; +} + +function Chip({ label, count, done }: ChipProps): ReactElement { + const filled = count !== undefined ? count > 0 : done; + return ( +
+ {label} + {count !== undefined ? ( + {count} + ) : done ? ( + + ) : ( + + )} +
+ ); +}