diff --git a/frontend/src/components/Flow/BrainNode.tsx b/frontend/src/components/Flow/BrainNode.tsx
index 7e32b1f..7de099d 100644
--- a/frontend/src/components/Flow/BrainNode.tsx
+++ b/frontend/src/components/Flow/BrainNode.tsx
@@ -13,13 +13,28 @@ export type BrainNodeData = {
flows: string[]
/** Circle diameter in pixels, from how many neurons this one is wired to. */
size: number
+ /** Why this neuron cannot run, if validation found something. */
+ issue?: string | null
[key: string]: unknown
}
+/**
+ * Ring thickness as a share of the diameter, measured off the brand mark: a
+ * 1.4545 stroke on a 6.3 outer diameter in `fluksio-light.svg`.
+ *
+ * A share rather than a fixed width because the neurons run from 32px to 96px:
+ * the same ring in pixels would swallow the smallest and read as a hairline on
+ * the largest, and it is the proportion that makes a circle look like the mark.
+ */
+const RING = 0.23
+
function BrainNodeComponent({ data }: NodeProps) {
- const { label, kind, members, flows, size } = data as BrainNodeData
+ const { label, kind, members, flows, size, issue } = data as BrainNodeData
const emits = useGroupEmits(members)
const failed = useGroupError(members)
+ // One ring for both kinds of trouble: a run that broke and wiring that can
+ // never run are the same answer to "can I trust this neuron right now".
+ const problem = failed || !!issue
// Brightest right after a publish, then left to decay. Only the peak needs
// holding: the floor is the resting style, so letting go is the whole decay.
const [firing, setFiring] = useState(false)
@@ -34,14 +49,16 @@ function BrainNodeComponent({ data }: NodeProps) {
return (
{/* Remounting on each emit is what restarts the animation. */}
{emits > 0 ? : null}
@@ -63,15 +80,23 @@ function BrainNodeComponent({ data }: NodeProps) {
{/*
* A name under every circle is what makes the graph unreadable, so only
- * the neuron under the pointer or the keyboard says what it is — except a
- * failing one, since colour is never the only carrier of a status.
+ * the neuron under the pointer or the keyboard says what it is — except
+ * one with something wrong, since colour is never the only carrier of a
+ * status. Two words rather than the reason itself: a cycle names every
+ * node in it, which is a sentence no label under a 32px circle can hold,
+ * so the phrase matches what Home says and the full text stays in the
+ * tooltip.
*/}
-
+
{label}
- {failed ? (
- failed
+ {/* The label box is only as wide as the circle it hangs under, so two
+ words break onto two lines unless told not to. */}
+ {problem ? (
+
+ {failed ? "failed" : "cannot run"}
+
) : null}
diff --git a/frontend/src/components/Flow/BrainView.tsx b/frontend/src/components/Flow/BrainView.tsx
index 8658941..d256c56 100644
--- a/frontend/src/components/Flow/BrainView.tsx
+++ b/frontend/src/components/Flow/BrainView.tsx
@@ -129,6 +129,7 @@ function build(graph: BrainGraph): { nodes: Node[]; edges: Edge[] } {
members: source.members ?? [],
flows: source.flows ?? [],
size: node.size,
+ issue: source.issue,
} satisfies BrainNodeData,
}
}),
diff --git a/frontend/src/components/Flow/flow.css b/frontend/src/components/Flow/flow.css
index 48487ea..148bc59 100644
--- a/frontend/src/components/Flow/flow.css
+++ b/frontend/src/components/Flow/flow.css
@@ -140,41 +140,31 @@
}
/*
- * A neuron is a filled disc rather than an outline: a `--card` circle on
- * `--background` is a hairline in light and all but nothing in dark.
+ * A neuron is the node of the brand mark: a disc of the identity blue inside a
+ * thick ring, wired to its neighbours. The ring thickness is a share of the
+ * diameter and lives in `BrainNode`, which is where the diameter is known.
*
- * The fill is neutral, and stays neutral — status still speaks only through the
- * border and the word under it. What its lightness carries is how recently this
- * page saw the neuron publish: brightest just after it fires, decaying to a
- * floor it never drops below, so a graph left open sorts itself into what is
- * busy and what is not. A fresh page has seen nothing either way, so everything
- * starts at `.brain-idle`, halfway, claiming no history it does not have.
+ * The fill is identity and carries no status — that is the ring's job, and the
+ * word under it. What the fill's lightness carries is how recently this page
+ * saw the neuron publish: brightest just after it fires, decaying to a floor it
+ * never drops below, so a graph left open sorts itself into what is busy and
+ * what is not. The floor stays blue enough to still read as the mark. A fresh
+ * page has seen nothing either way, so everything starts at `.brain-idle`,
+ * halfway, claiming no history it does not have.
*/
.brain-cell {
cursor: pointer;
- background-color: color-mix(
- in srgb,
- var(--muted-foreground) 28%,
- transparent
- );
+ background-color: color-mix(in srgb, var(--primary) 50%, transparent);
transition: border-color var(--duration-base) var(--ease-standard);
}
.brain-cell.brain-idle {
- background-color: color-mix(
- in srgb,
- var(--muted-foreground) 42%,
- transparent
- );
+ background-color: color-mix(in srgb, var(--primary) 65%, transparent);
}
/* Snapped into, decayed out of — never the other way round. */
.brain-cell.brain-hot {
- background-color: color-mix(
- in srgb,
- var(--muted-foreground) 65%,
- transparent
- );
+ background-color: color-mix(in srgb, var(--primary) 90%, transparent);
transition: none;
}
@@ -186,9 +176,15 @@
}
}
+/*
+ * Outside the ring rather than in it: the ring is what says whether this neuron
+ * can run, and the disc is already `--primary`, so recolouring either to mark
+ * the pointer would either overwrite a status or vanish into the fill.
+ */
.brain-cell:hover,
.react-flow__node:focus-visible .brain-cell {
- border-color: var(--primary);
+ outline: 2px solid var(--ring);
+ outline-offset: 2px;
}
/*
diff --git a/frontend/src/components/Health/HealthOverview.tsx b/frontend/src/components/Health/HealthOverview.tsx
index 1019414..2088225 100644
--- a/frontend/src/components/Health/HealthOverview.tsx
+++ b/frontend/src/components/Health/HealthOverview.tsx
@@ -117,12 +117,16 @@ export function HealthOverview({