Files
app/frontend/scripts/verify-endpoints.mjs
T
stroblmeandClaude Fable 5 75c26ef000 Say what caused a value, and draw what is not a node
Moving a dashboard slider lit up an edge between two nodes that had done
nothing. The canvas pulsed on the message's timestamp alone, and a message
has no idea who published it — so it credited whichever node happened to
be drawn as a producer.

That was never only about dashboards. Two nodes producing one message
pulsed both their edges whichever fired, and a message produced in another
flow changed with nothing on screen to account for it at all.

Values now carry their cause: a node, a dashboard widget, another flow, an
agent or an API caller. An edge pulses only for the producer that actually
published, and the edge inspector says where a value came from when it did
not come from a node.

What is not a node in this flow is now drawn as one — a label rather than
a card, because a dashboard with twenty tiles would otherwise bury the
logic the canvas exists to show. That covers cross-flow wiring too, which
is the link in/out affordance that has been missing.

They are never part of the document. They join at render, after everything
that reads or writes the canvas nodes, so an autosave, an undo or a delete
cannot reach them — with a Playwright test that drags a node and asserts
the stored flow still holds exactly what it did.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 15:51:54 +02:00

83 lines
2.7 KiB
JavaScript

/** The reported bug, end to end: move the real slider, watch the canvas. */
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()
async function login(page) {
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 })
}
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 login(page)
await page.goto(`${APP_URL}/flows/probe`, { waitUntil: "networkidle" })
await page.keyboard.press("Escape")
await page.waitForTimeout(2000)
await page.screenshot({ path: `${dir}/endpoints-canvas.png` })
if (theme === "light") {
// Watch the canvas while a second tab moves the real slider.
const watch = page.evaluate(async () => {
const seen = new Set()
const scan = () => {
for (const el of document.querySelectorAll(".edge-live")) {
const holder = el.closest("[data-id]")
if (holder) seen.add(holder.getAttribute("data-id"))
}
}
const observer = new MutationObserver(scan)
observer.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ["class"],
})
await new Promise((r) => setTimeout(r, 6000))
observer.disconnect()
return [...seen]
})
const other = await context.newPage()
await other.goto(`${APP_URL}/dashboards/probe`, {
waitUntil: "networkidle",
})
await other.waitForTimeout(1200)
const slider = other.locator('input[type="range"]').first()
await slider.click()
for (let i = 0; i < 3; i++) {
await slider.press("ArrowRight")
await other.waitForTimeout(700)
}
const pulsed = await watch
console.log(
" edges that pulsed while the slider moved:",
JSON.stringify(pulsed),
)
await other.close()
await page.waitForTimeout(500)
await page.screenshot({ path: `${dir}/endpoints-after-slider.png` })
}
console.log(` ${theme}: done`)
await context.close()
}
await browser.close()