Files
public/docs/dashy-soc-board.md
T
thesabear c10ff81d47 docs: add orchestration, registry, dashy, cron, and content pipeline write-ups
Completes the orchestration/registry/content batch - Kestra's
check-pause-approve-apply pattern, the self-hosted build pipeline,
the SOC dashboard's 14-source aggregation, the cron heatmap, and the
in-progress content pipeline, all in the diagram+callout+table style.
2026-08-22 07:36:47 +03:00

4.8 KiB

🖥️ Dashy SOC Board

Note

The homelab's "one pane of glass" — a single dashboard page backed by one large custom function, aggregating roughly a dozen distinct data sources that used to be scattered across separate pages into one glanceable board.

Fourteen data sources, one board

flowchart TD
    subgraph Sources["Data sources"]
        direction LR
        Wazuh["SOC indexer<br/>+ compliance API"]
        Loki["Log aggregator<br/>24h error counts"]
        Prom["Metrics<br/>CPU/RAM/disk"]
        Slack["Alert channels<br/>per app + per host"]
        Orch["Orchestration APIs<br/>execution health"]
        Audit["Fleet config-drift<br/>audit feed"]
        Hyper["Hypervisor API<br/>VM/LXC tree"]
        Vault["Secrets platform<br/>seal state, lease counts"]
        SSO["SSO provider<br/>login/failure stats"]
        DNS["DNS server<br/>query stats, blocklist"]
        Backup["Backup server<br/>job age/status"]
        FW["Edge firewall<br/>state table, top rules"]
    end

    Sources --> Function["One aggregation function"]
    Function --> Board["SOC Board<br/>client-rendered"]

Layout

A top row shows a 6-tier infrastructure dependency ladder alongside fleet-wide 24-hour KPIs. Below that, an "Active Issues" strip surfaces the worst findings fleet-wide, followed by a grid of per-host tiles — worst-severity badge, error sparkline, sorted consistently. Clicking a tile expands a full detail panel: compliance and resource gauges, that host's folded-in specialty widget where relevant, and findings grouped by category.

Tip

Deliberately scoped: security/error/warning signal only. Per-app uptime is handled separately by the dashboard's own native status checks on individual tiles — the only up/down signal inside this board is infrastructure-tier reachability, not per-app availability. Keeping the scope narrow is what keeps the board actually scannable.

Acknowledge mechanism: client-side, no backend

flowchart LR
    Finding["Finding<br/>stable content-derived key"] --> Ack["User acknowledges"]
    Ack --> Storage["Browser localStorage<br/>flat map, per-browser"]
    Storage --> Recompute["Live re-derive:<br/>badges, issue strip, gauges"]
    Recompute -.never touches.-> Upstream["Wazuh / Slack / any<br/>upstream source"]

Important

Acknowledging a finding is a display filter only — it never writes back to any upstream source, is scoped per-browser rather than fleet-wide, and a full data set for every host is always kept in memory so the recompute has everything it needs regardless of which panel happens to be open. A reload in a different browser shows the finding again — a deliberate tradeoff favoring simplicity (no backend state to manage) over persistence.

Real integration gotchas hit building this

Warning

A few non-obvious lessons from actually building this, in case they save someone else the debugging time:

  • Embedding via fetch + innerHTML means injected <script> tags never execute — each one has to be explicitly re-created on every refresh to keep interactive elements working.
  • A background poll that could clobber an open detail panel needs to check "is a panel open" both before and after the fetch, since the aggregation endpoint itself can take tens of seconds to respond.
  • One backing API hard-caps results per page regardless of a higher requested limit — real pagination has to be implemented, not assumed away.
  • Templating engines scoped to the wrong object silently produce an empty string instead of erroring — an easy, quiet failure mode.
  • Replacing a bind-mounted config file's content doesn't get picked up by a plain container restart — the container keeps watching the old inode; a full recreate is required.

Widget design: shape follows data, not one template everywhere

Every widget's visual style is chosen to match the shape of the data it shows, not a single card/badge component reused everywhere:

Data shape Visual treatment
Proportion of a whole (disk %, capacity) Gauge or bar, not a badge
Hierarchy (host containing guests) Real visual nesting — indentation, a tree — not a flat grid
Time-ordered audit/status log A dense, sortable list ranked worst-first, recency as the dominant signal
Named media/content items A poster/cover-art grid, never a text list

Tip

The media-poster widgets route through one shared image-proxy endpoint rather than each hitting its own upstream API directly — since these are private, authenticated homelab APIs, not public image CDNs. One exception: a public poster CDN gets hit directly, since no auth or proxying is needed there.


Related stacks: stacks/platform/dashy/, stacks/platform/node-red/