From 32ab38b3024dc96133effd29d64be68c16119636 Mon Sep 17 00:00:00 2001 From: enixCode <58286681+enixCode@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:45:59 +0200 Subject: [PATCH] docs(website): add animated architecture diagrams Custom SVG/React figures in the light-* design (icy-blue on near-black, Fraunces + JetBrains Mono), framed as diagram panels so they read the same in light and dark Fumadocs themes. Pure CSS animation, no client JS, so they survive static export, and honour prefers-reduced-motion. - Architecture: the 3-tier delegation chain (light-process -> light-run -> light-runner -> Docker) with a request token down and a result token up. Embedded in the Overview and Execution pages. - Dag: the flagship workflow graph, fan-out plus a conditional link. Embedded in Workflows. - NetworkServices: the proxy/secret pattern, the key stays in the proxy and the workload reaches it by hostname. Embedded in the Services section. build with cc --- website/content/docs/docker.mdx | 2 + website/content/docs/index.mdx | 2 + website/content/docs/workflows.mdx | 4 + .../src/components/diagrams/Architecture.tsx | 109 +++++++ website/src/components/diagrams/Dag.tsx | 106 +++++++ .../components/diagrams/NetworkServices.tsx | 104 +++++++ .../components/diagrams/diagrams.module.css | 266 ++++++++++++++++++ website/src/components/diagrams/index.ts | 3 + website/src/components/mdx.tsx | 4 + 9 files changed, 600 insertions(+) create mode 100644 website/src/components/diagrams/Architecture.tsx create mode 100644 website/src/components/diagrams/Dag.tsx create mode 100644 website/src/components/diagrams/NetworkServices.tsx create mode 100644 website/src/components/diagrams/diagrams.module.css create mode 100644 website/src/components/diagrams/index.ts diff --git a/website/content/docs/docker.mdx b/website/content/docs/docker.mdx index 0d31fd5..193a0f8 100644 --- a/website/content/docs/docker.mdx +++ b/website/content/docs/docker.mdx @@ -4,6 +4,8 @@ title: "Execution (light-run)" light-process no longer talks to Docker directly. Every node runs inside a container spawned by the [light-run](https://github.com/enixCode/light-run) HTTP service. + + ## Configuration ```bash diff --git a/website/content/docs/index.mdx b/website/content/docs/index.mdx index 13f22ef..5dc3b87 100644 --- a/website/content/docs/index.mdx +++ b/website/content/docs/index.mdx @@ -7,6 +7,8 @@ description: A DAG workflow engine that orchestrates code in Docker containers v Execution itself is delegated: each node runs on a [light-run](https://github.com/enixCode/light-run) instance over HTTP, which in turn drives [light-runner](https://github.com/enixCode/light-runner) and Docker. light-process never touches Docker directly; it owns orchestration, identity, and structured results. + + ## Where to start - **[Getting started](/docs/getting-started)** - install, point at a light-run instance, run the example. diff --git a/website/content/docs/workflows.mdx b/website/content/docs/workflows.mdx index f02975c..07856d2 100644 --- a/website/content/docs/workflows.mdx +++ b/website/content/docs/workflows.mdx @@ -4,6 +4,8 @@ title: "Workflows" A workflow is a directed acyclic graph (DAG) of nodes connected by links. Each node runs code in a Docker container. + + ## Folder structure ``` @@ -168,6 +170,8 @@ Isolation is topological: put each group of nodes on its own `networkDef` and th A `service` is a long-running container started **before** the DAG and stopped **after** it, living outside the DAG flow (it is never a node and never an entry node). The canonical use is a network proxy that holds a provider API key so a workload node can reach another provider by hostname **without the secret ever entering the workload container**. + + ```json { "id": "claude-wf", diff --git a/website/src/components/diagrams/Architecture.tsx b/website/src/components/diagrams/Architecture.tsx new file mode 100644 index 0000000..b91ddf3 --- /dev/null +++ b/website/src/components/diagrams/Architecture.tsx @@ -0,0 +1,109 @@ +import styles from './diagrams.module.css'; + +/* + * The strict top-down delegation chain: + * light-process (graph) -> light-run (HTTP) -> light-runner (Docker SDK) -> Docker + * A request token travels down the left rail, a result token back up the right. + * Pure CSS animation, no client JS - safe in static export. + */ + +const LAYERS = [ + { name: 'light-process', sub: 'DAG orchestrator - evaluates the graph', current: true }, + { name: 'light-run', sub: 'HTTP wrapper - POST /run, serves artifacts' }, + { name: 'light-runner', sub: 'Docker SDK - dockerode, extract, state' }, + { name: 'Docker', sub: 'isolation boundary - the container runs here' }, +]; + +const ROW_H = 52; +const GAP = 18; +const TOP = 16; +const BOX_X = 150; +const BOX_W = 372; + +export function Architecture() { + const height = TOP * 2 + LAYERS.length * ROW_H + (LAYERS.length - 1) * GAP; + const railTop = TOP + ROW_H / 2; + const railBottom = TOP + (LAYERS.length - 1) * (ROW_H + GAP) + ROW_H / 2; + const travel = railBottom - railTop; + + return ( +
+ + {/* down rail (request) */} + + {/* up rail (result) */} + + + {LAYERS.map((l, i) => { + const y = TOP + i * (ROW_H + GAP); + return ( + + + + {l.name} + + + {l.sub} + + {/* rail entry/exit ticks */} + + + + ); + })} + + {/* arrow captions */} + + request + + + POST /run + + + result + + + RunState + + + {/* traveling tokens */} + + + +
+ Fig. - one request, three layers, one isolation boundary. + no upward coupling +
+
+ ); +} diff --git a/website/src/components/diagrams/Dag.tsx b/website/src/components/diagrams/Dag.tsx new file mode 100644 index 0000000..36efef1 --- /dev/null +++ b/website/src/components/diagrams/Dag.tsx @@ -0,0 +1,106 @@ +import styles from './diagrams.module.css'; + +/* + * The DAG model: entry node fans out into a parallel batch; links carry output + * to the next node, optionally gated by a `when` condition. Live edges animate + * a flowing dash; the conditional edge is labelled with its predicate. Pure CSS, + * no client JS - safe in static export. Reused (scaled) as the landing hero. + */ + +type NodeDef = { + id: string; + x: number; + y: number; + label: string; + sub: string; + cls?: string; + pulse?: string; +}; + +const W = 124; +const H = 46; + +const NODES: NodeDef[] = [ + { id: 'fetch', x: 40, y: 110, label: 'fetch', sub: 'node:24', pulse: 'pulse' }, + { id: 'parse', x: 286, y: 40, label: 'parse', sub: 'python', pulse: 'pulseB' }, + { id: 'store', x: 286, y: 180, label: 'store', sub: 'node:24', pulse: 'pulseC' }, + { id: 'report', x: 516, y: 40, label: 'report', sub: 'python', pulse: 'pulseB' }, +]; + +function center(n: NodeDef) { + return { cx: n.x + W / 2, cy: n.y + H / 2 }; +} + +export function Dag() { + const byId = Object.fromEntries(NODES.map((n) => [n.id, n])); + const edge = (from: string, to: string) => { + const a = center(byId[from]); + const b = center(byId[to]); + const x1 = byId[from].x + W; + const y1 = a.cy; + const x2 = byId[to].x; + const y2 = b.cy; + const mx = (x1 + x2) / 2; + return `M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}`; + }; + + return ( +
+ + + + + + + + {/* edges (drawn first so nodes sit on top) */} + + + + + + + + {/* edge condition labels */} + + when ok:true + + + fan-out + + + when score > 0.8 + + + {/* nodes */} + {NODES.map((n) => ( + + + + {n.label} + + + {n.sub} + + + ))} + +
+ Fig. - nodes fan out; a link fires only when its condition holds. + data + conditions +
+
+ ); +} diff --git a/website/src/components/diagrams/NetworkServices.tsx b/website/src/components/diagrams/NetworkServices.tsx new file mode 100644 index 0000000..8903a0a --- /dev/null +++ b/website/src/components/diagrams/NetworkServices.tsx @@ -0,0 +1,104 @@ +import styles from './diagrams.module.css'; + +/* + * The networks/services proxy pattern. A run-scoped Docker network + * (`lp--svc-net`) holds the workload and a sidecar proxy service. The + * secret (API key) lives only in the proxy: the workload reaches it by + * hostname, no credential in the workload's env. The proxy is the single hop + * that attaches the key on the way out to the external API. Pure CSS animation, + * no client JS - safe in static export. + */ + +const BOX = { x: 20, y: 52, w: 432, h: 140 }; + +const WORKLOAD = { x: 44, y: 100, w: 150, h: 54 }; +const PROXY = { x: 258, y: 100, w: 150, h: 54 }; +const EXTERNAL = { x: 512, y: 100, w: 148, h: 54 }; + +function rightMid(n: { x: number; y: number; w: number; h: number }) { + return { x: n.x + n.w, y: n.y + n.h / 2 }; +} +function leftMid(n: { x: number; y: number; w: number; h: number }) { + return { x: n.x, y: n.y + n.h / 2 }; +} + +export function NetworkServices() { + const a = rightMid(WORKLOAD); + const b = leftMid(PROXY); + const c = rightMid(PROXY); + const d = leftMid(EXTERNAL); + + return ( +
+ + + + + + + + + + + {/* run-scoped network boundary */} + + + network: lp-<run>-svc-net + + + {/* edges first */} + + + + + + {/* edge labels */} + + http://proxy + + + no secret + + + + Bearer key + + + {/* workload node */} + + + workload + + + no API key in env + + + {/* proxy node (holds the secret) */} + + + proxy + + + holds the API key + + + {/* external API (outside the boundary) */} + + + external API + + + api.example.com + + +
+ Fig. - the workload reaches the API by hostname; the key never leaves the proxy. + secret isolation +
+
+ ); +} diff --git a/website/src/components/diagrams/diagrams.module.css b/website/src/components/diagrams/diagrams.module.css new file mode 100644 index 0000000..0101819 --- /dev/null +++ b/website/src/components/diagrams/diagrams.module.css @@ -0,0 +1,266 @@ +/* + * Self-contained design tokens + animations for the light-process docs + * diagrams. The docs site (Fumadocs) runs a neutral/blue theme; these figures + * deliberately carry the light-* landing aesthetic (near-black canvas, ice-blue + * accent, Fraunces/JetBrains Mono voice) as a framed "diagram panel", so they + * read the same in light and dark docs. Tokens are hardcoded here on purpose: + * the landing CSS variables are not in scope inside the docs. + */ + +.figure { + --d-ink: #050608; + --d-ink-2: #0a0c10; + --d-line: #1a1d24; + --d-line-hot: #242933; + --d-paper: #edeef0; + --d-paper-dim: #8a8f9a; + --d-paper-dimmer: #4d525c; + --d-halo: #c7e8ff; + --d-halo-soft: #7fddff; + --d-amber: #ffb855; + --d-ember: #ff6b35; + + position: relative; + margin: 1.75rem 0; + padding: 1.4rem 1.4rem 1rem; + background: radial-gradient(120% 140% at 50% 0%, var(--d-ink-2), var(--d-ink) 70%); + border: 1px solid var(--d-line); + border-radius: 3px; + overflow: hidden; + color: var(--d-paper); + font-family: + 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; +} + +/* Corner brackets, the landing's signature motif. */ +.figure::before, +.figure::after { + content: ''; + position: absolute; + width: 14px; + height: 14px; + border: 1px solid var(--d-halo-soft); + opacity: 0.5; + pointer-events: none; +} +.figure::before { + top: 8px; + left: 8px; + border-right: 0; + border-bottom: 0; +} +.figure::after { + bottom: 8px; + right: 8px; + border-left: 0; + border-top: 0; +} + +.svg { + display: block; + width: 100%; + height: auto; +} + +.caption { + margin-top: 0.9rem; + display: flex; + justify-content: space-between; + gap: 1rem; + font-size: 10.5px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--d-paper-dimmer); +} +.captionRight { + color: var(--d-halo-soft); + opacity: 0.7; +} + +/* --- SVG primitives (applied via className on SVG elements) --- */ + +.panel { + fill: var(--d-ink-2); + stroke: var(--d-line-hot); + stroke-width: 1; +} +.panelDashed { + fill: rgba(199, 232, 255, 0.03); + stroke: var(--d-halo-soft); + stroke-width: 1; + stroke-dasharray: 5 4; + opacity: 0.8; +} +.node { + fill: var(--d-ink-2); + stroke: var(--d-halo); + stroke-width: 1.25; +} +.nodeMuted { + fill: var(--d-ink-2); + stroke: var(--d-line-hot); + stroke-width: 1.25; +} +.nodeWarm { + fill: var(--d-ink-2); + stroke: var(--d-amber); + stroke-width: 1.25; +} + +.label { + fill: var(--d-paper); + font-family: 'Fraunces', Georgia, serif; + font-style: italic; + font-size: 15px; +} +.labelMono { + fill: var(--d-paper); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 12px; +} +.sub { + fill: var(--d-paper-dim); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 10px; + letter-spacing: 0.04em; +} +.tag { + fill: var(--d-halo-soft); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 9.5px; + letter-spacing: 0.06em; +} +.tagWarm { + fill: var(--d-amber); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 9.5px; + letter-spacing: 0.06em; +} + +.arrowLive { + fill: var(--d-halo); +} +.arrowMuted { + fill: var(--d-paper-dimmer); +} +.arrowWarm { + fill: var(--d-amber); +} + +.edge { + fill: none; + stroke: var(--d-line-hot); + stroke-width: 1.5; +} +.edgeLive { + fill: none; + stroke: var(--d-halo); + stroke-width: 1.5; + stroke-dasharray: 4 5; + animation: flow 1.1s linear infinite; +} +.edgeWarm { + fill: none; + stroke: var(--d-amber); + stroke-width: 1.5; + stroke-dasharray: 4 5; + animation: flow 1.4s linear infinite; +} + +/* A glowing pulse on a node outline. */ +.pulse { + animation: pulse 2.4s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; +} +.pulseB { + animation: pulse 2.4s ease-in-out infinite 0.8s; + transform-box: fill-box; + transform-origin: center; +} +.pulseC { + animation: pulse 2.4s ease-in-out infinite 1.6s; + transform-box: fill-box; + transform-origin: center; +} + +/* A token that travels along a path (architecture rails / proxy hop). */ +.travelDown { + fill: var(--d-halo); + animation: travelDown 2.6s ease-in-out infinite; +} +.travelUp { + fill: var(--d-halo-soft); + animation: travelUp 2.6s ease-in-out infinite 1.3s; +} +.glowDot { + fill: var(--d-halo); + filter: drop-shadow(0 0 4px var(--d-halo-soft)); +} + +@keyframes flow { + to { + stroke-dashoffset: -18; + } +} +@keyframes pulse { + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } +} +@keyframes travelDown { + 0% { + opacity: 0; + transform: translateY(0); + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateY(var(--travel, 210px)); + } +} +@keyframes travelUp { + 0% { + opacity: 0; + transform: translateY(0); + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateY(calc(-1 * var(--travel, 210px))); + } +} + +@media (prefers-reduced-motion: reduce) { + .edgeLive, + .edgeWarm, + .pulse, + .pulseB, + .pulseC, + .travelDown, + .travelUp { + animation: none; + } + .edgeLive, + .edgeWarm { + stroke-dasharray: 4 5; + } + .travelDown, + .travelUp { + opacity: 0; + } +} diff --git a/website/src/components/diagrams/index.ts b/website/src/components/diagrams/index.ts new file mode 100644 index 0000000..a6ad7d3 --- /dev/null +++ b/website/src/components/diagrams/index.ts @@ -0,0 +1,3 @@ +export { Architecture } from './Architecture'; +export { Dag } from './Dag'; +export { NetworkServices } from './NetworkServices'; diff --git a/website/src/components/mdx.tsx b/website/src/components/mdx.tsx index a640575..dc68724 100644 --- a/website/src/components/mdx.tsx +++ b/website/src/components/mdx.tsx @@ -1,9 +1,13 @@ import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; +import { Architecture, Dag, NetworkServices } from './diagrams'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, + Architecture, + Dag, + NetworkServices, ...components, } satisfies MDXComponents; }