Architecture
Ithiltir Dash is a single-instance application. One Dash process serves the Web UI, HTTP API, theme assets, install scripts, node binary downloads, and background services.
Runtime Boundary
Browser / Node
|
Reverse proxy
|
Ithiltir Dash
|
PostgreSQL + TimescaleDB
Redis
Dash is not designed for multiple active instances writing the same runtime state. Keep one active Dash process for one deployment.
Storage
| Store | Data |
|---|---|
| PostgreSQL | nodes, groups, metrics, node uptime, traffic facts, alert rules, alert events, notification outbox, settings, theme metadata |
| TimescaleDB | hypertables, retention policy, time-series compression path |
| Redis | admin sessions and disposable frontend caches |
| Filesystem | Dash config, notification encryption key, install identity, immutable releases, themes, and persistent update state |
Process memory holds the node auth index, alert pending/cooldown state, MTProto login handshakes, volatile Node update requests, and traffic rebuild state. --no-redis additionally moves sessions and frontend cache into process memory.
app.timezone is compiled during startup. Empty uses the local timezone; non-empty values must be valid IANA timezone names, otherwise config loading fails with the configured value in the error.
The admin update controller requires Linux/systemd. Manual dash update also supports explicit --service-manager=none. Jobs and transactions persist under $DASH_HOME/runtime/dash-update.
After the admin console starts a manual update, the browser session stores the target version. A root runtime blocks the stale UI and polls local /api/version until the backend job explicitly fails or the installed version reaches the target. Successful updates reload the document only after user confirmation.
SMART, thermal, and full RAID details are runtime state. On Linux, a root-side smartctl helper writes /run/ithiltir-node/smart.json, and a root-side /proc netns helper writes /run/ithiltir-node/connections.json; Ithiltir-node only reads those caches. SMART cache freshness, helper availability, device health, full thermal sensor payloads, and full RAID array/member payloads are kept in current snapshots or hot caches rather than historical PostgreSQL metric rows. SMART temperature for confirmed physical disks is reduced into disk_physical_metrics.temp_c; virtual disks and RAID devices are ignored. Thermal data is reduced into cpu_temp_c for host history, while full thermal details are split into a separate frontend field cache and composed back into front node JSON on read.
TCP/UDP connection counts are persisted numeric metrics. They are written to tcp_conn and udp_conn and remain available to history queries as conn.tcp and conn.udp. On Linux, full host/netns connection counts come from the root-side connections cache because Node runs with low privileges. The Linux installer compiles this helper locally when cc, gcc, or clang is available. If the cache is missing or stale, or the helper cannot be compiled, Node uses its built-in connection counting, which may miss container connections.
Linux PSI pressure metrics are fixed numeric time-series data. PSI avg10, avg60, avg300, and total values are stored as nullable columns on server_metrics and server_current_metrics; missing columns mean unavailable data, not zero pressure. Collection reason/status strings are ignored by dashboard persistence. PSI is not currently wired into alert evaluation.
Node Reporting
Nodes normally run in push mode:
Ithiltir-node -> /api/node/metrics
Ithiltir-node -> /api/node/static
Dash does not need to open inbound connections to nodes. The node secret authenticates /api/node/* requests and packaged /deploy/* asset downloads, and must not be exposed in browser code.
Node IP is an observation from authenticated Node requests. Dash reads the first IP in X-Forwarded-For when that header is present, otherwise it falls back to RemoteAddr; invalid values are not used. This field is for display and operations, not an auth boundary.
Alerts
Alert evaluation reads the latest process-local report snapshot or the current PostgreSQL projection, never the frontend Redis cache.
An existing non-offline firing alert remains open when its snapshot is missing, invalid, stale, or lacks an optional runtime field. Only a fresh sample that proves the condition cleared closes it as recovered. Unmounting or invalidating the rule closes the corresponding event.
Alert notifications enter the PostgreSQL outbox and are delivered by one process-local worker without runtime leases. If the first target load fails without a last-good snapshot, the transition is delayed; with a last-good snapshot, the event and outbox commit against that snapshot. Remote delivery failure does not roll back the alert event.
State and Retention
Server, disk I/O, disk usage, and physical-disk temperature samples use one-hour chunks and are compressed losslessly after one day. database.metrics_raw_retention_days controls their raw retention period and defaults to eight days. Fifteen-minute aggregates retain 16 days, and one-hour aggregates retain 32 days.
History ranges select raw samples or aggregates explicitly and include the latest samples. server_online_30m includes completed 30-minute periods only.
Raw NIC metrics and service checks remain controlled by database.retention_days (default 45 days), while five-minute traffic records use database.traffic_retention_days. Traffic retention is both a cleanup and rebuild boundary: traffic_5m stays writable and uses rolling retention, while historical 95th-percentile billing values remain in monthly snapshots.
Monthly usage aggregation and five-minute traffic generation keep independent progress in PostgreSQL. Monthly totals are maintained in lite and billing modes. Five-minute records are generated only in billing mode.
After a switch from lite to billing, Dash generates five-minute records starting 30 minutes before the switch. Earlier retained raw NIC data requires a per-node rebuild.
Historical five-minute records use one process-local rebuild task that is available only in billing mode. It rewrites one node at a time in six-hour ranges and invalidates overlapping snapshots. Switching to lite completes the current range and then stops the task. Rebuild state resets on Dash restart.
Every node stores an explicit billing cycle. Global settings provide only the default direction; traffic_direction_mode=default inherits it. A node cycle change invalidates and locally repairs only that node's derived monthly rows.
Uptime Data Flow
PostgreSQL/TimescaleDB jobs maintain node uptime. The minute job stores online durations covered by reports for each completed minute in node_online; the node_online_1h continuous aggregate sums online and observed durations by hour. Both datasets retain 46 days.
HTTP queries primarily read hourly aggregates and include minute records that have not yet been materialized. The dashboard displays the latest 45 calendar days and refreshes daily statistics independently every 60 seconds. Hourly details load on demand; the browser caches up to 45 dates per node for 60 seconds.
No new observations are generated before the first report, after deletion, or for missed sampling windows. Missing minutes remain unknown and are not automatically backfilled. Guest uptime visibility does not affect database jobs and is independent of historical-chart access. See Data Retention.
Public URL
app.public_url is the deployment root:
https://dash.example.com
It affects browser links, install script output, node target URLs, and reverse-proxy behavior. It must not contain a path prefix such as /dash.
Keep browser API calls same-origin. Cross-origin backend addresses require CORS, cookie, and CSRF policies to be configured together.