.nornir/assets/pipeline_viewer_flow.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 680" font-family="'DejaVu Sans', 'Segoe UI', sans-serif">
<defs>
<linearGradient id="board" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0b1c2e"/>
<stop offset="1" stop-color="#06111d"/>
</linearGradient>
<linearGradient id="chip" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#14384f"/>
<stop offset="1" stop-color="#0e2a3e"/>
</linearGradient>
<linearGradient id="rail" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#173d57"/>
<stop offset="1" stop-color="#0c2438"/>
</linearGradient>
<linearGradient id="slot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#123448"/>
<stop offset="1" stop-color="#0a2032"/>
</linearGradient>
</defs>
<!-- PCB substrate -->
<rect x="0" y="0" width="1180" height="680" fill="url(#board)"/>
<g stroke="#143a52" stroke-width="1">
<line x1="0" y1="76" x2="1180" y2="76"/>
<line x1="0" y1="632" x2="1180" y2="632"/>
</g>
<g fill="#16415c">
<circle cx="40" cy="40" r="2"/><circle cx="1140" cy="40" r="2"/>
<circle cx="40" cy="652" r="2"/><circle cx="1140" cy="652" r="2"/>
</g>
<!-- title -->
<text x="40" y="44" fill="#eaf6ff" font-size="22" font-weight="700">knut-pipelines</text>
<text x="282" y="44" fill="#9fc9e0" font-size="15">one <tspan fill="#bfe6ff" font-weight="700">ViewerState</tspan>, many frontends — every GUI capability is reachable from the CLI</text>
<!-- legend -->
<g font-size="12.5">
<line x1="804" y1="60" x2="844" y2="60" stroke="#6fd3f5" stroke-width="3"/>
<text x="852" y="64" fill="#cfe6f5">update stream (fold)</text>
<line x1="1006" y1="60" x2="1046" y2="60" stroke="#7fb0cc" stroke-width="2.5" stroke-dasharray="5 4"/>
<text x="1054" y="64" fill="#cfe6f5">render (pure fn)</text>
</g>
<!-- ===================== SPARK CONNECT (left) ===================== -->
<rect x="36" y="150" width="160" height="220" rx="11" fill="url(#chip)" stroke="#5aa6cf" stroke-width="2"/>
<text x="116" y="178" fill="#eaf6ff" font-size="13.5" font-weight="700" text-anchor="middle">Spark Connect</text>
<text x="116" y="196" fill="#9fc9e0" font-size="10" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">sc://host:15002</text>
<g font-family="'DejaVu Sans Mono', monospace" font-size="9.5">
<rect x="56" y="208" width="120" height="34" rx="6" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.3"/>
<text x="66" y="222" fill="#bfe6ff">SDP graph</text>
<text x="66" y="235" fill="#86b3cc" font-size="8.5">datasets + flows</text>
<rect x="56" y="248" width="120" height="34" rx="6" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.3"/>
<text x="66" y="262" fill="#bfe6ff">run events</text>
<text x="66" y="275" fill="#86b3cc" font-size="8.5">pipeline lifecycle</text>
<rect x="56" y="288" width="120" height="34" rx="6" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.3"/>
<text x="66" y="302" fill="#bfe6ff">job progress</text>
<text x="66" y="315" fill="#86b3cc" font-size="8.5">per-stage task counts</text>
</g>
<text x="116" y="346" fill="#7fa8c4" font-size="9" text-anchor="middle">plain jobs (execute_sql)</text>
<text x="116" y="360" fill="#7fa8c4" font-size="9" text-anchor="middle">AND SDP pipelines</text>
<rect x="196" y="222" width="6" height="8" fill="#6fd3f5"/>
<rect x="196" y="262" width="6" height="8" fill="#6fd3f5"/>
<rect x="196" y="302" width="6" height="8" fill="#6fd3f5"/>
<g stroke="#6fd3f5" stroke-width="3" fill="none">
<path d="M196 226 H236 V250 H276"/>
<path d="M196 266 H276"/>
<path d="M196 306 H236 V282 H276"/>
</g>
<circle cx="276" cy="266" r="5" fill="#bfecff"/>
<!-- ===================== CLIENT (run_updates) ===================== -->
<rect x="276" y="180" width="160" height="170" rx="11" fill="url(#chip)" stroke="#7fc8ea" stroke-width="2"/>
<text x="356" y="208" fill="#eaf6ff" font-size="13" font-weight="700" text-anchor="middle">PipelinesClient</text>
<text x="356" y="226" fill="#9fc9e0" font-size="9.5" text-anchor="middle">[feature grpc]</text>
<text x="356" y="248" fill="#bfe6ff" font-size="10" text-anchor="middle" font-weight="700">run_updates</text>
<text x="356" y="266" fill="#86b3cc" font-size="9" text-anchor="middle">merges events + progress</text>
<text x="356" y="280" fill="#86b3cc" font-size="9" text-anchor="middle">into one ViewerUpdate stream</text>
<text x="356" y="300" fill="#a9d8ee" font-size="9" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">execute_sql / define_graph</text>
<text x="356" y="316" fill="#7fa8c4" font-size="8.5" text-anchor="middle">tonic 0.14 + vendored proto</text>
<text x="356" y="332" fill="#7fa8c4" font-size="8.5" text-anchor="middle">UpdateStream</text>
<rect x="270" y="262" width="6" height="8" fill="#6fd3f5"/>
<rect x="436" y="262" width="6" height="8" fill="#6fd3f5"/>
<g stroke="#6fd3f5" stroke-width="3" fill="none"><path d="M436 266 H486"/></g>
<circle cx="486" cy="266" r="4" fill="#bfecff"/>
<!-- ===================== VIEWERSTATE (the single source of truth) ===================== -->
<rect x="486" y="150" width="200" height="230" rx="12" fill="url(#rail)" stroke="#7fc8ea" stroke-width="2.5"/>
<text x="586" y="178" fill="#eaf6ff" font-size="14.5" font-weight="700" text-anchor="middle">ViewerState</text>
<text x="586" y="196" fill="#bfe6ff" font-size="10" text-anchor="middle" font-weight="700">single source of truth</text>
<text x="586" y="214" fill="#9fc9e0" font-size="9.5" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">state.apply(update)</text>
<g font-family="'DejaVu Sans Mono', monospace" font-size="9.5">
<rect x="506" y="226" width="160" height="22" rx="5" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.2"/>
<text x="516" y="241" fill="#bfe6ff">DataflowGraph</text>
<rect x="506" y="252" width="160" height="22" rx="5" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.2"/>
<text x="516" y="267" fill="#bfe6ff">NodeStatus / node</text>
<rect x="506" y="278" width="160" height="22" rx="5" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.2"/>
<text x="516" y="293" fill="#bfe6ff">event log</text>
<rect x="506" y="304" width="160" height="22" rx="5" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.2"/>
<text x="516" y="319" fill="#bfe6ff">latest ExecutionProgress</text>
</g>
<text x="586" y="346" fill="#86b3cc" font-size="9" text-anchor="middle">validate · topo_order</text>
<text x="586" y="362" fill="#7fa8c4" font-size="9" text-anchor="middle">all frontends = pure functions of it</text>
<rect x="680" y="200" width="6" height="8" fill="#6fd3f5"/>
<rect x="680" y="300" width="6" height="8" fill="#6fd3f5"/>
<!-- fan-out to frontends (dashed = render) -->
<g stroke="#7fb0cc" stroke-width="2.5" fill="none" stroke-dasharray="6 4">
<path d="M686 200 H730 V170 H776"/>
<path d="M686 250 H776"/>
<path d="M686 300 H730 V330 H776"/>
</g>
<!-- ===================== FRONTENDS (right) ===================== -->
<rect x="776" y="146" width="180" height="56" rx="9" fill="url(#chip)" stroke="#7fc8ea" stroke-width="2"/>
<text x="866" y="170" fill="#eaf6ff" font-size="12.5" font-weight="700" text-anchor="middle">CLI text / JSON</text>
<text x="866" y="188" fill="#9fc9e0" font-size="8.5" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">dump_text / dump_json / dump_state</text>
<rect x="770" y="166" width="6" height="8" fill="#7fb0cc"/>
<rect x="776" y="222" width="180" height="56" rx="9" fill="url(#chip)" stroke="#7fc8ea" stroke-width="2"/>
<text x="866" y="246" fill="#eaf6ff" font-size="12.5" font-weight="700" text-anchor="middle">egui + snarl GUI</text>
<text x="866" y="264" fill="#9fc9e0" font-size="8.5" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">DagViewer : SnarlViewer [gui]</text>
<rect x="770" y="242" width="6" height="8" fill="#7fb0cc"/>
<rect x="776" y="298" width="180" height="56" rx="9" fill="url(#chip)" stroke="#7fc8ea" stroke-width="2"/>
<text x="866" y="322" fill="#eaf6ff" font-size="12.5" font-weight="700" text-anchor="middle">headless run API</text>
<text x="866" y="340" fill="#9fc9e0" font-size="8.5" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">fold + dump_state(state, tail)</text>
<rect x="770" y="318" width="6" height="8" fill="#7fb0cc"/>
<!-- knutpipe binary note -->
<rect x="990" y="190" width="150" height="170" rx="10" fill="url(#chip)" stroke="#5aa6cf" stroke-width="2"/>
<text x="1065" y="216" fill="#eaf6ff" font-size="13" font-weight="700" text-anchor="middle">knutpipe</text>
<g font-family="'DejaVu Sans Mono', monospace" font-size="9.5">
<rect x="1004" y="228" width="122" height="22" rx="4" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1"/>
<text x="1014" y="243" fill="#bfe6ff">show [graph.json]</text>
<rect x="1004" y="254" width="122" height="22" rx="4" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1"/>
<text x="1014" y="269" fill="#bfe6ff">watch <sc-url></text>
<rect x="1004" y="280" width="122" height="22" rx="4" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1"/>
<text x="1014" y="295" fill="#bfe6ff">gui [graph.json]</text>
<rect x="1004" y="306" width="122" height="22" rx="4" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1"/>
<text x="1014" y="321" fill="#bfe6ff">pipeline --spec</text>
</g>
<text x="1065" y="346" fill="#7fa8c4" font-size="8.5" text-anchor="middle">live = gui + grpc</text>
<g stroke="#7fb0cc" stroke-width="2" fill="none" stroke-dasharray="4 3">
<path d="M956 174 H990 V228"/>
<path d="M956 250 H980 V260 H990"/>
<path d="M956 326 H980 V306 H990"/>
</g>
<!-- ===================== GROUND RAIL ===================== -->
<rect x="36" y="552" width="1104" height="56" rx="10" fill="url(#rail)" stroke="#7fc8ea" stroke-width="2"/>
<g fill="#0a1c2c" stroke="#7fc8ea" stroke-width="1.5">
<circle cx="80" cy="580" r="4"/><circle cx="420" cy="580" r="4"/>
<circle cx="720" cy="580" r="4"/><circle cx="1040" cy="580" r="4"/>
</g>
<text x="106" y="579" fill="#dff1ff" font-size="14" font-weight="700">laws</text>
<text x="180" y="575" fill="#bcd9ec" font-size="11">one model, many frontends · CLI parity is a hard requirement · Spark jobs, not only pipelines</text>
<text x="180" y="596" fill="#7fa8c4" font-size="10" font-family="'DejaVu Sans Mono', monospace">heavy stacks (grpc, gui) opt-in · egui/eframe pinned 0.28 for egui-snarl 0.4</text>
</svg>