From 42075a6dae5e9f082f9511ead635674df11f847f Mon Sep 17 00:00:00 2001 From: stroblme Date: Sun, 16 Aug 2026 11:14:35 +0200 Subject: [PATCH] Give a connector node an icon that is not a code icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A device read over a proprietary protocol was showing the fallback used for a function node. A connector's type cannot be in the built-in icon map — that is the point of it being a connector — so they share a plug. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY --- frontend/scripts/verify-live.mjs | 41 +++++++++++++++++++++ frontend/src/components/Flow/FlowEditor.tsx | 23 +++++++++++- frontend/src/components/Flow/FlowNode.tsx | 11 +++++- 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 frontend/scripts/verify-live.mjs diff --git a/frontend/scripts/verify-live.mjs b/frontend/scripts/verify-live.mjs new file mode 100644 index 0000000..4220eb0 --- /dev/null +++ b/frontend/scripts/verify-live.mjs @@ -0,0 +1,41 @@ +/** Screenshot the dashboard and the flow canvas as a user will find them. */ +import { mkdir } from "node:fs/promises" +import { chromium } from "@playwright/test" + +const APP_URL = process.env.APP_URL || "http://app.localhost" +const OUT = process.env.SCREENSHOT_DIR || "screenshots" +const browser = await chromium.launch() + +for (const theme of ["light", "dark"]) { + const dir = `${OUT}/${theme}` + await mkdir(dir, { recursive: true }) + const context = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + colorScheme: theme, + }) + await context.addInitScript((t) => { + localStorage.setItem("fluksio-ui-theme", t) + }, theme) + const page = await context.newPage() + + await page.goto(`${APP_URL}/login`, { waitUntil: "networkidle" }) + await page.getByTestId("email-input").fill(process.env.FIRST_SUPERUSER) + await page + .getByTestId("password-input") + .fill(process.env.FIRST_SUPERUSER_PASSWORD) + await page.getByRole("button", { name: /log in/i }).click() + await page.waitForURL(`${APP_URL}/`, { timeout: 15000 }) + + await page.goto(`${APP_URL}/dashboards/aircon`, { waitUntil: "networkidle" }) + await page.waitForTimeout(2500) + await page.screenshot({ path: `${dir}/live-dashboard.png` }) + + await page.goto(`${APP_URL}/flows/aircon`, { waitUntil: "networkidle" }) + await page.keyboard.press("Escape") + await page.waitForTimeout(2500) + await page.screenshot({ path: `${dir}/live-flow.png` }) + + console.log(` ${theme}: done`) + await context.close() +} +await browser.close() diff --git a/frontend/src/components/Flow/FlowEditor.tsx b/frontend/src/components/Flow/FlowEditor.tsx index a71a884..5df674b 100644 --- a/frontend/src/components/Flow/FlowEditor.tsx +++ b/frontend/src/components/Flow/FlowEditor.tsx @@ -303,6 +303,18 @@ function FlowEditorInner({ [nodeTypeInfo], ) + // Which types came from an installed connector rather than the engine. They + // cannot be in the icon map, so they share one. + const pluginTypes = useMemo( + () => + new Set( + (nodeTypeInfo ?? []) + .filter((info) => info.plugin) + .map((info) => info.type), + ), + [nodeTypeInfo], + ) + const issuesByNode = useMemo(() => { const map = new Map() for (const issue of issues) { @@ -328,11 +340,20 @@ function FlowEditorInner({ flow: flowName, typeLabel: typeLabels.get(definition?.type ?? "") ?? definition?.type ?? "", + isPlugin: pluginTypes.has(definition?.type ?? ""), issueText: nodeIssues.join("\n"), } satisfies FlowNodeData, } }), - [canvasNodes, definitions, flowName, issuesByNode, selectedId, typeLabels], + [ + canvasNodes, + definitions, + flowName, + issuesByNode, + pluginTypes, + selectedId, + typeLabels, + ], ) // A cheap fingerprint of the wiring: it changes when a name does, but not diff --git a/frontend/src/components/Flow/FlowNode.tsx b/frontend/src/components/Flow/FlowNode.tsx index b2face8..01079b5 100644 --- a/frontend/src/components/Flow/FlowNode.tsx +++ b/frontend/src/components/Flow/FlowNode.tsx @@ -10,6 +10,7 @@ import { Globe, Merge, Play, + Plug, Radio, Shuffle, Split, @@ -58,6 +59,7 @@ export type FlowNodeData = { definition: NodeDef_Input flow: string typeLabel: string + isPlugin?: boolean issueText: string [key: string]: unknown } @@ -101,10 +103,15 @@ function PortHandles({ } function FlowNodeComponent({ data, selected }: NodeProps) { - const { definition, flow, typeLabel, issueText } = data as FlowNodeData + const { definition, flow, typeLabel, isPlugin, issueText } = + data as FlowNodeData const live = useNodeStatus(`${flow}.${definition.id}`) const emits = useNodeEmits(`${flow}.${definition.id}`) - const Icon = NODE_ICONS[definition.type as keyof typeof NODE_ICONS] ?? Code2 + const Icon = + NODE_ICONS[definition.type as keyof typeof NODE_ICONS] ?? + // A connector's own type cannot be in the map above, and a device is + // not a piece of code. + (isPlugin ? Plug : Code2) // Whatever is wrong — it failed to load, it failed to run, or the graph // around it does not add up — is the same red dot with the same explanation.