Round-2 spec dropped the input handle from source nodes and the output
handle from sink nodes so the topology's endpoints read as terminal at a
glance. But source/sink is *computed from graph topology* — a node with no
inbound edges is a "source". That meant every freshly-created (still-
unconnected) node classified as a source, lost its input handle, and could
never receive an edge — the graph became unusable the moment you tried to
build one from scratch.
Type is already carried by the SOURCE/ROUTER/SINK eyebrow + the top-border
weight on sources + the inverse fill on sinks. The handle presence was a
redundant fourth signal that broke basic functionality. Always render both;
keep the rest of the type language intact.
Applies the tesanti brand pack (docs/brand/tesanti.md) across every admin
surface, replacing the old slate + orange Tailwind defaults with black + white
+ signal red, three variable-weight fonts (Space Grotesk / Inter / JetBrains
Mono), mono eyebrows above every heading, and a hairline-first (not shadow-
first) elevation language.
New primitives:
- Wordmark — "VolcanicCore" in Space Grotesk 500 with the red signal dot.
- Toast — provider + useToast() hook + 360px surface-2 card with slide+
fade. Replaces every alert() (edge/node mutation failures).
- ConfirmDialog — provider + promise-based useConfirm() covering optional
type-to-confirm gating. Replaces every confirm()/prompt()
(user delete, node remove, edge delete). Zero browser dialogs
left in the app.
- RouteEdge — custom React Flow edge that keeps the canvas quiet by default
and surfaces the mono "filter → transform" label as an opaque
pill only when the edge is selected.
Graph marquee:
- vc-node states (source / router / sink) differentiated by top-border
weight, inverse fill for sinks, and eyebrow — never by color. Red is
reserved for selected + error.
- Node width 240px; edge label overlap eliminated.
- Zero-node empty state with a red primary "Add node" CTA.
Screen coverage:
- Login — centered wordmark + "NATS ROUTING CONTROL" eyebrow.
- AppShell — 240px sidebar, numbered mono eyebrows, 2px red left-border
on the active item, Lucide log-out on sign-out.
- Users — mono-headed table, outline-destructive delete, chips.
- Server — surface-2 card headers, hairline dividers, outline OK/UP
status chips.
- Audit — full mono table.
- Every error banner picked up the alert-triangle prefix and full accent
border.
Icons: added lucide-react and standardized on it (X, AlertTriangle, Check,
Share2, LogOut). Every unicode × / control glyph swapped out.
Fonts self-hosted via @fontsource-variable/{space-grotesk,inter,
jetbrains-mono}; no external requests, CSP-friendly.
Three real bugs and one data-model quirk showed up while wiring the
Examples/NodeExample smoke test to production. Adding them as design
invariants so future sessions don't re-hit them:
- JS API subjects belong in publish.allow (request/reply direction)
- SIGHUP must go through libc.kill; procps isn't in the base image
- Node display name is not the JetStream stream name
Also add the Examples path to the repo layout, mention the end-to-end
smoke procedure, and gitignore .playwright-mcp/.
The node's display name is "Test_2" but its JetStream stream name (chosen
by the operator, constrained to [A-Za-z0-9_-]+) is "Test2". NATS API
calls target streamName, not the node's display name.
The aspnet:9.0 runtime base image doesn't ship procps, so
Process.Start(\"kill\", \"-HUP <pid>\") failed with ENOENT — the first
reconcile after boot wrote the new nats.conf to disk, then threw when
signalling, and subsequent reconciles saw the on-disk text already
matched the target and skipped the retry. Net effect: nats-server kept
running with whatever config it parsed at startup, and configuration
changes never took effect at runtime.
Switch to a direct P/Invoke to libc kill(2) so we don't depend on any
external binary.
The JetStream request/reply model has the CLIENT publishing requests to
$JS.API.<...> (and $JS.ACK.<...>) and receiving replies on _INBOX.>.
ConfigGenerator was granting these on the subscribe side instead, so
end users could never create consumers, fetch, ack, or query streams —
their JS calls silently timed out on unauthorized publishes. Also add
STREAM.INFO / CONSUMER.INFO / CONSUMER.DURABLE.CREATE / CONSUMER.DELETE,
which end-user clients need but weren't granted at all.
Tests now assert placement (publish vs subscribe block) so this can't
regress silently. The example switches to Test_2 as the destination
stream name to match the real deployment.
Small .NET 9 console app that opens two NATS connections, one per node,
to exercise the edge-routing feature end-to-end. Uses NATS.Client.Core
for the publisher and NATS.Client.JetStream for the ephemeral consumer.
Removes the 'at least one subject required' validator from both the backend
POST /api/nodes endpoint and the frontend node drawer. NATS accepts streams
with no subjects — messages flow in via 'sources' (incoming edges) only.
Publishers can still be assigned but they'll get an empty publish allow list.
Adds a lightweight table + repository + POST /api/nodes/positions endpoint.
/api/graph now returns positions keyed by node id. Frontend uses saved
positions on load and debounce-saves 300ms after a drag ends.
The previous stage downloaded from GitHub via curl, which fails under Coolify's
builder network with an opaque exit 2. Using the official image avoids the
network dependency entirely.
NATS forbids JetStream on the system account, so the admin client cannot
use SYS for stream ops. Add an internal _reconciler user to APP with full
JS + subject access (never surfaced in the UI), and change the admin
connection to auth as that user.
KICK-on-user-remove is deferred to a future two-connection design: for
now, ListCidsForUserAsync/KickCidAsync are no-ops. Removed users stay
connected until they voluntarily disconnect (SIGHUP alone doesn't
disconnect existing sessions).