@flame0510/project-aether 1.9.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/app/agents/CostSection.tsx +94 -0
- package/app/agents/PageClient.tsx +14 -0
- package/app/agents/PanelRow.tsx +9 -0
- package/app/agents/ResourceSection.tsx +105 -0
- package/app/api/assistant/route.ts +10 -3
- package/app/api/containers/route.ts +18 -2
- package/app/api/costs/agent/route.ts +55 -0
- package/app/api/costs/route.ts +19 -77
- package/app/api/costs/usage/route.ts +65 -0
- package/app/api/gateway/sync.ts +18 -4
- package/app/api/metrics/alerts/route.ts +47 -0
- package/app/api/metrics/containers/route.ts +54 -0
- package/app/api/metrics/route.ts +4 -6
- package/app/api/stats/route.ts +2 -3
- package/app/api/stats-since/route.ts +1 -1
- package/app/api/stream/route.ts +3 -5
- package/app/api/system-health/route.ts +33 -32
- package/app/components/CostBreakdown.tsx +1 -1
- package/app/components/Sidebar.tsx +10 -0
- package/app/components/SystemCockpit.tsx +1 -1
- package/app/components/ui/Accordion.tsx +45 -0
- package/app/components/ui/TimeSeriesChart.tsx +47 -11
- package/app/components/ui/index.ts +1 -0
- package/app/containers/ContainersClient.tsx +48 -6
- package/app/costs/CostsSkeleton.tsx +88 -0
- package/app/costs/PageClient.tsx +366 -0
- package/app/costs/loading.tsx +13 -0
- package/app/costs/page.tsx +5 -0
- package/app/globals.css +42 -0
- package/app/system/AgentCharts.tsx +138 -0
- package/app/system/AgentsSection.tsx +281 -0
- package/app/system/PageClient.tsx +7 -7
- package/app/system/RecentAlerts.tsx +72 -0
- package/app/system/SystemSkeleton.tsx +53 -1
- package/app/system/loading.tsx +5 -1
- package/daemon.js +646 -41
- package/docs/ARCHITECTURE.md +72 -31
- package/docs/DESIGN-SYSTEM.md +2 -2
- package/docs/FRONTEND-ARCHITECTURE.md +14 -3
- package/docs/REV4A.md +6 -5
- package/docs/dev/API-REFERENCE.md +208 -38
- package/docs/dev/DATABASE.md +176 -20
- package/docs/dev/GATEWAY.md +53 -9
- package/docs/rag/DATA-FRESHNESS.md +34 -7
- package/docs/rag/GLOSSARY.md +8 -5
- package/docs/rag/REV4A-OVERVIEW.md +10 -3
- package/docs/rag/WHAT-I-CAN-ANSWER.md +6 -3
- package/instrumentation.ts +11 -0
- package/lib/agent-costs.ts +172 -0
- package/lib/container-metrics.ts +340 -0
- package/lib/cost-reconciliation.ts +78 -0
- package/lib/costs-db.ts +31 -0
- package/lib/docker-socket-path.js +133 -0
- package/lib/docker-socket.ts +10 -99
- package/lib/docker-stats.js +284 -0
- package/lib/metrics-db.ts +15 -6
- package/lib/model-pricing.ts +123 -3
- package/lib/price-schedule-sync.ts +133 -0
- package/lib/utils/format.ts +38 -0
- package/model-pricing.json +269 -121
- package/package.json +1 -1
- package/scripts/refresh-model-pricing.mjs +16 -4
- package/scripts/test-docker-stats.mjs +270 -0
- package/lib/billing.ts +0 -100
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Rev4a Architecture — Design & Vision
|
|
2
2
|
|
|
3
3
|
> **Status:** Active — `main` branch
|
|
4
|
-
> **Last updated:** 2026-
|
|
4
|
+
> **Last updated:** 2026-10-01
|
|
5
5
|
> **Goal:** Transform Rev4a from a monitoring dashboard into a central orchestrator for a distributed multi-container agency.
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
**Credentials vault:** The `/credentials` page stores third-party service tokens in `credentials.db` and installs them into containers on explicit sync. Provider API keys live separately in `provider-keys.json`, managed from the Gateway UI.
|
|
74
74
|
|
|
75
|
-
**Container management UI:** The `/containers` page lists all Docker containers, running or stopped, with status, image, IP and ports, and opens a web terminal into any running one. Fully functional.
|
|
75
|
+
**Container management UI:** The `/containers` page lists all Docker containers, running or stopped, with status, image, IP and ports, and opens a web terminal into any running one. Fully functional. Each running container's CPU, memory and processes are on its row, with a link to its charts on `/system`, which also shows the machine's CPU, RAM and disk.
|
|
76
76
|
|
|
77
77
|
**Workspace API:** Lazy-loaded file tree explorer with real-time reads (no caching), supports both host and container workspaces via `docker exec`.
|
|
78
78
|
|
|
@@ -136,7 +136,8 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
136
136
|
hand (identity, `enabled`, `deprecated`, and an optional `info` block for what no API
|
|
137
137
|
publishes: the vendor's size claim, benchmarks, the docs link, notes).
|
|
138
138
|
`model-pricing.json` and `model-details.json` are **generated** by
|
|
139
|
-
`npm run refresh:pricing` (`scripts/refresh-model-pricing.mjs`): prices from OpenRouter
|
|
139
|
+
`npm run refresh:pricing` (`scripts/refresh-model-pricing.mjs`): prices from OpenRouter
|
|
140
|
+
(cache rates included; direct vendors' prices by hand from their pricing pages),
|
|
140
141
|
and per model the description, architecture and benchmarks from OpenRouter plus the size,
|
|
141
142
|
weight mix, licence and dates from the Hugging Face card. Two scripts back the curation
|
|
142
143
|
of `info`: `npm run info:suggest` (`scripts/model-info-suggest.mjs`) prints the candidate
|
|
@@ -577,8 +578,12 @@ The Rev4a daemon (`daemon.js`) is a standalone Node.js process that bridges the
|
|
|
577
578
|
3. Emit `spawn` / `complete` / `fail` / `spawn_timeout` events into the `events` table
|
|
578
579
|
4. Sample the host machine (CPU, RAM, swap, storage) every 30 s into `metrics.db`, on a
|
|
579
580
|
timer of its own
|
|
580
|
-
5.
|
|
581
|
-
|
|
581
|
+
5. Sample what each container consumes every 60 s, and the disk Docker holds every 10 min,
|
|
582
|
+
into `metrics.db` (see [Container Consumption](#container-consumption))
|
|
583
|
+
6. Detect anomalies (CPU > 85%, RAM > 90%, a disk at 90%) and record them as events
|
|
584
|
+
7. Read what each running agent spent, as its own OpenClaw priced it, every 5 min into
|
|
585
|
+
`costs.db`, and each vendor's balance or usage every hour (see [Agent Costs](#agent-costs))
|
|
586
|
+
8. Manage DB lifecycle (WAL mode; events.db checkpointed after each poll cycle)
|
|
582
587
|
|
|
583
588
|
### Poll Interval
|
|
584
589
|
|
|
@@ -586,33 +591,12 @@ A fixed 30 s timer, whether or not sessions are working. When the OpenClaw CLI i
|
|
|
586
591
|
the host, the session poll logs it once and stays idle; the machine metrics keep being
|
|
587
592
|
sampled, since they have their own timer.
|
|
588
593
|
|
|
589
|
-
###
|
|
594
|
+
### Session costs
|
|
590
595
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
| `claude-sonnet-4` | substring | $3.00 | $15.00 |
|
|
596
|
-
| `claude-opus-4` | substring | $15.00 | $75.00 |
|
|
597
|
-
| `gpt-5-mini` | substring | $0.15 | $0.60 |
|
|
598
|
-
| `codex` | substring | $3.00 | $15.00 |
|
|
599
|
-
| `gemini` | substring | $0.075 | $0.30 |
|
|
600
|
-
| `flash` | substring | $0.075 | $0.30 |
|
|
601
|
-
| `deepseek` | substring | $0.55 | $2.19 |
|
|
602
|
-
| `default` | fallback | $3.00 | $15.00 |
|
|
603
|
-
|
|
604
|
-
**Aliases** (resolved before substring match):
|
|
605
|
-
|
|
606
|
-
| Alias | Resolves to |
|
|
607
|
-
|---|---|
|
|
608
|
-
| `cheap` | `flash` |
|
|
609
|
-
| `fast` | `claude-sonnet-4` |
|
|
610
|
-
| `big` | `claude-opus-4` |
|
|
611
|
-
| `coder` | `codex` |
|
|
612
|
-
| `pro` | `gemini` |
|
|
613
|
-
| `reason` | `deepseek` |
|
|
614
|
-
|
|
615
|
-
Formula: `cost = (tokens_in / 1_000_000 * price_in) + (tokens_out / 1_000_000 * price_out)`
|
|
596
|
+
The session poll writes no cost: `sessions.cost_usd` stays 0. It used to be estimated from a
|
|
597
|
+
fixed 2025 rate table (`$3 / $15` per 1M for anything unknown), which the dashboard showed as
|
|
598
|
+
spend. What the agents actually spend is priced by each agent's own OpenClaw and read by the
|
|
599
|
+
Agent Costs timer — see [Agent Costs](#agent-costs).
|
|
616
600
|
|
|
617
601
|
### Upsert Logic
|
|
618
602
|
|
|
@@ -647,6 +631,63 @@ taken at load), then one every 30 s. Samples older than 30 days are pruned at ev
|
|
|
647
631
|
it through `lib/metrics-db.ts` (read-only, `null` when there is nothing to read);
|
|
648
632
|
`GET /api/metrics` serves the System page (`/system`) and the dashboard's Machine card.
|
|
649
633
|
|
|
634
|
+
### Container Consumption
|
|
635
|
+
|
|
636
|
+
The System page says *that* the machine is busy; this says *who*. On a timer of its own the
|
|
637
|
+
daemon reads every running container from the Docker Engine API — `GET /containers/{id}/stats?stream=false&one-shot=true`,
|
|
638
|
+
10–20 ms a container, where `docker stats --no-stream` waits a second each and returns formatted
|
|
639
|
+
strings — and writes `container_metrics` into `metrics.db` (schema in `docs/dev/DATABASE.md`):
|
|
640
|
+
|
|
641
|
+
- **CPU** from the cumulative CPU time (ns): the difference between two samples over the
|
|
642
|
+
elapsed time, in cores; the first sample after a start only primes the counters. A counter
|
|
643
|
+
that goes backwards (the container restarted) gives no figure for that sample, never a spike.
|
|
644
|
+
- **Memory** is the working set — usage minus the inactive page cache (`inactive_file` on
|
|
645
|
+
cgroup v2, `total_inactive_file` on v1), what `docker stats` shows.
|
|
646
|
+
- **Processes, network and block I/O** as gauges and per-second rates. A container that stops
|
|
647
|
+
leaves a gap; one that is gone takes its counters with it.
|
|
648
|
+
- **Names:** an agent's `AGENT_NAME` (read once per container id from its inspect), else the
|
|
649
|
+
container's name. Every running container is sampled — agents, and the rest as "container".
|
|
650
|
+
- **Docker's disk** (`/system/df`, every 10 min): each named volume — which gives each agent's
|
|
651
|
+
storage, through the volume names its mounts list —, each writable layer, the images and the
|
|
652
|
+
build cache with what no container/build uses. Sizes and "unused" are readings, not advice.
|
|
653
|
+
- The shares need Docker's own cores and memory (`/info`, re-read every 10 min): on Docker
|
|
654
|
+
Desktop those are its VM's (8 GB of a 16 GB Mac), on the VPS the machine's.
|
|
655
|
+
|
|
656
|
+
The socket path rules (env, `/var/run`, Docker Desktop's `~/.docker/run`, `docker context`) live
|
|
657
|
+
once, in `lib/docker-socket-path.js`, required by both the daemon and `lib/docker-socket.ts`. A
|
|
658
|
+
Docker that is down is logged once and recovered from by itself; a failed disk reading leaves the
|
|
659
|
+
previous one in place. `lib/container-metrics.ts` reads it all for `GET /api/metrics/containers`
|
|
660
|
+
(the System page's *Agents and containers* section, the agent panel's RESOURCES, the Containers
|
|
661
|
+
page). Anomalies per container (processes, sustained memory) are not raised yet.
|
|
662
|
+
|
|
663
|
+
### Agent Costs
|
|
664
|
+
|
|
665
|
+
Rev4a computes no cost. Every model in the `rev4a` provider block synced into the agents
|
|
666
|
+
carries its price (`cost`, from `model-pricing.json` through `priceAt()`), so each agent's
|
|
667
|
+
OpenClaw prices every call itself — per call, session and day, cache split out. A vendor
|
|
668
|
+
priced by time of day (DeepSeek off-peak) is re-synced at each change of band by
|
|
669
|
+
`lib/price-schedule-sync.ts`, started from `instrumentation.ts`; OpenClaw keeps the cost
|
|
670
|
+
recorded at call time. Details in `docs/dev/GATEWAY.md` (Costs).
|
|
671
|
+
|
|
672
|
+
Every 5 min (first pass 20 s after start) the daemon first wakes every running agent with one
|
|
673
|
+
short call — OpenClaw rebuilds its transcript index in about a minute after a restart or a
|
|
674
|
+
price change, and woken together the agents rebuild in parallel — then asks each, one at a
|
|
675
|
+
time, for its figures over the last 30 UTC days — `usage.cost` and
|
|
676
|
+
`sessions.usage` through `docker exec … openclaw gateway call` on the agent's port 3000,
|
|
677
|
+
the token resolved inside the container — and rewrites that window in `costs.db` (schema
|
|
678
|
+
in `docs/dev/DATABASE.md`). An answer from an index OpenClaw is still rebuilding is never
|
|
679
|
+
stored. The readers — `GET /api/costs/usage` (the Costs page, `/costs`), `GET /api/costs` and
|
|
680
|
+
the SSE stream (the dashboard), the system-health cost check, and `GET /api/costs/agent` (the
|
|
681
|
+
agent panel) — share one read-only handle (`lib/costs-db.ts`) and one set of spend queries
|
|
682
|
+
(`lib/agent-costs.ts`); the vendor readings are read by `lib/cost-reconciliation.ts`.
|
|
683
|
+
|
|
684
|
+
After a pass, at most once an hour, the daemon also reads each vendor's own figure —
|
|
685
|
+
DeepSeek's balance, OpenRouter's lifetime usage (read-only calls, never billed) — and stores
|
|
686
|
+
it next to the agents' cumulative priced total for that vendor's models, in the same row
|
|
687
|
+
(`vendor_balance`). Between two rows, what the vendor billed and what the agents priced
|
|
688
|
+
cover the same interval: the Costs page's *Against the bill* (`lib/cost-reconciliation.ts`).
|
|
689
|
+
`costs.db` is opened in a try: if it cannot be, collection is off (`[COSTS] disabled`).
|
|
690
|
+
|
|
650
691
|
### Anomaly Detection
|
|
651
692
|
|
|
652
693
|
After each sample the daemon inserts a `system_anomaly` event into `events.db`
|
package/docs/DESIGN-SYSTEM.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Source of truth: `app/design-system.ts` + CSS tokens in `app/globals.css`.
|
|
4
4
|
|
|
5
|
-
> **Last updated:** 2026-09-
|
|
5
|
+
> **Last updated:** 2026-09-27
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -82,7 +82,7 @@ then use it — a one-off copy of markup is the thing this section exists to pre
|
|
|
82
82
|
5. **CSS vars over hardcoded colors.** Never inline `#22c55e`, `#ef4444`, `#888` — use the CSS token.
|
|
83
83
|
6. **Loading feedback.** Use `loading` prop on `Button` (shows inline spinner). Do not render separate loader elements.
|
|
84
84
|
7. **Minimal animation.** Transitions on hover/active (0.1s–0.12s), spin on loading spinner. No bounce, shake, or decorative animations.
|
|
85
|
-
8. **Data display: violet for the data, status colours for levels.** A chart series is `var(--violet)` (2px line, 10% wash), grid and axis text are `var(--border)` / `var(--text-dim)`, never the series colour. A `Meter` fill is violet when fine and `var(--yellow)` / `var(--red)` at its warning / critical level, with the level also written out — status never rests on colour alone. Focus on a chart is a 1px `outline` in `var(--violet-dim)`, not a shadow. See `TimeSeriesChart` and `Meter` in `docs/FRONTEND-ARCHITECTURE.md`.
|
|
85
|
+
8. **Data display: violet for the data, status colours for levels.** A chart series is `var(--violet)` (2px line, 10% wash), grid and axis text are `var(--border)` / `var(--text-dim)`, never the series colour. A `Meter` fill is violet when fine and `var(--yellow)` / `var(--red)` at its warning / critical level, with the level also written out — status never rests on colour alone. Focus on a chart is a 1px `outline` in `var(--violet-dim)`, not a shadow. A figure that is only a share of a whole (the Costs page's agents and models) uses the same `Meter`, always at its `ok` level: it is a proportion, not a warning. See `TimeSeriesChart` and `Meter` in `docs/FRONTEND-ARCHITECTURE.md`.
|
|
86
86
|
|
|
87
87
|
### React hook
|
|
88
88
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a Frontend Architecture
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
3
|
+
> **Last updated:** 2026-10-01
|
|
4
4
|
|
|
5
5
|
## Layering
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
39
39
|
| `Metric` | `Metric.tsx` | Metric card with title, value, subtitle, tone, and a `size` (`md` default, `sm` for a grid inside a dialog: smaller value, tighter spacing). |
|
|
40
40
|
| `StatusCard` | `StatusCard.tsx` | Health/status report card. |
|
|
41
41
|
| `Meter` / `meterLevel` | `Meter.tsx` | Horizontal 0-100 % fill whose colour carries the level (`ok` accent, `warning` yellow, `critical` red); the level is also written out beside it, never colour alone. `meterLevel(percent, warning, critical)` picks the level. ARIA `role="meter"`. |
|
|
42
|
-
| `TimeSeriesChart` | `TimeSeriesChart.tsx` | One series over time in plain SVG measured to its container (text never scaled): a 2px accent line for the value, a 10 % wash up to the bucket's peak, a hairline grid at 0 / 50 / 100 %, the latest value labelled at the line's end. Points further apart than 1.5 buckets are not joined (a gap means no samples). Hover or keyboard focus (← → Esc) shows a crosshair snapped to the nearest point with value and peak; a **Table** disclosure holds the same data. `stale` dims the previous render while a new range loads. Used by the System
|
|
42
|
+
| `TimeSeriesChart` | `TimeSeriesChart.tsx` | One series over time in plain SVG measured to its container (text never scaled): a 2px accent line for the value, a 10 % wash up to the bucket's peak, a hairline grid at 0 / 50 / 100 %, the latest value labelled at the line's end. Points further apart than 1.5 buckets are not joined (a gap means no samples). Hover or keyboard focus (← → Esc) shows a crosshair snapped to the nearest point with value and peak; a **Table** disclosure holds the same data. `stale` dims the previous render while a new range loads. `yMax` (a number, or `'auto'`: a rounded ceiling just above the highest value or peak — for a series that is a small share of its natural scale, like one agent's CPU) / `unit` scale and label a percentage by default; `formatValue` replaces the label format everywhere (axis, end label, tooltip, table) — the Costs page passes dollars. The left and right margins grow with the longest axis label and the end label, so a longer format is never cut. Used by the System and Costs pages. |
|
|
43
43
|
| `Surface` | `Surface.tsx` | Shared panel/card surface, variant prop. |
|
|
44
44
|
| `Page` / `PageHeader` | `Page.tsx` | Full-page layout shell. |
|
|
45
45
|
| `Icons` | `Icons.tsx` | SVG icons (`EyeIcon`, `EyeOffIcon`), 16/20px shared. |
|
|
@@ -47,7 +47,12 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
47
47
|
| `UpdateSection` | `app/agents/UpdateSection.tsx` | OPENCLAW VERSION section of the agent detail panel: the version the agent runs, **Update to <version>** when a newer supported version is downloaded (confirm modal, disabled while the agent is stopped), the running update's steps with backup progress, the outcome, and **Roll back to <version>** after an update. Polls `/api/agents/[id]/update` every 2 s while an update or rollback runs; one action at a time (keyed busy state). |
|
|
48
48
|
| `BackupSection` | inline in `app/agents/PageClient.tsx` | BACKUP section of the agent detail panel, on the cold backup and the restore. **Backup Now** starts `POST /api/agents/[id]/cold-backup`; while the job runs a banner shows the file and its live percent with **Cancel** (`DELETE /cold-backup`). **Restore** (after a confirm) starts `POST /restore` and a banner shows `Restoring <file>…` (no percent: the extract is a single `tar xzf`, and there is no Cancel). Both sections poll their `GET` every 2 s while running, and on mount pick up a job that is already running — a backup lives in a Docker helper, a restore in `agent_restores`, so reloading the page or navigating away never loses them nor allows a second one (the server answers 409 anyway). Delete per row; all actions disabled while one runs; keyed busy state `{ kind, file }` so only the row in action shows the spinner. On the agent list, an activity Badge (fed by `/api/agents/activity-summary`, polled at 2 s only while something runs, otherwise riding the 15 s list poll) reads `BACKUP nn%`, `RESTORING`, `RECREATING`, `EDITING` or `UPDATING`. |
|
|
49
49
|
| `RecreateSection` | inline in `app/agents/PageClient.tsx` | RECREATE section of the agent detail panel. **Recreate Container** starts `POST /api/agents/[id]/recreate` (202) after a confirm; a banner then shows the phase — *Backing up … nn%* while the cold backup runs, *Recreating container…* while the container is rebuilt and the gateway starts. The section polls `GET /recreate` every 2 s, and on mount picks up a recreate that is already running, so a reload or navigation never loses it; it refetches the agent once the job reports `done`. |
|
|
50
|
-
|
|
|
50
|
+
| `Accordion` | `Accordion.tsx` | A row that opens: the header is a real `<button type="button">` (`aria-expanded`, `aria-controls` while open) carrying a `title`, `summary` figures and an optional muted `detail` line, so the answer to "what is this now" is on screen closed; the body holds what takes room and is **mounted only while open**, so a chart inside fetches nothing while closed. Controlled (`open` / `onToggle`: the parent can deep-link or fetch on it), optional `id` for a link to land on. Square, flat, tokens only. On mobile the summary wraps under the title. Used by the System page's agents. |
|
|
51
|
+
| System page | `app/system/PageClient.tsx` | `/system`: CPU, memory and storage of the host, from `GET /api/metrics`. Three cards (CPU with cores and load; memory with available and swap; one `Meter` per filesystem with its roles) — thresholds 85/95 % for CPU and memory, 80/90 % for storage — then a range switch (`Tabs`: 1h · 24h · 7d · 30d) scoping the charts below it: CPU and memory average with peak, one chart per filesystem. Polls every 30 s with an `AbortController` ref (a range change aborts the previous fetch). Times in the configured Rev4a timezone (`useRev4aTimezone`). Before the first answer the cards and charts are skeletons shaped like them (`app/system/SystemSkeleton.tsx`, also the route's `loading.tsx`), so nothing jumps; the hostname line is cut with an ellipsis and never widens the page. Says *No samples yet* before the daemon's first sample and *Not collecting* when the latest sample is older than three intervals. Two columns of charts from lg (992px) up, one below; an odd last chart spans both columns instead of leaving a hole. Under them, **Agents and containers** (`app/system/AgentsSection.tsx`, heading in the accent eyebrow with a rule above — `system-section-heading`): *Docker storage* (images, volumes, writable layers, build cache, each with what no container uses — wording neutral on purpose: an unused image may be a rollback target, an unused volume the cold backups), then **one `Accordion` per container**, sorted by CPU now (`GET /api/metrics/containers`, polled every 30 s with an `AbortController` ref). Closed: name, an *agent* / *container* pill, *stopped* when it is — or *no data* when Docker still lists it but its stats call fails — and CPU (share of Docker's cores), memory, storage, PIDs, with average · peak over the range tabs, cores, network and memory share on a muted line. Open (`AgentCharts.tsx`): the same three charts as the machine — CPU (share), Memory, Storage — for that container, fetched when it opens (`?container=`), polled every 30 s, aborted on close or range change; each with `yMax="auto"` (an agent is 0.01–2 % of Docker's cores) and sizes in MB/GB, on `subtle` surfaces inside the accordion. A last row, *Everything else*, is the machine minus the containers. `/system?agent=<container>` opens one and scrolls to it. Then **Recent alerts** (`RecentAlerts.tsx`, `GET /api/metrics/alerts`): the machine's latest 10 anomalies with time (configured timezone), metric pill and message. The section has its own skeleton (`SystemAgentsSkeleton`, also in the route's `loading.tsx`). |
|
|
52
|
+
| Costs page | `app/costs/PageClient.tsx` | `/costs`: what each agent spent, as its own OpenClaw priced it, from `GET /api/costs/usage`. A range switch (`Tabs`: Today · 7 days · 30 days, UTC days) scopes everything below it: three cards (spent with tokens and days; the cost split into input / output / cache read / cache write; the tokens with the share of input served from cache), the daily spend (`TimeSeriesChart` with dollars, not on Today), then *By agent* and *By model* side by side — each row a name and figure over a share `Meter`, the pattern of the System storage card — *Against the bill* (per vendor, what it billed, from balance or usage readings, next to what the agents priced over the same readings, top-ups apart), and the most expensive sessions. Warnings above: *No figures yet* before the daemon's first pass, *Not current* for agents still being read whose last read failed or is older than three intervals (a stopped or deleted agent keeps its figures unflagged), and *Calls without a price* by model (a model with no price is also marked in *By model*, so a $0.00 is never read as free). The header shows the DeepSeek band now (peak / off-peak, until when, UTC) when a DeepSeek model is on offer. Polls every 60 s with an `AbortController` ref. Skeletons shaped like the content (`app/costs/CostsSkeleton.tsx`, also the route's `loading.tsx`). Costs under a cent keep four decimals. |
|
|
53
|
+
| Agent costs | `app/agents/CostSection.tsx` | The COSTS section of the agent panel, after MODEL: today (UTC), last 7 and 30 days with tokens, the top model, calls without a price, when the agent was last read (a note when not recently: a stopped agent keeps its last figures), and a link to `/costs`. From `GET /api/costs/agent`, every 60 s with an `AbortController` ref. |
|
|
54
|
+
| Agent resources | `app/agents/ResourceSection.tsx` | The RESOURCES section of the agent panel, after COSTS: CPU and memory now and over the last 24 h, processes, network, storage (volume and layer), from `GET /api/metrics/containers`, every 60 s with an `AbortController` ref, reset on agent switch; *Stopped* when the agent is not running, *Not collecting* when the newest sample is older than three intervals (the "now" figures then stand down), a refresh-failure line while the last answer stays, and a link to its charts on `/system?agent=<container>` — the history lives in one place. Rows are the shared `PanelRow` (`app/agents/PanelRow.tsx`, also used by COSTS). |
|
|
55
|
+
| Containers page | `app/containers/ContainersClient.tsx` | `/containers`: one card per Docker container; a running one the daemon has sampled adds a line — CPU share, memory, PIDs — and *Charts →* to its accordion on `/system`. The figures are a bonus: a daemon that has not sampled yet, or a failed request, leaves the list as it was. |
|
|
51
56
|
| Machine card | `app/components/SystemCockpit.tsx` | On the dashboard, next to Active sessions: CPU, RAM and the fullest disk now, each figure coloured only when past its threshold (same thresholds as the System page), the issues named in words, and a link to `/system`. It says *No samples yet* before the first sample, *Not collecting* when the newest is stale, and *Metrics unavailable* when a refresh fails (keeping the last figures). A `Surface`, not a `Metric`: a danger `Metric` paints its whole value red. |
|
|
52
57
|
| Container terminal | `app/containers/terminal/[id]/TerminalClient.tsx` | Full-screen shell into a container: xterm.js 6 (`@xterm/xterm` + `addon-fit` refitted by a `ResizeObserver`, `addon-web-links`), loaded client-side only, palette from the design tokens. Keystrokes are sent raw (`onData`, `onBinary`), the size on connect and whenever the columns or rows change. A bar with `ui-kicker`, the container name, a status `Badge` (Connecting / Connected / Reconnecting / Closed) and `Button`s **Reconnect** / **Close**; styles are the `terminal-page` classes. Session id per container in `sessionStorage` (a UUID from `getRandomValues`: `randomUUID` is missing over plain HTTP), so a reload resumes the shell; after a drop it reconnects with `resume=1` and is told when the shell is gone; retries with backoff (six failed opens, about 40 s, then a message) and stops on the server's final close codes with the reason shown. **Close** while reconnecting still ends the waiting shell. Protocol and sessions: `docs/CONTAINER-TERMINAL.md`. |
|
|
53
58
|
| `ImageDownloadBanner` | `app/agents/ImageDownloadBanner.tsx` | Agent image banner on the Agents page. Polls `/api/agents/image-status` every 2 s; offers **Download Image** when no supported version is downloaded, **Download <version>** when the registry publishes a newer one, and while a download runs shows the percent of layers finished and the latest line of docker's output (the bar is indeterminate until a percent can be computed). **Cancel** (`DELETE /api/agents/download-image`, own loading state) appears once `image-status` reports the download running; before that the button reads *Starting…* and is disabled. Every outcome — ready and cancelled for 3 s, a failure until the next action — comes from the server's `lastResult`, so one that ended while the page was closed or reloading is still reported if it is less than 30 s old (aged with `serverTime`). Each outcome is announced once per page load, although the Agents page mounts the banner in three places (mobile list, mobile detail, desktop). Downloading changes no agent. |
|
|
@@ -65,6 +70,12 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
65
70
|
| `Badge` | `Badge.tsx` | Inline status tag with tone variants (success, danger, warning, neutral). Used for channel chips, pairing labels, error/success messages. |
|
|
66
71
|
| `Skeleton` + shape helpers | `app/components/Skeleton.tsx` | Shimmer placeholders. `Skeleton` is the primitive; the rest mirror one specific layout each: `TreeSkeleton`, `CodeSkeleton`, `CronJobsSkeleton`, `CronRunsSkeleton`, `CredentialCardsSkeleton`, `AgentSyncRowsSkeleton`, `SkeletonLines`, `SkeletonMetric`, `CardRowSkeleton`. A route's `loading.tsx` sits in `RouteLoadingShell` (full width up to `maxWidth`, centred): the app shell's content area is a column flexbox, and a `margin: 0 auto` child without `width: 100%` shrinks to its content — every loader did, and its rows came out as wide as its 120 px title (measured: 120 px instead of 1040). `ListPageSkeleton` (a title over rows) is the list pages' loader. Create Agent (`app/agents/create/loading.tsx`) and the container terminal (`app/containers/terminal/[id]/loading.tsx`) have their own, shaped like the page, instead of inheriting the Agents / Containers rows; the Agents page prefetches `/agents/create` (its **+ New** uses `router.push`, which prefetches nothing) and the Containers **Terminal** is a `Link`, so both open without a full reload. A shape helper must match the real markup it stands in for — same row structure, same paddings, same element count where the count is known — so nothing reflows when data replaces it. Inside text (a `<p>`, a `<span>`) pass `as="span"`: a `<div>` there is invalid HTML, and the parser splits the paragraph in the server-rendered page. The System page keeps its own shapes in `app/system/SystemSkeleton.tsx`, each shimmer inside the class of the text it replaces so the line boxes are the real ones — measured to 0 px of movement on desktop and mobile. |
|
|
67
72
|
|
|
73
|
+
The System page's container rows distinguish a recent Docker listing from fresh resource
|
|
74
|
+
statistics. A row seen in Docker without figures says *no data*. When the newest statistics
|
|
75
|
+
sample ages past three intervals, current CPU, memory and processes disappear; an older row
|
|
76
|
+
says *not current*. The page advances its clock at each poll and recomputes age from
|
|
77
|
+
`sampled_at`, so repeated failed refreshes cannot leave a previous answer labelled as current.
|
|
78
|
+
|
|
68
79
|
### Rules
|
|
69
80
|
|
|
70
81
|
1. **Prefer existing primitives over new ad-hoc markup.** Every new component starts from the shared catalog.
|
package/docs/REV4A.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a — VPS Dashboard
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
3
|
+
> **Last updated:** 2026-10-01
|
|
4
4
|
|
|
5
5
|
A Next.js 16 dashboard for monitoring and managing the OpenClaw ecosystem.
|
|
6
6
|
|
|
@@ -99,10 +99,11 @@ When environment variables are saved via the Config page (Save & Restart):
|
|
|
99
99
|
|
|
100
100
|
| Route | Description |
|
|
101
101
|
|---|---|
|
|
102
|
-
| `/` | System overview — health, sessions, the machine's CPU / RAM / disk (links to `/system`),
|
|
102
|
+
| `/` | System overview — health, sessions, the machine's CPU / RAM / disk (links to `/system`), what the agents spent today (as on `/costs`), live feed |
|
|
103
103
|
| `/agents` | Running agents (Docker containers with `AGENT_ID`), gateway token management, agent creation wizard, channel manager (Telegram pairing) |
|
|
104
|
-
| `/containers` | All Docker containers on the host |
|
|
105
|
-
| `/
|
|
104
|
+
| `/containers` | All Docker containers on the host, each running one with its CPU, memory and processes now (from the daemon's container samples) and a link to its charts |
|
|
105
|
+
| `/costs` | What each agent spent, as its own OpenClaw priced it with the rates Rev4a syncs: today / 7 / 30 UTC days, by day, by agent, by model, the most expensive sessions, the calls that could not be priced, the DeepSeek band now, and a check against what DeepSeek and OpenRouter actually billed (read by the daemon every 5 min into `costs.db`; each agent's own figures are also in its panel, COSTS) |
|
|
106
|
+
| `/system` | The host machine: CPU, memory and swap, storage per filesystem — current values and 1h / 24h / 7d / 30d history (from `metrics.db`, sampled by the daemon every 30 s); **Agents and containers**: what Docker holds on disk, and one row per container — CPU, memory, storage, processes — that opens to its own CPU / Memory / Storage charts (sampled every 60 s; a recently seen row without current statistics says "no data", and stale current figures are hidden; each agent's numbers are also in its panel, RESOURCES); and the machine's recent alerts |
|
|
106
107
|
| `/wizard` | First-run setup wizard (Welcome → Providers → Agent → Ready) |
|
|
107
108
|
| `/workspace` | File explorer with tree view + editor — VPS host or container workspaces |
|
|
108
109
|
| `/lineage` | Agent lineage / orchestration tree |
|
|
@@ -307,7 +308,7 @@ Store third-party service credentials and install them into agent containers.
|
|
|
307
308
|
### Container management (`/containers`)
|
|
308
309
|
View all running Docker containers with:
|
|
309
310
|
- Name, image, status, ports, uptime
|
|
310
|
-
- Resource usage (CPU, memory)
|
|
311
|
+
- Resource usage now (CPU share, memory, processes) for each running container, with *Charts →* to its history on `/system`
|
|
311
312
|
- Quick links to agent control UIs
|
|
312
313
|
|
|
313
314
|
### Agent management (`/agents`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a API Reference
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
3
|
+
> **Last updated:** 2026-10-01
|
|
4
4
|
|
|
5
5
|
All routes are under `/api/`. Authentication is required on every endpoint
|
|
6
6
|
unless otherwise noted.
|
|
@@ -469,12 +469,12 @@ Returns all sessions (up to 2000), newest first, joined with lineage labels.
|
|
|
469
469
|
"model": "openai-codex/gpt-5.4",
|
|
470
470
|
"tokens_in": 12500,
|
|
471
471
|
"tokens_out": 3200,
|
|
472
|
-
"cost_usd": 0
|
|
472
|
+
"cost_usd": 0,
|
|
473
473
|
"status": "idle",
|
|
474
474
|
"task_preview": "Run hygiene audit...",
|
|
475
|
-
"started_at":
|
|
475
|
+
"started_at": 1749200000000,
|
|
476
476
|
"ended_at": null,
|
|
477
|
-
"updated_at":
|
|
477
|
+
"updated_at": 1749201000000,
|
|
478
478
|
"trello_card_url": null,
|
|
479
479
|
"lineage_label": null,
|
|
480
480
|
"lineage_agent_name": null
|
|
@@ -517,8 +517,76 @@ One session with its events and any child sessions.
|
|
|
517
517
|
|
|
518
518
|
## Stats & Costs
|
|
519
519
|
|
|
520
|
+
Agent costs come from `costs.db`: what each agent's own OpenClaw priced, copied by the
|
|
521
|
+
daemon (the queries are in `lib/agent-costs.ts` (spend) and `lib/cost-reconciliation.ts` (`vendor_balance`), on a read-only handle from `lib/costs-db.ts`). `GET /api/costs/usage` feeds the Costs
|
|
522
|
+
page, `GET /api/costs` the dashboard, `GET /api/costs/agent` the agent panel. `GET
|
|
523
|
+
/api/stats` and `GET /api/stats-since` still read the `sessions` table of `events.db`,
|
|
524
|
+
whose `cost_usd` is no longer estimated (always 0).
|
|
525
|
+
|
|
526
|
+
### `GET /api/costs/usage?range=today|7d|30d`
|
|
527
|
+
What the agents spent in a range of UTC days, today included (default `7d`), read from
|
|
528
|
+
`costs.db` (written by the daemon every 5 minutes, see `docs/dev/DATABASE.md`). Rev4a
|
|
529
|
+
computes no cost: every figure is the one OpenClaw recorded with the prices Rev4a syncs.
|
|
530
|
+
|
|
531
|
+
**Auth:** browser cookie or Bearer
|
|
532
|
+
|
|
533
|
+
**Response:**
|
|
534
|
+
```json
|
|
535
|
+
{
|
|
536
|
+
"available": true,
|
|
537
|
+
"range": "7d",
|
|
538
|
+
"startDate": "2026-09-21",
|
|
539
|
+
"endDate": "2026-09-27",
|
|
540
|
+
"intervalS": 300,
|
|
541
|
+
"pricing": { "vendor": "DeepSeek", "band": "off-peak", "nextChange": 1790557200000, "source": "https://api-docs.deepseek.com/quick_start/pricing" },
|
|
542
|
+
"totals": { "cost": 0.0086, "tokens": 114053, "input": 47436, "output": 2233, "cacheRead": 64384, "cacheWrite": 0,
|
|
543
|
+
"inputCost": 0.0071, "outputCost": 0.0013, "cacheReadCost": 0.0002, "cacheWriteCost": 0, "missing": 0 },
|
|
544
|
+
"daily": [ { "date": "2026-09-21", "cost": 0.0071, "tokens": 96806, "missing": 0 } ],
|
|
545
|
+
"byAgent": [ { "container": "agent_9253eee3", "agentId": "agent_9253eee3", "name": "test", "cost": 0.0077, "tokens": 96806,
|
|
546
|
+
"missing": 0, "collectedAt": 1790526404843, "attemptedAt": 1790526404843, "error": null } ],
|
|
547
|
+
"byModel": [ { "provider": "rev4a", "model": "deepseek/deepseek-flash", "cost": 0.0086, "tokens": 114053, "calls": 13 } ],
|
|
548
|
+
"sessions": [ { "container": "agent_9253eee3", "agentName": "test", "sessionId": "…", "sessionKey": "agent:main:…", "label": "…",
|
|
549
|
+
"agentId": "main", "model": "deepseek/deepseek-flash", "cost": 0.004, "tokens": 18681, "missing": 0, "lastActivity": 1790322536609 } ],
|
|
550
|
+
"unpriced": [ { "model": "rev4a/deepseek/deepseek-v4-flash", "calls": 84, "agents": ["test", "test 2"] } ],
|
|
551
|
+
"reconciliation": [ { "vendor": "deepseek", "currency": "USD", "from": 1790529518869, "to": 1790536317000,
|
|
552
|
+
"billed": 0.02, "priced": 0.0171, "topUps": 0, "readings": 3, "comparable": true } ]
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
- `daily` has one entry per day of the range, zeros included.
|
|
557
|
+
- `missing` counts calls OpenClaw could not price (no price for their model): they are in
|
|
558
|
+
the tokens, not in the cost. `unpriced` lists them by model over each agent's last 30
|
|
559
|
+
days.
|
|
560
|
+
- `byAgent` holds every agent read, running or not, with use in the range, an error, or
|
|
561
|
+
a read in the last 15 minutes; `error` is why its last read stored nothing (OpenClaw's
|
|
562
|
+
own reason, e.g. "Gateway not reachable…"). An agent still being read (`attemptedAt`
|
|
563
|
+
within 3 × `intervalS`) with an `error` or a `collectedAt` older than that is not
|
|
564
|
+
current; a stopped or deleted agent is no longer read and keeps its last figures.
|
|
565
|
+
- `sessions`: the 20 most expensive sessions active in the range; a session's figures are
|
|
566
|
+
its total over the last 30 days.
|
|
567
|
+
- `pricing`: the band DeepSeek bills at now and when it changes, when a DeepSeek model is
|
|
568
|
+
on offer; `null` otherwise.
|
|
569
|
+
- `reconciliation`: per vendor with a configured key (DeepSeek, OpenRouter), what it
|
|
570
|
+
billed next to what the agents priced over the same hourly readings — `{ vendor,
|
|
571
|
+
currency, from, to, billed, priced, topUps, readings, comparable }`. `billed` is the
|
|
572
|
+
balance drop (DeepSeek; rises are `topUps`, not spend) or the usage growth (OpenRouter);
|
|
573
|
+
`priced` the growth of the agents' total for that vendor's models. `billed`/`priced`
|
|
574
|
+
are `null` until two readings exist; `comparable` is false for a non-USD balance.
|
|
575
|
+
`vendor` is the lowercase key (`deepseek`, `openrouter`); `pricing.vendor` is a display
|
|
576
|
+
name. Known gaps, shown rather than corrected: the same key may also pay for the
|
|
577
|
+
assistant or clients outside Rev4a; a top-up and spend in the same hour net out; an
|
|
578
|
+
agent missed by a pass joins the agents' total one pass later; calls never priced are
|
|
579
|
+
priced by OpenClaw when read, so the agents' total can grow for calls made earlier.
|
|
580
|
+
- `available: false` (zeros, empty lists) until the daemon has created `costs.db`.
|
|
581
|
+
|
|
582
|
+
**Errors:** `400` for another `range`; `500` when `costs.db` cannot be read.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
520
586
|
### `GET /api/stats`
|
|
521
|
-
Current-month aggregate stats (tokens + cost) grouped by model
|
|
587
|
+
Current-month aggregate stats (tokens + cost) grouped by model, from the `sessions` table
|
|
588
|
+
(`started_at` is in milliseconds). The costs are the `sessions.cost_usd` column, which is no
|
|
589
|
+
longer estimated: 0, except rows written before 2026-09-27. Agent spend is `GET /api/costs/usage`.
|
|
522
590
|
|
|
523
591
|
**Auth:** Bearer or browser cookie
|
|
524
592
|
|
|
@@ -526,13 +594,13 @@ Current-month aggregate stats (tokens + cost) grouped by model.
|
|
|
526
594
|
```json
|
|
527
595
|
{
|
|
528
596
|
"total": {
|
|
529
|
-
"total":
|
|
597
|
+
"total": 0,
|
|
530
598
|
"total_in": 850000,
|
|
531
599
|
"total_out": 210000,
|
|
532
600
|
"sessions": 47
|
|
533
601
|
},
|
|
534
602
|
"byModel": [
|
|
535
|
-
{ "model": "
|
|
603
|
+
{ "model": "rev4a/deepseek/deepseek-flash", "cost": 0, "tokens_in": 600000, "tokens_out": 150000, "sessions": 30 }
|
|
536
604
|
]
|
|
537
605
|
}
|
|
538
606
|
```
|
|
@@ -540,7 +608,8 @@ Current-month aggregate stats (tokens + cost) grouped by model.
|
|
|
540
608
|
---
|
|
541
609
|
|
|
542
610
|
### `GET /api/stats-since?ts=<unix_ms>`
|
|
543
|
-
|
|
611
|
+
Sum of `sessions.cost_usd` since a given timestamp — 0 for sessions polled since 2026-09-27
|
|
612
|
+
(see `GET /api/stats`).
|
|
544
613
|
|
|
545
614
|
**Auth:** Bearer or browser cookie
|
|
546
615
|
|
|
@@ -548,28 +617,49 @@ Total cost (USD) since a given timestamp.
|
|
|
548
617
|
|
|
549
618
|
**Response:**
|
|
550
619
|
```json
|
|
551
|
-
{ "total": 0
|
|
620
|
+
{ "total": 0 }
|
|
552
621
|
```
|
|
553
622
|
|
|
554
623
|
---
|
|
555
624
|
|
|
556
625
|
### `GET /api/costs`
|
|
557
|
-
|
|
626
|
+
The dashboard's figures: today's spend (UTC day), all-time spend, today's calls without a
|
|
627
|
+
price, and today's spend by model — from `costs.db`. The SSE stream (`GET /api/stream`)
|
|
628
|
+
sends the same `today`.
|
|
558
629
|
|
|
559
|
-
**Auth:** browser cookie
|
|
630
|
+
**Auth:** browser cookie or Bearer
|
|
560
631
|
|
|
561
632
|
**Response:**
|
|
562
633
|
```json
|
|
563
634
|
{
|
|
564
|
-
"today": 0.
|
|
565
|
-
"
|
|
566
|
-
"
|
|
567
|
-
"
|
|
568
|
-
"
|
|
569
|
-
"byModel": [ ... ]
|
|
635
|
+
"today": 0.0112,
|
|
636
|
+
"allTime": 0.0559,
|
|
637
|
+
"allTimeSource": "agents_openclaw",
|
|
638
|
+
"missingToday": 0,
|
|
639
|
+
"byModel": [ { "model": "deepseek/deepseek-flash", "cost_usd": 0.0112, "tokens": 244157, "calls": 7 } ]
|
|
570
640
|
}
|
|
571
641
|
```
|
|
572
642
|
|
|
643
|
+
`byModel` lists the models that cost something today; the free and unpriced ones are on the
|
|
644
|
+
Costs page.
|
|
645
|
+
|
|
646
|
+
**Errors:** `500` when `costs.db` cannot be read.
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
### `GET /api/costs/agent?container=<name>`
|
|
651
|
+
One agent's spend for its panel, from `costs.db`.
|
|
652
|
+
|
|
653
|
+
**Auth:** browser cookie or Bearer
|
|
654
|
+
|
|
655
|
+
**Response:** `{ "available": true, "intervalS": 300, "today": 0.011, "week": 0.019,
|
|
656
|
+
"month": 0.022, "tokensMonth": 2409574, "topModel": { "model": "deepseek/deepseek-flash",
|
|
657
|
+
"cost": 0.022 }, "unpricedMonth": 81, "collectedAt": 1790529516737, "error": null }` —
|
|
658
|
+
UTC days (today, last 7, last 30); `collectedAt`/`error` from the agent's last read.
|
|
659
|
+
`{ "available": false, "intervalS": 300 }` before `costs.db` exists.
|
|
660
|
+
|
|
661
|
+
**Errors:** `400` for an invalid container name; `500` when `costs.db` cannot be read.
|
|
662
|
+
|
|
573
663
|
---
|
|
574
664
|
|
|
575
665
|
### `GET /api/cost-override?month=YYYY-MM`
|
|
@@ -619,13 +709,17 @@ Paginated event log.
|
|
|
619
709
|
---
|
|
620
710
|
|
|
621
711
|
### `GET /api/stream`
|
|
622
|
-
Server-Sent Events stream.
|
|
712
|
+
Server-Sent Events stream. Every 5 s it sends one message per kind, each `{ type, data }`:
|
|
713
|
+
`events` (new rows of the `events` table, only when there are any), `sessions` (the 100 most
|
|
714
|
+
recent sessions with their lineage label) and `costs` (`{ "today": … }`, what the agents spent
|
|
715
|
+
today, UTC day — the same figure as `GET /api/costs`).
|
|
623
716
|
|
|
624
|
-
**Auth:** browser cookie
|
|
717
|
+
**Auth:** browser cookie or Bearer
|
|
625
718
|
|
|
626
719
|
**Event format:**
|
|
627
720
|
```
|
|
628
|
-
data: {"
|
|
721
|
+
data: {"type":"sessions","data":[...]}
|
|
722
|
+
data: {"type":"costs","data":{"today":0.0112}}
|
|
629
723
|
```
|
|
630
724
|
|
|
631
725
|
Use `EventSource` in the browser or `curl -N` for testing.
|
|
@@ -1951,6 +2045,81 @@ average and peak: 60 s for `1h`, 720 s for `24h`, 5040 s for `7d`, 21 600 s for
|
|
|
1951
2045
|
means the daemon is not sampling; the page says so.
|
|
1952
2046
|
- `host` is read live by the API process, which runs on the same host.
|
|
1953
2047
|
|
|
2048
|
+
### `GET /api/metrics/containers?range=1h|24h|7d|30d`
|
|
2049
|
+
What every container consumes — now, and as an average and peak over the range — plus what is
|
|
2050
|
+
not a container and the disk Docker holds. From `metrics.db` (the daemon samples every 60 s,
|
|
2051
|
+
see `docs/dev/DATABASE.md`). Default range `1h`.
|
|
2052
|
+
|
|
2053
|
+
**Auth:** browser cookie or Bearer
|
|
2054
|
+
|
|
2055
|
+
**Response:**
|
|
2056
|
+
```json
|
|
2057
|
+
{
|
|
2058
|
+
"available": true, "range": "1h", "interval_s": 60,
|
|
2059
|
+
"host": { "ncpu": 10, "mem_total_mb": 7837 },
|
|
2060
|
+
"sampled_at": 1790789760, "age_s": 10,
|
|
2061
|
+
"containers": [
|
|
2062
|
+
{ "container": "agent_9253eee3", "name": "test", "agent_id": "agent_9253eee3", "is_agent": true,
|
|
2063
|
+
"running": true, "last_seen": 1790789760,
|
|
2064
|
+
"now": { "cpu_cores": 0.022, "cpu_percent": 0.22, "mem_mb": 668, "mem_percent": 8.5, "pids": 12,
|
|
2065
|
+
"net_rx_bps": 0, "net_tx_bps": 0, "blk_read_bps": 0, "blk_write_bps": 0 },
|
|
2066
|
+
"range": { "cpu_avg_percent": 1.02, "cpu_max_percent": 2.17, "mem_avg_mb": 679, "mem_max_mb": 701 },
|
|
2067
|
+
"storage": { "volume_mb": 1544, "layer_mb": 10, "total_mb": 1554, "ts": 1790789645 } }
|
|
2068
|
+
],
|
|
2069
|
+
"rest": { "cpu_percent": 42.26, "mem_mb": 11864 },
|
|
2070
|
+
"docker_storage": { "ts": 1790789645, "images_mb": 4734, "images_reclaimable_mb": 3518,
|
|
2071
|
+
"build_cache_mb": 1597, "build_cache_reclaimable_mb": 1597,
|
|
2072
|
+
"volumes_mb": 10120, "unused_volumes_mb": 7337, "layers_mb": 30,
|
|
2073
|
+
"unused_volumes": [ { "name": "rev4a-backups", "size_mb": 7337 } ] }
|
|
2074
|
+
}
|
|
2075
|
+
```
|
|
2076
|
+
|
|
2077
|
+
- `cpu_percent` is a share of Docker's cores (`host.ncpu`), `mem_percent` of Docker's memory —
|
|
2078
|
+
on Docker Desktop that is its VM, on the VPS the machine. `cpu_cores` is the raw figure.
|
|
2079
|
+
- `now` is `null` for a container that is not running (no sample within 3 intervals of the
|
|
2080
|
+
newest sample — running is judged against the newest sample, not the wall clock, so a
|
|
2081
|
+
stopped daemon does not make every container look stopped);
|
|
2082
|
+
`running: false` ones stay listed while they were seen in the range. A rate that is null in
|
|
2083
|
+
the newest sample (the first after a start) is replaced by the previous reading.
|
|
2084
|
+
- The list is sorted running first, then by CPU now. `storage` is the named volumes plus the
|
|
2085
|
+
writable layer, from the last Docker disk reading (every 10 min).
|
|
2086
|
+
- `rest` is the machine's newest sample minus the running containers (the containers' CPU
|
|
2087
|
+
re-scaled to the machine's cores first, so on Docker Desktop the two shares still compare):
|
|
2088
|
+
the host, Rev4a and other software (`null` without a machine sample or Docker's figures).
|
|
2089
|
+
- `docker_storage.*_reclaimable_mb` and `unused_volumes` (the 5 largest): what **no container
|
|
2090
|
+
uses** — images of older agent versions you may still roll back to count as such; a volume
|
|
2091
|
+
like the cold backups' is listed, not advised for removal. `volumes_mb` sums all volumes;
|
|
2092
|
+
`unused_volumes_mb` is the total of what no container uses, `unused_volumes` only names the
|
|
2093
|
+
five largest.
|
|
2094
|
+
- `available: false` until the daemon has created the tables; with the tables present and no
|
|
2095
|
+
rows yet it answers `available: true` with an empty list.
|
|
2096
|
+
|
|
2097
|
+
**With `&container=<name>`** — that container's history, bucketed like `GET /api/metrics`:
|
|
2098
|
+
```json
|
|
2099
|
+
{ "available": true, "range": "1h", "interval_s": 60, "host": { "ncpu": 10, "mem_total_mb": 7837 },
|
|
2100
|
+
"container": "agent_9253eee3", "name": "test", "bucket_s": 120,
|
|
2101
|
+
"history": [ { "ts": 1790789640, "cpu_avg": 1.41, "cpu_max": 2.17, "mem_avg": 685, "mem_max": 701 } ],
|
|
2102
|
+
"storage_bucket_s": 1200, "storage_history": [ { "ts": 1790788800, "total_mb": 1554 } ] }
|
|
2103
|
+
```
|
|
2104
|
+
`cpu_*` are shares of Docker's cores, `mem_*` MB. A container that was not running has no
|
|
2105
|
+
points for that stretch (a gap). A name that matches the rules but was never sampled answers
|
|
2106
|
+
`available: true` with an empty history.
|
|
2107
|
+
|
|
2108
|
+
**Errors:** `400` for another `range` or an invalid container name. A `metrics.db` that
|
|
2109
|
+
cannot be opened (missing, corrupt) answers `available: false`, not an error.
|
|
2110
|
+
|
|
2111
|
+
### `GET /api/metrics/alerts?limit=10`
|
|
2112
|
+
The machine's latest `system_anomaly` events (CPU above 85 % or memory above 90 % on two
|
|
2113
|
+
samples in a row, a disk at 90 %), newest first, from `events.db`. `limit` 1–50, default 10.
|
|
2114
|
+
|
|
2115
|
+
**Auth:** browser cookie or Bearer
|
|
2116
|
+
|
|
2117
|
+
**Response:**
|
|
2118
|
+
```json
|
|
2119
|
+
{ "alerts": [ { "ts": 1790789625355, "metric": "disk", "message": "Disk high: / 94% used", "threshold": 90, "values": [94] } ] }
|
|
2120
|
+
```
|
|
2121
|
+
`ts` is in **milliseconds** (the `events` table's unit); `metric` is `cpu`, `ram`, `disk` or `null`.
|
|
2122
|
+
|
|
1954
2123
|
---
|
|
1955
2124
|
|
|
1956
2125
|
## System Health
|
|
@@ -1973,14 +2142,16 @@ say so; it does not fail the others.
|
|
|
1973
2142
|
{ "id": "runtime.sessions", "label": "Runtime sessions", "health": "ok", "value": 1, "details": "last session activity 3m ago", "source": "runtime" }
|
|
1974
2143
|
],
|
|
1975
2144
|
"recommendations": [
|
|
1976
|
-
{ "id": "cost.review-usage-based", "severity": "warning", "source": "cost", "title": "High
|
|
2145
|
+
{ "id": "cost.review-usage-based", "severity": "warning", "source": "cost", "title": "High spend today", "details": "…", "actionHref": "/costs", "dismissible": true, "createdAt": "2026-09-26T10:00:00.000Z" }
|
|
1977
2146
|
],
|
|
1978
2147
|
"generatedAt": "2026-09-26T10:00:00.000Z"
|
|
1979
2148
|
}
|
|
1980
2149
|
```
|
|
1981
2150
|
|
|
1982
2151
|
Checks: `runtime.sessions`, `runtime.heartbeat`, `runtime.errors24h`,
|
|
1983
|
-
`cost.usageBasedToday
|
|
2152
|
+
`cost.usageBasedToday` ("Spent today": the agents' spend today, UTC day, from `costs.db`;
|
|
2153
|
+
a warning above $10 or with calls without a price, each with its recommendation,
|
|
2154
|
+
`cost.review-usage-based` / `cost.unpriced-calls`), `cron.jobs`. `health` (overall and per check) is `"ok"` /
|
|
1984
2155
|
`"warning"` / `"error"`; a recommendation's `severity` is `"info"` / `"warning"` /
|
|
1985
2156
|
`"critical"`. `generatedAt` and `createdAt` are ISO strings.
|
|
1986
2157
|
|
|
@@ -2206,27 +2377,26 @@ Full key management documentation at [PROVIDERS.md](PROVIDERS.md).
|
|
|
2206
2377
|
## Containers
|
|
2207
2378
|
|
|
2208
2379
|
### `GET /api/containers`
|
|
2209
|
-
|
|
2380
|
+
Every Docker container, running or stopped, with its identity and network details. What a
|
|
2381
|
+
running container consumes lives in `GET /api/metrics/containers`.
|
|
2210
2382
|
|
|
2211
|
-
**Auth:** browser cookie
|
|
2383
|
+
**Auth:** browser cookie or Bearer
|
|
2212
2384
|
|
|
2213
|
-
**Response:**
|
|
2385
|
+
**Response:** a bare array —
|
|
2214
2386
|
```json
|
|
2215
|
-
|
|
2216
|
-
"
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
"status": "running",
|
|
2221
|
-
"ports": ["0.0.0.0:3731->3000/tcp"],
|
|
2222
|
-
"created": "2026-06-20T10:00:00Z",
|
|
2223
|
-
"cpu_percent": 2.1,
|
|
2224
|
-
"mem_usage_mb": 340
|
|
2225
|
-
}
|
|
2226
|
-
]
|
|
2227
|
-
}
|
|
2387
|
+
[
|
|
2388
|
+
{ "id": "a1b2c3d4e5f6", "name": "openclaw-atlas", "image": "openclaw-agent-base:2026.9.3",
|
|
2389
|
+
"status": "Up 3 hours", "state": "running", "ports": "0.0.0.0:3731->3000/tcp",
|
|
2390
|
+
"ip": "172.18.0.2", "agentId": "agent_atlas", "displayName": "Atlas", "created": null }
|
|
2391
|
+
]
|
|
2228
2392
|
```
|
|
2229
2393
|
|
|
2394
|
+
- `name` is the daemon's `primaryName` (the name with one leading slash) — the same key the
|
|
2395
|
+
metrics store and the `/system?agent=` link use.
|
|
2396
|
+
- `displayName` is the agent's `AGENT_NAME`, read from the inspect. If an inspect fails, the
|
|
2397
|
+
row still comes from Docker's list (`ip` and `displayName` then `null`): a container is
|
|
2398
|
+
never dropped, and never renamed, because one call failed.
|
|
2399
|
+
|
|
2230
2400
|
The container list is the only container route. There is no logs endpoint and no
|
|
2231
2401
|
action endpoint: logs are read through the terminal WebSocket, and lifecycle
|
|
2232
2402
|
operations on agent containers go through `/api/agents/[id]/lifecycle`.
|