Files
trade-doc/mevnode-bot-topology-interactive.html

1083 lines
35 KiB
HTML

<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>mevnode interactive topology</title>
<style>
:root {
--bg: #f2ede5;
--paper: rgba(255, 251, 244, 0.92);
--panel: #fffdf8;
--ink: #1e1a16;
--muted: #665f55;
--line: #d8c9b3;
--read: #0d6d73;
--write: #b7412f;
--data: #495565;
--ops: #8a6a12;
--focus: #171411;
--shadow: 0 20px 50px rgba(30, 26, 22, 0.10);
--radius: 18px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(13, 109, 115, 0.12), transparent 28%),
radial-gradient(circle at top right, rgba(183, 65, 47, 0.12), transparent 24%),
linear-gradient(180deg, #faf6ef 0%, var(--bg) 100%);
}
.page {
max-width: 1600px;
margin: 0 auto;
padding: 28px 18px 40px;
}
.topbar {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 20px;
margin-bottom: 18px;
}
.hero,
.panel,
.card,
.inspector,
.legend {
background: var(--paper);
border: 1px solid rgba(216, 201, 179, 0.9);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
}
.hero {
padding: 24px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(30, 26, 22, 0.10);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
h1 {
margin: 14px 0 10px;
font-size: clamp(2rem, 4vw, 3.7rem);
line-height: 0.98;
font-family: "IBM Plex Serif", Georgia, serif;
}
p {
margin: 0;
color: var(--muted);
line-height: 1.55;
}
.legend {
padding: 18px 20px;
display: grid;
gap: 10px;
align-content: start;
}
.legend-row {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
}
.dot {
width: 12px;
height: 12px;
border-radius: 999px;
flex: 0 0 12px;
}
.dot.read { background: var(--read); }
.dot.write { background: var(--write); }
.dot.data { background: var(--data); }
.dot.ops { background: var(--ops); }
.toolbar {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 18px 0 8px;
}
.filter {
border: 1px solid var(--line);
background: #fffaf4;
color: var(--ink);
border-radius: 999px;
padding: 10px 14px;
font: inherit;
font-weight: 700;
cursor: pointer;
transition: 140ms ease;
}
.filter:hover,
.filter.active {
transform: translateY(-1px);
border-color: rgba(30, 26, 22, 0.18);
box-shadow: 0 10px 22px rgba(30, 26, 22, 0.08);
}
.filter[data-filter="read"].active { color: var(--read); }
.filter[data-filter="write"].active { color: var(--write); }
.filter[data-filter="data"].active { color: var(--data); }
.filter[data-filter="ops"].active { color: var(--ops); }
.layout {
display: grid;
grid-template-columns: minmax(900px, 1.35fr) minmax(300px, 0.65fr);
gap: 18px;
align-items: start;
}
.panel {
padding: 16px;
overflow: auto;
}
.canvas {
position: relative;
min-height: 920px;
border-radius: 16px;
background:
linear-gradient(rgba(216, 201, 179, 0.18) 1px, transparent 1px),
linear-gradient(90deg, rgba(216, 201, 179, 0.18) 1px, transparent 1px),
linear-gradient(180deg, #fffdf9, #fff9f1);
background-size: 28px 28px, 28px 28px, 100% 100%;
padding: 14px;
}
.network {
display: grid;
grid-template-columns: 320px 120px 1fr;
gap: 18px;
align-items: stretch;
min-height: 100%;
}
.col {
display: grid;
gap: 14px;
align-content: start;
}
.node-title {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
margin-bottom: 4px;
padding: 4px 2px 8px;
}
.node-title h2 {
margin: 0;
font-size: 1.35rem;
font-family: "IBM Plex Serif", Georgia, serif;
}
.node-title span {
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
.card {
padding: 15px;
background: var(--panel);
border-left: 6px solid var(--line);
cursor: pointer;
transition: 160ms ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 16px 30px rgba(30, 26, 22, 0.10);
}
.card.active {
border-color: var(--focus);
box-shadow: 0 18px 35px rgba(30, 26, 22, 0.15);
transform: translateY(-2px) scale(1.01);
}
.card.dimmed,
.link.dimmed,
.lane-label.dimmed {
opacity: 0.22;
}
.card.read { border-left-color: var(--read); }
.card.write { border-left-color: var(--write); }
.card.data { border-left-color: var(--data); }
.card.ops { border-left-color: var(--ops); }
.card h3 {
margin: 0 0 6px;
font-size: 1rem;
}
.meta {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 700;
margin-bottom: 8px;
}
.chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.chip {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: rgba(30, 26, 22, 0.06);
color: var(--ink);
}
.middle {
position: relative;
display: grid;
grid-template-rows: repeat(5, 1fr);
align-items: center;
justify-items: center;
gap: 14px;
padding: 42px 0;
}
.link {
width: 6px;
height: 100%;
border-radius: 999px;
position: relative;
min-height: 90px;
transition: opacity 140ms ease, transform 140ms ease;
}
.link::after {
content: "";
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 10px solid currentColor;
color: inherit;
}
.link.read { background: var(--read); color: var(--read); }
.link.write { background: var(--write); color: var(--write); }
.link.data { background: var(--data); color: var(--data); }
.link.ops { background: var(--ops); color: var(--ops); }
.lane-label {
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
text-align: center;
max-width: 100px;
}
.inspector {
padding: 18px;
position: sticky;
top: 18px;
}
.inspector h2 {
margin: 0 0 8px;
font-size: 1.25rem;
font-family: "IBM Plex Serif", Georgia, serif;
}
.inspector .kind {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 800;
margin-bottom: 14px;
}
.inspector dl {
margin: 18px 0 0;
display: grid;
gap: 12px;
}
.inspector dt {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 800;
margin-bottom: 4px;
}
.inspector dd {
margin: 0;
line-height: 1.5;
color: var(--ink);
}
.inspector ul {
margin: 6px 0 0;
padding-left: 18px;
}
.inspector li { margin: 6px 0; }
.hint {
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--line);
font-size: 14px;
color: var(--muted);
}
.service-map {
margin-top: 18px;
display: grid;
gap: 18px;
}
.map-title {
padding: 4px 2px;
}
.map-title h2 {
margin: 0 0 6px;
font-size: 1.5rem;
font-family: "IBM Plex Serif", Georgia, serif;
}
.namespaces {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.namespace {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 16px;
padding: 16px;
}
.namespace h3 {
margin: 0 0 4px;
font-size: 1.02rem;
}
.namespace small {
display: block;
color: var(--muted);
margin-bottom: 12px;
line-height: 1.45;
}
.k8s-item {
border: 1px solid rgba(216, 201, 179, 0.9);
border-radius: 14px;
padding: 12px;
background: #fffdf8;
margin-bottom: 10px;
}
.k8s-item:last-child { margin-bottom: 0; }
.k8s-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.k8s-head strong {
display: block;
font-size: 0.98rem;
}
.type {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 800;
padding: 5px 8px;
border-radius: 999px;
background: rgba(30, 26, 22, 0.06);
white-space: nowrap;
}
.status.ok {
color: #17603b;
background: rgba(23, 96, 59, 0.10);
}
.status.warn {
color: #8d3a14;
background: rgba(183, 65, 47, 0.10);
}
.k8s-item p {
font-size: 14px;
}
code {
font-family: "IBM Plex Mono", monospace;
background: rgba(30, 26, 22, 0.06);
padding: 2px 6px;
border-radius: 6px;
}
@media (max-width: 1240px) {
.topbar,
.layout {
grid-template-columns: 1fr;
}
.network {
grid-template-columns: 1fr;
}
.middle {
grid-template-columns: repeat(5, 1fr);
grid-template-rows: none;
padding: 0;
}
.link {
width: 100%;
height: 6px;
min-height: 6px;
}
.link::after {
right: -4px;
left: auto;
bottom: 50%;
transform: translateY(50%) rotate(-90deg);
}
.lane-label {
max-width: none;
}
.inspector {
position: static;
}
}
</style>
</head>
<body>
<div class="page">
<section class="topbar">
<div class="hero">
<div class="eyebrow">Interactive topology / click to inspect</div>
<h1>Interaktywny diagram uslug na <code>mevnode_bot</code> i zaleznosci od <code>mevnode_sol</code></h1>
<p>
Klikaj komponenty albo filtruj sciezki. Diagram rozdziela read plane,
write plane, data plane i ops plane. Celem jest utrzymanie twardego podzialu:
<strong>validator zostaje na sol</strong>, a <strong>produkt, ingest i execution</strong>
dzialaja na bot w <code>k3s</code>.
</p>
<div class="toolbar" id="filters">
<button class="filter active" data-filter="all">All</button>
<button class="filter" data-filter="read">Read path</button>
<button class="filter" data-filter="write">Write path</button>
<button class="filter" data-filter="data">Data plane</button>
<button class="filter" data-filter="ops">Ops plane</button>
</div>
</div>
<aside class="legend">
<div class="legend-row"><span class="dot read"></span> Read: Yellowstone, ingest, DLOB read side</div>
<div class="legend-row"><span class="dot write"></span> Write: tx-router, RPC, TPU, Jito</div>
<div class="legend-row"><span class="dot data"></span> Data: Postgres, Redis, Hasura</div>
<div class="legend-row"><span class="dot ops"></span> Ops: frontend, API, Grafana, Portainer, GitOps</div>
<p>
Klikniecie komponentu otwiera po prawej opis roli, wejsc, wyjsc i punktow ryzyka.
</p>
</aside>
</section>
<section class="layout">
<div class="panel">
<div class="canvas">
<div class="network">
<div class="col" id="left-col">
<div class="node-title">
<h2>mevnode_sol</h2>
<span>bare metal</span>
</div>
<article class="card read active" data-id="agave" data-kind="read ops">
<div class="meta">validator / rpc</div>
<h3>Agave validator</h3>
<p>Source of truth dla chain state, snapshotow i lokalnego RPC.</p>
<div class="chips">
<span class="chip">127.0.0.1:8899</span>
<span class="chip">getAccountInfo</span>
<span class="chip">getMultipleAccounts</span>
</div>
</article>
<article class="card read" data-id="yellowstone" data-kind="read">
<div class="meta">geyser / live stream</div>
<h3>Yellowstone gRPC</h3>
<p>Pushuje account updates, sloty, tx i block meta po <code>wg0</code>.</p>
<div class="chips">
<span class="chip">10.91.0.1:10000</span>
<span class="chip">x-token</span>
</div>
</article>
<article class="card ops" data-id="sol-metrics" data-kind="ops">
<div class="meta">monitoring</div>
<h3>Plugin metrics</h3>
<p>Metryki pluginu i gRPC do Prometheusa i diagnostyki.</p>
<div class="chips">
<span class="chip">10.91.0.1:8999</span>
</div>
</article>
</div>
<div class="middle">
<div class="link read" data-kind="read"></div>
<div class="lane-label" data-kind="read">yellowstone / live feed</div>
<div class="link write" data-kind="write"></div>
<div class="lane-label" data-kind="write">rpc / tpu / jito</div>
<div class="link ops" data-kind="ops"></div>
</div>
<div class="col" id="right-col">
<div class="node-title">
<h2>mevnode_bot</h2>
<span>single-node k3s</span>
</div>
<article class="card read" data-id="yellowstone-consumer" data-kind="read">
<div class="meta">trade namespace</div>
<h3>yellowstone-consumer</h3>
<p>Bierze live feed z Yellowstone i zamienia go na dane wewnetrzne.</p>
<div class="chips">
<span class="chip">accounts</span>
<span class="chip">slots</span>
<span class="chip">transactions</span>
</div>
</article>
<article class="card read" data-id="trade-ingestor" data-kind="read data">
<div class="meta">trade namespace</div>
<h3>trade-ingestor</h3>
<p>Transformuje dane i zapisuje je do DB lub feedow uslugowych.</p>
</article>
<article class="card read data" data-id="dlob-publisher" data-kind="read data">
<div class="meta">trade namespace</div>
<h3>dlob-publisher</h3>
<p>Czyta RPC Agavy i buduje stan DLOB, ktory trafia do Redis.</p>
<div class="chips">
<span class="chip">getMultipleAccounts</span>
<span class="chip">Redis</span>
</div>
</article>
<article class="card read data" data-id="dlob-server" data-kind="read data ops">
<div class="meta">trade namespace</div>
<h3>dlob-server</h3>
<p>Serwuje gotowy widok DLOB dla API i wizualizacji.</p>
</article>
<article class="card write" data-id="strategy-engine" data-kind="write">
<div class="meta">execution</div>
<h3>strategy-engine</h3>
<p>Konsumuje feed i decyduje o akcjach tradingowych.</p>
</article>
<article class="card write" data-id="tx-router" data-kind="write">
<div class="meta">execution</div>
<h3>tx-router</h3>
<p>Wybiera write path: <code>RPC</code>, <code>TPU</code> albo <code>Jito</code>.</p>
</article>
<article class="card data" data-id="postgres" data-kind="data">
<div class="meta">data namespace</div>
<h3>Postgres + Hasura</h3>
<p>Trwaly storage, stan aplikacji, historia, pozycje i GraphQL.</p>
</article>
<article class="card data" data-id="redis" data-kind="data">
<div class="meta">data namespace</div>
<h3>Redis</h3>
<p>Cache, pub/sub i szybki stan DLOB dla serwisow read-side.</p>
</article>
<article class="card ops" data-id="frontend-api" data-kind="ops">
<div class="meta">web namespace</div>
<h3>Frontend + API + Ingress</h3>
<p>Warstwa produktu. UI pokazuje dane, API agreguje odpowiedzi dla klienta.</p>
</article>
<article class="card ops" data-id="ops-stack" data-kind="ops">
<div class="meta">ops namespace</div>
<h3>Prometheus / Grafana / Portainer / GitOps</h3>
<p>Monitoring, obsluga klastra, deployment z Gitea i obserwowalnosc.</p>
</article>
</div>
</div>
</div>
</div>
<aside class="inspector" id="inspector">
<div class="kind" id="inspector-kind">Selected component</div>
<h2 id="inspector-title">Agave validator</h2>
<p id="inspector-text">
Bazowy komponent po stronie <code>mevnode_sol</code>. Utrzymuje ledger, snapshoty,
lokalne RPC i stan kont. Bez zdrowego RPC nie przejdzie bootstrap DLOB publishera.
</p>
<dl>
<div>
<dt>Inputs</dt>
<dd id="inspector-inputs">entrypoints, snapshots, accounts state, network gossip</dd>
</div>
<div>
<dt>Outputs</dt>
<dd id="inspector-outputs">RPC responses, local chain state, base data for Yellowstone</dd>
</div>
<div>
<dt>Why it exists</dt>
<dd id="inspector-why">To jest zrodlo prawdy dla chain access i jedyne miejsce, gdzie stoi validator.</dd>
</div>
<div>
<dt>Risk / note</dt>
<dd id="inspector-risk">Nie przenosimy tego na bot. Read plane i execution plane maja pozostac oddzielone.</dd>
</div>
</dl>
<div class="hint">
Kliknij kolejny komponent albo wybierz filtr u gory, zeby przygaszac pozostale warstwy.
</div>
</aside>
</section>
<section class="service-map">
<div class="map-title">
<h2>k3s object map</h2>
<p>
To jest aktualny rozklad obiektow w klastrze <code>mevnode_bot</code>:
workloady, uslugi i typy ekspozycji. Tu widac juz konkretne
<code>Deployment</code>, <code>StatefulSet</code>, <code>ClusterIP</code>,
<code>NodePort</code> i stan gotowosci.
</p>
</div>
<div class="namespaces">
<section class="namespace">
<h3>kube-system</h3>
<small>Podstawowe komponenty klastra potrzebne, zeby single-node k3s dzialal stabilnie.</small>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>metrics-server</strong>
<div class="type">Deployment + ClusterIP</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>metrics-server</code> na porcie <code>443</code>.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>coredns</strong>
<div class="type">Deployment + ClusterIP</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>kube-dns</code> z DNS wewnatrz klastra.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>local-path-provisioner</strong>
<div class="type">Deployment</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Provisioning lokalnego storage dla PVC na pojedynczym nodzie.</p>
</article>
</section>
<section class="namespace">
<h3>portainer</h3>
<small>Most pomiedzy Dockerowym Portainerem na hoscie a lokalnym klastrem k3s.</small>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>portainer-agent</strong>
<div class="type">Deployment + NodePort</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>portainer-agent</code> wystawiony jako <code>NodePort 30778 -> 9001</code>.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>portainer-agent-headless</strong>
<div class="type">Headless Service</div>
</div>
<span class="status ok">cluster wiring</span>
</div>
<p>Uzywany przez agenta do wewnetrznego cluster discovery.</p>
</article>
</section>
<section class="namespace">
<h3>trade-staging</h3>
<small>Glowny namespace produktu: web, ingest, DLOB i bazy danych.</small>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>trade-frontend</strong>
<div class="type">Deployment + NodePort</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>trade-frontend</code>: <code>NodePort 30081 -> 8081</code>.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>trade-api</strong>
<div class="type">Deployment + ClusterIP</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>trade-api</code> na porcie <code>8787</code>.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>trade-ingestor</strong>
<div class="type">Deployment</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Ingest pipeline bez osobnego Service. Przetwarza dane w tle.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>dlob-redis</strong>
<div class="type">Deployment + ClusterIP</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>dlob-redis</code> na porcie <code>6379</code>.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>dlob-server</strong>
<div class="type">Deployment + ClusterIP</div>
</div>
<span class="status warn">0/1 not ready</span>
</div>
<p>Service <code>dlob-server</code> na porcie <code>6969</code>. Zalezy od poprawnie napelnionego stanu DLOB.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>dlob-publisher</strong>
<div class="type">Deployment</div>
</div>
<span class="status warn">0/1 not ready</span>
</div>
<p>Nie ma Service. To komponent zalezn y od zdrowego Agave RPC i bootstrapu Drift account reads.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>dlob-worker / depth / slippage</strong>
<div class="type">Deployments</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Workery pochodnych widokow orderbooka, bez ekspozycji zewnetrznej.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>postgres</strong>
<div class="type">StatefulSet + Headless Service</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>postgres</code> na <code>5432</code>, headless dla stabilnej tozsamosci StatefulSet.</p>
</article>
<article class="k8s-item">
<div class="k8s-head">
<div>
<strong>hasura</strong>
<div class="type">Deployment + ClusterIP</div>
</div>
<span class="status ok">1/1 ready</span>
</div>
<p>Service <code>hasura</code> na porcie <code>8080</code>.</p>
</article>
</section>
</div>
</section>
</div>
<script>
const model = {
agave: {
title: 'Agave validator',
kind: 'read / ops / source of truth',
text: 'Bazowy komponent po stronie mevnode_sol. Utrzymuje ledger, snapshoty, lokalne RPC i stan kont. Bez zdrowego RPC nie przejdzie bootstrap DLOB publishera.',
inputs: 'entrypoints, snapshots, accounts state, network gossip',
outputs: 'RPC responses, local chain state, feed source for Yellowstone plugin',
why: 'Validator i chain access zostaja tylko na sol. Bot nie ma nigdy Agave ani ledgera.',
risk: 'Jesli read RPC dusi sie na getAccountInfo/getMultipleAccounts, read side na bot traci bootstrap.'
},
yellowstone: {
title: 'Yellowstone gRPC',
kind: 'read / live stream',
text: 'Plugin Geyser wystawia pushowy stream account updates, slotow, transakcji i block meta. To jest glowny low-latency feed dla bot.',
inputs: 'Agave runtime, Geyser plugin callbacks, x-token auth',
outputs: 'gRPC subscriptions na 10.91.0.1:10000',
why: 'Live stream jest lepszy do ingestu niz polling RPC dla wszystkiego.',
risk: 'To nie zastapi wszystkich request/response z RPC. Biblioteki dalej moga potrzebowac punktowych odczytow kont.'
},
'sol-metrics': {
title: 'Plugin metrics',
kind: 'ops / observability',
text: 'Metryki gRPC i pluginu Geyser wystawione po wg0 do diagnostyki.',
inputs: 'Agave plugin internals',
outputs: 'Prometheus metrics na 10.91.0.1:8999',
why: 'Bez metryk ciezko diagnozowac lagi, reconnecty i zachowanie snapshot/replay.',
risk: 'To jest tylko read-only ops path, nie sluzy do write path.'
},
'yellowstone-consumer': {
title: 'yellowstone-consumer',
kind: 'read / k3s workload',
text: 'Pierwszy komponent po stronie bot, ktory konsumuje live stream i mapuje go na wewnetrzne modele.',
inputs: 'Yellowstone gRPC subscriptions: accounts, slots, transactions',
outputs: 'events dla strategy-engine, ingestora i dalszych workerow',
why: 'Oddziela surowy stream chaina od logiki biznesowej.',
risk: 'Musi byc lekki i odporny na reconnect. Nie powinien wykonywac ciezkich transformacji w goracej sciezce.'
},
'trade-ingestor': {
title: 'trade-ingestor',
kind: 'read / data',
text: 'Warstwa transformacji i zapisu. Odbiera dane z consumerow i utrzymuje je w ksztalcie uzytecznym dla reszty stacku.',
inputs: 'parsed chain events, market data, application transforms',
outputs: 'Postgres, Redis, internal service feeds',
why: 'Normalizuje dane przed API i frontendem.',
risk: 'Jesli zrobi sie za ciezki, zacznie opozniac caly read path.'
},
'dlob-publisher': {
title: 'dlob-publisher',
kind: 'read / data / drift-specific',
text: 'Buduje stan DLOB. Nadal potrzebuje zdrowego Agave RPC do bootstrapu i odczytow kont typu getMultipleAccounts.',
inputs: 'Agave RPC, opcjonalnie Yellowstone stream, Drift account reads',
outputs: 'Redis state dla dlob-server i workerow',
why: 'To jest most miedzy chain state i orderbookiem widocznym dla produktu.',
risk: 'To byl glowny punkt blokady, kiedy Agave read RPC nie odpowiadalo na ciezsze account reads.'
},
'dlob-server': {
title: 'dlob-server',
kind: 'read / data / ops-facing',
text: 'Wystawia gotowy stan DLOB dla API, frontendu i ewentualnych klientow wewnetrznych.',
inputs: 'Redis, workers, precomputed depth/slippage state',
outputs: 'read endpoints dla produktu',
why: 'UI i API nie powinny bezposrednio dotykac surowego stanu z publishera.',
risk: 'Bez poprawnie napelnionego Redis serwuje pusty albo niepelny widok.'
},
'strategy-engine': {
title: 'strategy-engine',
kind: 'write / execution',
text: 'Tu zapada decyzja tradingowa. Konsumuje live feed i stan rynku, ale nie powinien robic wszystkiego sam.',
inputs: 'yellowstone-consumer, DLOB state, risk rules',
outputs: 'intencje zlecen przekazywane do order-manager / tx-router',
why: 'Rozdziela decyzje tradingowe od transportu transakcji.',
risk: 'Jesli tu wlozysz za duzo efektow ubocznych, trudno bedzie kontrolowac opoznienia i awarie.'
},
'tx-router': {
title: 'tx-router',
kind: 'write / transport',
text: 'Wybiera sciezke wysylki transakcji: RPC, TPU albo Jito. To osobna warstwa od strategii.',
inputs: 'signed transactions lub order intents',
outputs: 'RPC sendTransaction, TPU submit, Jito submit',
why: 'Read path i write path musza byc osobnymi decyzjami operacyjnymi.',
risk: 'Nie nalezy mieszac wyboru write path z warstwa UI ani z parserami read-side.'
},
postgres: {
title: 'Postgres + Hasura',
kind: 'data / durable storage',
text: 'Trwaly storage dla stanu aplikacji, historii, pozycji i konfiguracji. Hasura robi warstwe GraphQL nad tym modelem.',
inputs: 'trade-ingestor, API writes, migrations, bootstrap jobs',
outputs: 'query layer dla API i frontendu',
why: 'Jedno glowne zrodlo stanu aplikacji po stronie bot.',
risk: 'Nie warto mieszac tu feedu tick-by-tick, jesli pozniej pojawi sie potrzeba ciezszej analityki; wtedy lepszy jest ClickHouse.'
},
redis: {
title: 'Redis',
kind: 'data / fast state',
text: 'Szybki cache i stan posredni dla DLOB, pub/sub i workerow o niskiej latencji.',
inputs: 'dlob-publisher, workers, internal events',
outputs: 'dlob-server, API caches, ephemeral coordination',
why: 'Odciaza Postgresa z goracej sciezki read-side.',
risk: 'Nie moze byc jedynym trwalym zrodlem prawdy.'
},
'frontend-api': {
title: 'Frontend + API + Ingress',
kind: 'ops / product surface',
text: 'Warstwa produktu. Frontend pokazuje dane, a API agreguje odpowiedzi z DB i serwisow read-side.',
inputs: 'Postgres, Hasura, dlob-server, internal APIs',
outputs: 'HTTP/HTTPS dla uzytkownika i operatora',
why: 'Produkt ma zyc na bot, nie na hoście validatora.',
risk: 'Nie powinien miec bezposrednich zaleznosci do ledgera ani do samego procesu validatora.'
},
'ops-stack': {
title: 'Prometheus / Grafana / Portainer / GitOps',
kind: 'ops / platform',
text: 'Warstwa operacyjna klastra: monitoring, widocznosc workloadow w Portainerze i deployment z Gitea.',
inputs: 'k3s metrics, app metrics, manifests from Gitea',
outputs: 'dashboards, alerts, cluster visibility, controlled rollouts',
why: 'Przestajesz zarzadzac luznymi kontenerami i trzymasz runtime w jednym modelu.',
risk: 'To nie powinno siedziec na sol. Validator host ma byc maksymalnie prosty.'
}
};
const cards = [...document.querySelectorAll('.card')];
const filterButtons = [...document.querySelectorAll('.filter')];
const links = [...document.querySelectorAll('.link')];
const laneLabels = [...document.querySelectorAll('.lane-label')];
const inspector = {
kind: document.getElementById('inspector-kind'),
title: document.getElementById('inspector-title'),
text: document.getElementById('inspector-text'),
inputs: document.getElementById('inspector-inputs'),
outputs: document.getElementById('inspector-outputs'),
why: document.getElementById('inspector-why'),
risk: document.getElementById('inspector-risk')
};
let activeFilter = 'all';
let activeCard = 'agave';
function renderInspector(id) {
const item = model[id];
if (!item) return;
inspector.kind.textContent = item.kind;
inspector.title.textContent = item.title;
inspector.text.innerHTML = item.text;
inspector.inputs.textContent = item.inputs;
inspector.outputs.textContent = item.outputs;
inspector.why.textContent = item.why;
inspector.risk.textContent = item.risk;
}
function applyFilter() {
cards.forEach((card) => {
const kinds = card.dataset.kind.split(' ');
const match = activeFilter === 'all' || kinds.includes(activeFilter);
card.classList.toggle('dimmed', !match);
});
links.forEach((link) => {
const match = activeFilter === 'all' || link.dataset.kind === activeFilter;
link.classList.toggle('dimmed', !match);
});
laneLabels.forEach((label) => {
const match = activeFilter === 'all' || label.dataset.kind === activeFilter;
label.classList.toggle('dimmed', !match);
});
filterButtons.forEach((button) => {
button.classList.toggle('active', button.dataset.filter === activeFilter);
});
}
cards.forEach((card) => {
card.addEventListener('click', () => {
activeCard = card.dataset.id;
cards.forEach((x) => x.classList.toggle('active', x.dataset.id === activeCard));
renderInspector(activeCard);
});
});
filterButtons.forEach((button) => {
button.addEventListener('click', () => {
activeFilter = button.dataset.filter;
applyFilter();
});
});
renderInspector(activeCard);
applyFilter();
</script>
</body>
</html>