docs(#1110): update README diagram; add animated SVG component diagram

README: fix stale ASCII diagram
- hm1nd → h-ruth (manager container name, post-#1014 rename)
- per-agent web UIs now route through hive-gateway unix sockets, not
  direct :8100-8999 TCP (HIVE_WEB_SOCKET is now unconditional)
- split into 'optional containers' and 'agent containers' sections
- add hive-gateway, hive-forge, hive-matrix rows

New: branding/component-diagram.svg — extended animated diagram
for website use (as suggested in the issue). Matches the existing
hyperhive branding aesthetic (dark #0a0600 bg, amber #ffb300 accents,
monospace, scanlines). Shows:
- host machine frame with hive-c0re as central hub
- optional containers: gateway, forge, matrix
- agent containers: h-ruth, h-iris, h-damocles, h-<name>
- animated dots flowing along connection paths (broker messages,
  HTTP proxying, config pushes)
- pulsing glow ring on hive-c0re
- legend panel
This commit is contained in:
iris 2026-06-03 09:57:35 +02:00 committed by mara
commit 84b42f5751
2 changed files with 408 additions and 5 deletions

View file

@ -21,16 +21,21 @@ the substrate.
host (NixOS, runs hive-c0re.service)
├── operator
│ ├── browser → :7000 hive-c0re dashboard
├── browser → :8100-8999 per-agent web UIs (all agents, FNV-1a hash)
│ ├── browser → :80 (hive-gateway) dashboard + per-agent UIs
│ /agent/<name>/ → per-agent unix socket
│ └── CLI → /run/hyperhive/host.sock admin protocol
├── hive-c0re (Rust daemon: lifecycle / broker / approvals /
│ auto-update / dashboard / sockets)
└── nixos-containers
├── hm1nd manager agent (privileged MCP surface)
└── h-<name> sub-agent (vanilla MCP surface + per-agent extras)
├── optional containers
│ ├── hive-gateway nginx — proxies :80 → c0re dashboard + per-agent sockets
│ ├── hive-forge Forgejo — per-agent accounts, config mirror (agent-configs/)
│ └── hive-matrix tuwunel — Matrix homeserver + per-agent accounts
└── agent containers
├── h-ruth manager (privileged MCP surface, approval gating)
└── h-<name> sub-agent (claude + MCP tools + per-agent web UI + unix socket)
```
Depth lives in [`docs/`](docs/) — pick the one matching your task:

View file

@ -0,0 +1,398 @@
<svg width="1000" height="620" viewBox="0 0 1000 620" role="img"
xmlns="http://www.w3.org/2000/svg">
<title>hyperhive — component diagram</title>
<desc>Architecture diagram: host machine running hive-c0re, optional service containers (gateway, forge, matrix), and agent containers (manager + sub-agents) communicating over unix sockets and a sqlite broker.</desc>
<defs>
<!-- scanline texture -->
<pattern id="scan" x="0" y="0" width="1" height="4" patternUnits="userSpaceOnUse">
<rect x="0" y="0" width="1" height="1" fill="#ffb300" opacity="0.04"/>
</pattern>
<!-- arrow marker -->
<marker id="arr" viewBox="0 0 8 8" refX="7" refY="4"
markerWidth="5" markerHeight="5" orient="auto">
<path d="M0,0 L8,4 L0,8 Z" fill="#ffb300" opacity="0.7"/>
</marker>
<marker id="arr-dim" viewBox="0 0 8 8" refX="7" refY="4"
markerWidth="5" markerHeight="5" orient="auto">
<path d="M0,0 L8,4 L0,8 Z" fill="#7a4f00" opacity="0.8"/>
</marker>
<marker id="arr-blue" viewBox="0 0 8 8" refX="7" refY="4"
markerWidth="5" markerHeight="5" orient="auto">
<path d="M0,0 L8,4 L0,8 Z" fill="#6ab4f5" opacity="0.8"/>
</marker>
<!-- glow filter for active elements -->
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-lg" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- dot that travels along a path (message animation) -->
<circle id="dot" r="3.5" fill="#ffb300" filter="url(#glow)"/>
<circle id="dot-blue" r="3" fill="#6ab4f5" filter="url(#glow)"/>
<circle id="dot-green" r="2.5" fill="#76d97e" filter="url(#glow)"/>
</defs>
<!-- ── background ── -->
<rect width="1000" height="620" fill="#0a0600"/>
<rect width="1000" height="620" fill="url(#scan)"/>
<!-- ── host outer frame ── -->
<rect x="12" y="12" width="976" height="596" rx="4"
fill="none" stroke="#ffb300" stroke-width="1" opacity="0.25"/>
<text x="22" y="28" font-family="monospace" font-size="10" fill="#ffb300" opacity="0.5"
letter-spacing="2">HOST (NixOS)</text>
<!-- ═══════════════════════════════════════════════════════════
OPERATOR (top-left)
═══════════════════════════════════════════════════════════ -->
<g id="operator" transform="translate(60,55)">
<rect x="0" y="0" width="130" height="58" rx="3"
fill="#1a0e00" stroke="#ffb300" stroke-width="1" opacity="0.9"/>
<text x="65" y="18" text-anchor="middle"
font-family="monospace" font-size="10" fill="#ffb300" letter-spacing="1">OPERATOR</text>
<text x="65" y="33" text-anchor="middle"
font-family="monospace" font-size="8.5" fill="#c89000" opacity="0.8">browser</text>
<text x="65" y="47" text-anchor="middle"
font-family="monospace" font-size="8.5" fill="#c89000" opacity="0.8">+ CLI</text>
</g>
<!-- ═══════════════════════════════════════════════════════════
hive-c0re (center-left, the hub)
═══════════════════════════════════════════════════════════ -->
<g id="c0re" transform="translate(290,240)">
<!-- outer glow ring -->
<circle cx="95" cy="52" r="72" fill="none" stroke="#ffb300"
stroke-width="0.5" opacity="0.15" filter="url(#glow-lg)"/>
<rect x="0" y="0" width="190" height="104" rx="4"
fill="#140d00" stroke="#ffb300" stroke-width="1.5"/>
<text x="95" y="18" text-anchor="middle"
font-family="monospace" font-size="11" fill="#ffb300" letter-spacing="2"
filter="url(#glow)">hive-c0re</text>
<line x1="10" y1="23" x2="180" y2="23" stroke="#ffb300" stroke-width="0.4" opacity="0.4"/>
<text x="95" y="37" text-anchor="middle"
font-family="monospace" font-size="8" fill="#c89000" opacity="0.75">lifecycle · broker · approvals</text>
<text x="95" y="51" text-anchor="middle"
font-family="monospace" font-size="8" fill="#c89000" opacity="0.75">auto-update · dashboard</text>
<text x="95" y="65" text-anchor="middle"
font-family="monospace" font-size="8" fill="#c89000" opacity="0.75">sockets · rebuild-queue</text>
<line x1="10" y1="73" x2="180" y2="73" stroke="#ffb300" stroke-width="0.4" opacity="0.3"/>
<text x="95" y="86" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#7a5500" opacity="0.9">:7000 · /run/hyperhive/host.sock</text>
<text x="95" y="99" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#7a5500" opacity="0.9">/run/hyperhive/agent-*.sock</text>
</g>
<!-- ═══════════════════════════════════════════════════════════
OPTIONAL CONTAINERS (right side)
═══════════════════════════════════════════════════════════ -->
<!-- hive-gateway -->
<g id="gateway" transform="translate(610,50)">
<rect x="0" y="0" width="155" height="72" rx="3"
fill="#0d0e00" stroke="#d4c400" stroke-width="1" opacity="0.85"/>
<text x="77" y="16" text-anchor="middle"
font-family="monospace" font-size="10" fill="#d4c400" letter-spacing="1">hive-gateway</text>
<line x1="8" y1="21" x2="147" y2="21" stroke="#d4c400" stroke-width="0.4" opacity="0.4"/>
<text x="77" y="34" text-anchor="middle"
font-family="monospace" font-size="8" fill="#a09200" opacity="0.8">nginx container · :80</text>
<text x="77" y="47" text-anchor="middle"
font-family="monospace" font-size="8" fill="#a09200" opacity="0.7">/agent/&lt;name&gt;/ → unix sock</text>
<text x="77" y="60" text-anchor="middle"
font-family="monospace" font-size="8" fill="#a09200" opacity="0.7">forge.&lt;domain&gt; vhost</text>
</g>
<!-- hive-forge -->
<g id="forge" transform="translate(610,160)">
<rect x="0" y="0" width="155" height="72" rx="3"
fill="#000d0d" stroke="#3ab8b8" stroke-width="1" opacity="0.85"/>
<text x="77" y="16" text-anchor="middle"
font-family="monospace" font-size="10" fill="#3ab8b8" letter-spacing="1">hive-forge</text>
<line x1="8" y1="21" x2="147" y2="21" stroke="#3ab8b8" stroke-width="0.4" opacity="0.4"/>
<text x="77" y="34" text-anchor="middle"
font-family="monospace" font-size="8" fill="#2a9090" opacity="0.8">Forgejo · :3000</text>
<text x="77" y="47" text-anchor="middle"
font-family="monospace" font-size="8" fill="#2a9090" opacity="0.7">per-agent accounts + tokens</text>
<text x="77" y="60" text-anchor="middle"
font-family="monospace" font-size="8" fill="#2a9090" opacity="0.7">agent-configs/ mirror</text>
</g>
<!-- hive-matrix -->
<g id="matrix" transform="translate(610,270)">
<rect x="0" y="0" width="155" height="72" rx="3"
fill="#0d000d" stroke="#b060c8" stroke-width="1" opacity="0.85"/>
<text x="77" y="16" text-anchor="middle"
font-family="monospace" font-size="10" fill="#b060c8" letter-spacing="1">hive-matrix</text>
<line x1="8" y1="21" x2="147" y2="21" stroke="#b060c8" stroke-width="0.4" opacity="0.4"/>
<text x="77" y="34" text-anchor="middle"
font-family="monospace" font-size="8" fill="#8040a0" opacity="0.8">tuwunel Matrix server</text>
<text x="77" y="47" text-anchor="middle"
font-family="monospace" font-size="8" fill="#8040a0" opacity="0.7">per-agent accounts</text>
<text x="77" y="60" text-anchor="middle"
font-family="monospace" font-size="8" fill="#8040a0" opacity="0.7">federation enabled</text>
</g>
<!-- ═══════════════════════════════════════════════════════════
AGENT CONTAINERS (bottom row)
═══════════════════════════════════════════════════════════ -->
<!-- manager: h-ruth -->
<g id="ruth" transform="translate(80,400)">
<rect x="0" y="0" width="160" height="100" rx="3"
fill="#140d00" stroke="#ffb300" stroke-width="1.5"/>
<text x="80" y="16" text-anchor="middle"
font-family="monospace" font-size="10" fill="#ffb300" letter-spacing="1"
filter="url(#glow)">h-ruth</text>
<line x1="8" y1="21" x2="152" y2="21" stroke="#ffb300" stroke-width="0.4" opacity="0.4"/>
<text x="80" y="34" text-anchor="middle"
font-family="monospace" font-size="8" fill="#c89000" opacity="0.8">manager agent</text>
<text x="80" y="47" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#7a5500" opacity="0.9">privileged MCP surface</text>
<text x="80" y="60" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#7a5500" opacity="0.9">approval gating</text>
<text x="80" y="73" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#7a5500" opacity="0.9">config proposals</text>
<text x="80" y="90" text-anchor="middle"
font-family="monospace" font-size="7" fill="#4a3500" opacity="0.8">unix sock + web UI</text>
</g>
<!-- sub-agent 1 -->
<g id="agent1" transform="translate(280,400)">
<rect x="0" y="0" width="140" height="100" rx="3"
fill="#0d0c14" stroke="#7070d0" stroke-width="1"/>
<text x="70" y="16" text-anchor="middle"
font-family="monospace" font-size="10" fill="#9090e0" letter-spacing="1">h-iris</text>
<line x1="8" y1="21" x2="132" y2="21" stroke="#7070d0" stroke-width="0.4" opacity="0.4"/>
<text x="70" y="34" text-anchor="middle"
font-family="monospace" font-size="8" fill="#6060b0" opacity="0.8">sub-agent</text>
<text x="70" y="47" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#4a4a8a" opacity="0.9">claude + MCP tools</text>
<text x="70" y="60" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#4a4a8a" opacity="0.9">send/recv/ask</text>
<text x="70" y="73" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#4a4a8a" opacity="0.9">bash tasks</text>
<text x="70" y="90" text-anchor="middle"
font-family="monospace" font-size="7" fill="#35356a" opacity="0.8">unix sock + web UI</text>
</g>
<!-- sub-agent 2 -->
<g id="agent2" transform="translate(455,400)">
<rect x="0" y="0" width="140" height="100" rx="3"
fill="#0d0c14" stroke="#7070d0" stroke-width="1"/>
<text x="70" y="16" text-anchor="middle"
font-family="monospace" font-size="10" fill="#9090e0" letter-spacing="1">h-damocles</text>
<line x1="8" y1="21" x2="132" y2="21" stroke="#7070d0" stroke-width="0.4" opacity="0.4"/>
<text x="70" y="34" text-anchor="middle"
font-family="monospace" font-size="8" fill="#6060b0" opacity="0.8">sub-agent</text>
<text x="70" y="47" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#4a4a8a" opacity="0.9">claude + MCP tools</text>
<text x="70" y="60" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#4a4a8a" opacity="0.9">NixOS plumbing</text>
<text x="70" y="73" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#4a4a8a" opacity="0.9">harness ergonomics</text>
<text x="70" y="90" text-anchor="middle"
font-family="monospace" font-size="7" fill="#35356a" opacity="0.8">unix sock + web UI</text>
</g>
<!-- sub-agent N (ellipsis) -->
<g id="agentN" transform="translate(630,400)">
<rect x="0" y="0" width="130" height="100" rx="3"
fill="#0d0c14" stroke="#3a3a70" stroke-width="1" stroke-dasharray="4,3"/>
<text x="65" y="16" text-anchor="middle"
font-family="monospace" font-size="10" fill="#505080" letter-spacing="1">h-&lt;name&gt;</text>
<line x1="8" y1="21" x2="122" y2="21" stroke="#3a3a70" stroke-width="0.4" opacity="0.3"/>
<text x="65" y="55" text-anchor="middle"
font-family="monospace" font-size="20" fill="#3a3a70" opacity="0.6">···</text>
<text x="65" y="90" text-anchor="middle"
font-family="monospace" font-size="7" fill="#2a2a50" opacity="0.8">any number of agents</text>
</g>
<!-- ═══════════════════════════════════════════════════════════
CONNECTIONS (static lines)
═══════════════════════════════════════════════════════════ -->
<!-- operator → gateway (:80) -->
<path d="M190,84 Q290,84 612,86" fill="none" stroke="#d4c400"
stroke-width="1" opacity="0.5" marker-end="url(#arr-dim)"/>
<text x="370" y="79" text-anchor="middle"
font-family="monospace" font-size="7.5" fill="#a09200" opacity="0.6">:80</text>
<!-- operator → c0re (dashboard :7000 fallback) -->
<path d="M125,113 L125,180 Q125,240 290,292"
fill="none" stroke="#ffb300" stroke-width="1" opacity="0.35"
marker-end="url(#arr)" stroke-dasharray="5,4"/>
<text x="96" y="230" text-anchor="middle"
font-family="monospace" font-size="7" fill="#7a5500" opacity="0.7"
transform="rotate(-90,96,230)">:7000</text>
<!-- gateway ↔ c0re (proxies /agent/* to per-agent unix sockets via c0re's socket map) -->
<path d="M610,86 Q530,86 481,240" fill="none" stroke="#d4c400"
stroke-width="1" opacity="0.4" marker-end="url(#arr-dim)"/>
<!-- c0re ↔ forge -->
<path d="M480,275 Q545,236 610,196" fill="none" stroke="#3ab8b8"
stroke-width="1" opacity="0.45" marker-end="url(#arr-dim)"/>
<text x="548" y="228" text-anchor="middle"
font-family="monospace" font-size="7" fill="#2a9090" opacity="0.7">config mirror</text>
<!-- c0re ↔ matrix -->
<path d="M480,292 Q545,292 610,306" fill="none" stroke="#b060c8"
stroke-width="1" opacity="0.4" marker-end="url(#arr-dim)"/>
<!-- c0re ↔ h-ruth (manager socket) -->
<path d="M340,344 L200,400" fill="none" stroke="#ffb300"
stroke-width="1.2" opacity="0.55" marker-end="url(#arr)"/>
<text x="248" y="378" text-anchor="middle"
font-family="monospace" font-size="7" fill="#7a5500" opacity="0.7"
transform="rotate(-32,248,378)">manager sock</text>
<!-- c0re ↔ h-iris -->
<path d="M385,344 L360,400" fill="none" stroke="#7070d0"
stroke-width="1" opacity="0.5" marker-end="url(#arr-dim)"/>
<!-- c0re ↔ h-damocles -->
<path d="M420,344 L490,400" fill="none" stroke="#7070d0"
stroke-width="1" opacity="0.5" marker-end="url(#arr-dim)"/>
<!-- c0re ↔ h-name -->
<path d="M460,344 Q580,360 630,400" fill="none" stroke="#3a3a70"
stroke-width="1" opacity="0.4" marker-end="url(#arr-dim)" stroke-dasharray="3,3"/>
<!-- broker label (on the c0re box, with arrows down to agents) -->
<text x="382" y="366" text-anchor="middle"
font-family="monospace" font-size="7" fill="#ffb300" opacity="0.45"
letter-spacing="1">sqlite broker</text>
<!-- ═══════════════════════════════════════════════════════════
ANIMATED MESSAGE DOTS (broker traffic between agents)
═══════════════════════════════════════════════════════════ -->
<!-- dot: iris → c0re broker (send) -->
<circle r="3" fill="#9090e0" filter="url(#glow)" opacity="0.9">
<animateMotion dur="2.8s" repeatCount="indefinite" begin="0s">
<mpath href="#path-iris-up"/>
</animateMotion>
<animate attributeName="opacity" values="0;0.9;0.9;0" dur="2.8s" repeatCount="indefinite"/>
</circle>
<!-- dot: c0re → ruth (deliver) -->
<circle r="3" fill="#ffb300" filter="url(#glow)" opacity="0.9">
<animateMotion dur="2.8s" repeatCount="indefinite" begin="0.7s">
<mpath href="#path-c0re-ruth"/>
</animateMotion>
<animate attributeName="opacity" values="0;0.9;0.9;0" dur="2.8s" repeatCount="indefinite" begin="0.7s"/>
</circle>
<!-- dot: c0re → iris (deliver) -->
<circle r="3" fill="#9090e0" filter="url(#glow)" opacity="0.9">
<animateMotion dur="3.4s" repeatCount="indefinite" begin="1.4s">
<mpath href="#path-c0re-iris"/>
</animateMotion>
<animate attributeName="opacity" values="0;0.9;0.9;0" dur="3.4s" repeatCount="indefinite" begin="1.4s"/>
</circle>
<!-- dot: c0re → damocles (deliver) -->
<circle r="3" fill="#9090e0" filter="url(#glow)" opacity="0.9">
<animateMotion dur="3.1s" repeatCount="indefinite" begin="2.1s">
<mpath href="#path-c0re-dam"/>
</animateMotion>
<animate attributeName="opacity" values="0;0.9;0.9;0" dur="3.1s" repeatCount="indefinite" begin="2.1s"/>
</circle>
<!-- dot: forge config push (c0re → forge) -->
<circle r="2.5" fill="#3ab8b8" filter="url(#glow)" opacity="0.8">
<animateMotion dur="4s" repeatCount="indefinite" begin="1s">
<mpath href="#path-forge"/>
</animateMotion>
<animate attributeName="opacity" values="0;0.8;0.8;0" dur="4s" repeatCount="indefinite" begin="1s"/>
</circle>
<!-- dot: operator → gateway -->
<circle r="2.5" fill="#d4c400" filter="url(#glow)" opacity="0.85">
<animateMotion dur="2.2s" repeatCount="indefinite" begin="0.3s">
<mpath href="#path-op-gw"/>
</animateMotion>
<animate attributeName="opacity" values="0;0.85;0.85;0" dur="2.2s" repeatCount="indefinite" begin="0.3s"/>
</circle>
<!-- dot: gateway → agent sock (response) -->
<circle r="2.5" fill="#d4c400" filter="url(#glow)" opacity="0.8">
<animateMotion dur="2.2s" repeatCount="indefinite" begin="1.2s">
<mpath href="#path-gw-c0re"/>
</animateMotion>
<animate attributeName="opacity" values="0;0.8;0.8;0" dur="2.2s" repeatCount="indefinite" begin="1.2s"/>
</circle>
<!-- invisible paths for animateMotion hrefs -->
<path id="path-iris-up" d="M360,400 L385,344" fill="none"/>
<path id="path-c0re-ruth" d="M340,344 L200,400" fill="none"/>
<path id="path-c0re-iris" d="M385,344 L360,400" fill="none"/>
<path id="path-c0re-dam" d="M420,344 L490,400" fill="none"/>
<path id="path-forge" d="M480,275 Q545,236 610,196" fill="none"/>
<path id="path-op-gw" d="M190,84 Q290,84 612,86" fill="none"/>
<path id="path-gw-c0re" d="M610,86 Q530,86 481,240" fill="none"/>
<!-- ═══════════════════════════════════════════════════════════
PULSING RINGS on key nodes
═══════════════════════════════════════════════════════════ -->
<!-- c0re pulse -->
<circle cx="385" cy="292" r="72" fill="none" stroke="#ffb300" stroke-width="1" opacity="0">
<animate attributeName="r" values="72;90;72" dur="3s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.18;0;0.18" dur="3s" repeatCount="indefinite"/>
</circle>
<!-- ═══════════════════════════════════════════════════════════
LEGEND
═══════════════════════════════════════════════════════════ -->
<g transform="translate(790,400)">
<rect x="0" y="0" width="185" height="165" rx="3"
fill="#0d0900" stroke="#3a2a00" stroke-width="1"/>
<text x="92" y="16" text-anchor="middle"
font-family="monospace" font-size="9" fill="#7a5500" letter-spacing="1">LEGEND</text>
<line x1="8" y1="21" x2="177" y2="21" stroke="#3a2a00" stroke-width="0.6"/>
<!-- host sockets -->
<line x1="12" y1="35" x2="48" y2="35" stroke="#ffb300" stroke-width="1.5" opacity="0.7"/>
<text x="55" y="39" font-family="monospace" font-size="8" fill="#7a5500">unix sockets (control)</text>
<!-- broker -->
<line x1="12" y1="55" x2="48" y2="55" stroke="#9090e0" stroke-width="1" opacity="0.7"/>
<text x="55" y="59" font-family="monospace" font-size="8" fill="#7a5500">broker messages</text>
<!-- web proxy -->
<line x1="12" y1="75" x2="48" y2="75" stroke="#d4c400" stroke-width="1" opacity="0.7"/>
<text x="55" y="79" font-family="monospace" font-size="8" fill="#7a5500">HTTP / web proxy</text>
<!-- config -->
<line x1="12" y1="95" x2="48" y2="95" stroke="#3ab8b8" stroke-width="1" opacity="0.7"/>
<text x="55" y="99" font-family="monospace" font-size="8" fill="#7a5500">config / git</text>
<!-- matrix -->
<line x1="12" y1="115" x2="48" y2="115" stroke="#b060c8" stroke-width="1" opacity="0.7"/>
<text x="55" y="119" font-family="monospace" font-size="8" fill="#7a5500">Matrix homeserver</text>
<!-- optional -->
<line x1="12" y1="135" x2="48" y2="135" stroke="#3a3a70" stroke-width="1"
stroke-dasharray="4,3" opacity="0.7"/>
<text x="55" y="139" font-family="monospace" font-size="8" fill="#7a5500">optional / many</text>
<text x="92" y="158" text-anchor="middle"
font-family="monospace" font-size="7" fill="#3a2a00" opacity="0.8">optional containers dashed</text>
</g>
<!-- ═══════════════════════════════════════════════════════════
FOOTER
═══════════════════════════════════════════════════════════ -->
<text x="500" y="608" text-anchor="middle"
font-family="monospace" font-size="8" fill="#3a2a00" letter-spacing="2">
hyperhive — identity = unix socket · communication = broker · config = git
</text>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB