@flame0510/project-aether 1.6.1 → 1.7.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -2
  3. package/agent-templates/atlas/HEARTBEAT.md +1 -1
  4. package/app/agents/ImageDownloadBanner.tsx +171 -37
  5. package/app/api/agents/download-image/route.ts +29 -5
  6. package/app/api/agents/image-status/route.ts +17 -2
  7. package/app/api/assistant/route.ts +13 -4
  8. package/app/api/auth/login/route.ts +2 -2
  9. package/app/api/setup/agent-image/route.ts +6 -4
  10. package/app/api/system-health/route.ts +3 -3
  11. package/app/components/DashboardToolbar.tsx +2 -2
  12. package/app/components/LineageGraphPage.tsx +4 -4
  13. package/app/components/SessionDrawer.tsx +8 -8
  14. package/app/components/SystemCockpit.tsx +1 -1
  15. package/app/globals.css +1 -1
  16. package/app/setup/PageClient.tsx +1 -1
  17. package/bin/postinstall.js +5 -1
  18. package/daemon.js +13 -34
  19. package/docs/ARCHITECTURE.md +44 -26
  20. package/docs/CONTAINER-TERMINAL.md +17 -8
  21. package/docs/DESIGN-SYSTEM.md +21 -12
  22. package/docs/FRONTEND-ARCHITECTURE.md +2 -2
  23. package/docs/REV4A.md +35 -85
  24. package/docs/dev/API-REFERENCE.md +73 -19
  25. package/docs/dev/DATABASE.md +18 -7
  26. package/docs/dev/GATEWAY.md +23 -0
  27. package/docs/dev/SESSION-MAINTENANCE-PLAN.md +6 -6
  28. package/docs/rag/DATA-FRESHNESS.md +20 -9
  29. package/docs/rag/GLOSSARY.md +2 -2
  30. package/docs/rag/REV4A-OVERVIEW.md +10 -6
  31. package/docs/rag/WHAT-I-CAN-ANSWER.md +3 -3
  32. package/lib/agent-images.ts +43 -14
  33. package/lib/buildAgentImage.ts +142 -6
  34. package/lib/patterns/sessionPresentation.ts +4 -2
  35. package/lib/rev4a-auth.d.ts +1 -0
  36. package/lib/rev4a-auth.js +18 -2
  37. package/models.config.json +937 -45
  38. package/next.config.mjs +9 -1
  39. package/package.json +5 -3
  40. package/scripts/backup.sh +48 -54
  41. package/scripts/check-language.mjs +21 -3
  42. package/scripts/model-info-verify.mjs +194 -0
  43. package/scripts/restore.sh +77 -59
@@ -180,27 +180,27 @@ export default function SessionDrawer({ sessionId, onClose }: SessionDrawerProps
180
180
  {truncate(session.session_id, 60)}
181
181
  </div>
182
182
  <Grid>
183
- <Row label="STATO">
183
+ <Row label="STATUS">
184
184
  <span style={{ color: statusColor(session.status) }}>●</span>{' '}
185
185
  {session.status ?? 'idle'}
186
186
  </Row>
187
- <Row label="TIPO">
187
+ <Row label="TYPE">
188
188
  {session.session_id.includes(':cron:') ? 'cron' : 'session'}
189
189
  </Row>
190
190
  <Row label="MODEL">{session.model ?? '-'}</Row>
191
- <Row label="COSTO">{formatUsdOrDash(session.cost_usd)}</Row>
191
+ <Row label="COST">{formatUsdOrDash(session.cost_usd)}</Row>
192
192
  <Row label="TOKEN">
193
193
  in: {formatTokens(session.tokens_in)} out: {formatTokens(session.tokens_out)}
194
194
  </Row>
195
- <Row label="INIZIO">{formatTimeFromUnixSeconds(session.started_at)}</Row>
196
- <Row label="AGGIORN.">{formatTimeFromUnixSeconds(session.updated_at)}</Row>
197
- <Row label="FINE">{formatTimeFromUnixSeconds(session.ended_at)}</Row>
198
- <Row label="DURATA">{formatDuration(session.started_at, endedOrUpdated)}</Row>
195
+ <Row label="STARTED">{formatTimeFromUnixSeconds(session.started_at)}</Row>
196
+ <Row label="UPDATED">{formatTimeFromUnixSeconds(session.updated_at)}</Row>
197
+ <Row label="ENDED">{formatTimeFromUnixSeconds(session.ended_at)}</Row>
198
+ <Row label="DURATION">{formatDuration(session.started_at, endedOrUpdated)}</Row>
199
199
  <Row label="TASK">{truncate(session.task_preview ?? '-', 200)}</Row>
200
200
  </Grid>
201
201
  </Section>
202
202
 
203
- <Section title="AZIONI RECENTI">
203
+ <Section title="RECENT ACTIONS">
204
204
  {events.length === 0 && <Muted>No events</Muted>}
205
205
  {events.map((evt) => (
206
206
  <div key={evt.id ?? `${evt.ts ?? 0}-${evt.type ?? 'event'}`} style={{ fontSize: 12 }}>
@@ -144,7 +144,7 @@ export default function SystemCockpit({
144
144
  <span>{item.severity}</span>
145
145
  </div>
146
146
  <div className="ui-muted" style={{ marginTop: 4 }}>{item.details}</div>
147
- {item.actionHref && <Link href={item.actionHref} className="ui-link" style={{ display: 'inline-block', marginTop: 6 }}>Apri →</Link>}
147
+ {item.actionHref && <Link href={item.actionHref} className="ui-link" style={{ display: 'inline-block', marginTop: 6 }}>Open →</Link>}
148
148
  </li>
149
149
  ))}
150
150
  </ul>
package/app/globals.css CHANGED
@@ -1387,7 +1387,7 @@ html, body { height: 100%; height: 100dvh; background: var(--bg); color: var(--t
1387
1387
  white-space: nowrap;
1388
1388
  }
1389
1389
 
1390
- /* Padding bottom per non coprire contenuti — handled by .app-shell__content */
1390
+ /* Bottom padding so content is not covered — handled by .app-shell__content */
1391
1391
 
1392
1392
  /* Chat button raised above the bottom navbar */
1393
1393
  .ochat__trigger {
@@ -149,7 +149,7 @@ export default function SetupPageClient() {
149
149
  <div style={{ display: 'grid', gap: 6, textAlign: 'left', padding: '10px 12px', background: 'rgba(52,211,153,0.06)', border: '1px solid rgba(52,211,153,0.15)' }}>
150
150
  {[
151
151
  'Set admin password',
152
- 'Agent base image is built by the setup script',
152
+ 'Agent base image: download it from the banner on the Agents page',
153
153
  'Next: add providers & agents in the wizard',
154
154
  ].map((t) => (
155
155
  <div key={t} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--text)' }}>
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Post-install hook: build Next.js app if .next doesn't exist.
4
+ *
5
+ * Skipped when REV4A_SKIP_POSTINSTALL_BUILD is set: CI and the release workflow run
6
+ * `npm run build` as an explicit step, and a second build here would run before every
7
+ * other check and swallow its own failure.
4
8
  */
5
9
  const { existsSync } = require('fs');
6
10
  const { join } = require('path');
@@ -8,7 +12,7 @@ const { join } = require('path');
8
12
  const ROOT = join(__dirname, '..');
9
13
  const NEXT_DIR = join(ROOT, '.next');
10
14
 
11
- if (!existsSync(NEXT_DIR)) {
15
+ if (!process.env.REV4A_SKIP_POSTINSTALL_BUILD && !existsSync(NEXT_DIR)) {
12
16
  try {
13
17
  const { execSync } = require('child_process');
14
18
  console.log('[rev4a] Building dashboard… (one-time, may take a minute)');
package/daemon.js CHANGED
@@ -14,7 +14,6 @@ const path = require('path');
14
14
 
15
15
  const DB_PATH = process.env.REV4A_DB || path.join(os.homedir(), '.config', 'rev4a', 'data', 'events.db');
16
16
  const POLL_INTERVAL_MS = 30_000;
17
- const POLL_INTERVAL_ACTIVE_MS = 15_000; // 15s when working sessions detected
18
17
 
19
18
  // Cost rates per 1M tokens (separate in/out pricing)
20
19
  const MODEL_PRICING = {
@@ -300,19 +299,14 @@ function readHostCpuSample() {
300
299
  return { idle, total };
301
300
  }
302
301
 
302
+ // The filesystem holding Rev4a's own data (the events database's directory), in GB.
303
+ // statfs works the same on Linux and macOS; the previous `df -BG /data` read a mount
304
+ // that existed only on one old server and used GNU-only flags, so it reported 0/0.
303
305
  function getDiskStats() {
304
306
  try {
305
- const result = spawnSync('df', ['-BG', '/data', '--output=used,size'], {
306
- encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL',
307
- });
308
- if (result.status !== 0 || !result.stdout) return { used: 0, total: 0 };
309
- const lines = result.stdout.trim().split('\n');
310
- // lines[0] = header, lines[1] = data
311
- if (lines.length < 2) return { used: 0, total: 0 };
312
- const parts = lines[1].trim().split(/\s+/);
313
- const used = parseFloat(parts[0]) || 0; // already in GB (BG flag strips G)
314
- const total = parseFloat(parts[1]) || 0;
315
- return { used, total };
307
+ const st = fs.statfsSync(path.dirname(DB_PATH));
308
+ const gb = (blocks) => Math.round((blocks * st.bsize) / 1024 ** 3);
309
+ return { used: gb(st.blocks - st.bfree), total: gb(st.blocks) };
316
310
  } catch (e) {
317
311
  return { used: 0, total: 0 };
318
312
  }
@@ -357,10 +351,10 @@ function collectSystemMetrics() {
357
351
  metric: 'cpu',
358
352
  values: [recent[1].cpu_percent, recent[0].cpu_percent],
359
353
  threshold: 85,
360
- message: `CPU alta: ${recent[0].cpu_percent}% per 2 campioni consecutivi`,
354
+ message: `CPU high: ${recent[0].cpu_percent}% for 2 consecutive samples`,
361
355
  }),
362
356
  });
363
- log(`[ANOMALY] CPU alta: ${recent[0].cpu_percent}%`);
357
+ log(`[ANOMALY] CPU high: ${recent[0].cpu_percent}%`);
364
358
  }
365
359
  }
366
360
  // RAM >90% for 2 consecutive samples
@@ -377,10 +371,10 @@ function collectSystemMetrics() {
377
371
  metric: 'ram',
378
372
  values: [ram1pct, ram0pct],
379
373
  threshold: 90,
380
- message: `RAM alta: ${ram0pct}% per 2 campioni consecutivi`,
374
+ message: `RAM high: ${ram0pct}% for 2 consecutive samples`,
381
375
  }),
382
376
  });
383
- log(`[ANOMALY] RAM alta: ${ram0pct}%`);
377
+ log(`[ANOMALY] RAM high: ${ram0pct}%`);
384
378
  }
385
379
  }
386
380
  }
@@ -540,7 +534,7 @@ function pollSessions() {
540
534
  const session_id = s.key;
541
535
 
542
536
  // Skip Telegram channel/group sessions (multi-user).
543
- // Keep Telegram direct sessions (agent:ops:telegram:argus:direct:...) as root nodes
537
+ // Keep Telegram direct sessions (agent:<id>:telegram:<account>:direct:...) as root nodes
544
538
  // since they are the parent of all sub-agents spawned via Telegram.
545
539
  if (session_id.includes(':telegram:') && !session_id.includes(':direct:')) continue;
546
540
  const { label, parent_id: inferredParent } = parseSessionKey(s.key);
@@ -698,27 +692,12 @@ function pollSessions() {
698
692
  }),
699
693
  });
700
694
  log(`[TIMEOUT] ${session_id} missing for ${Math.round(missingFor / 60000)} min`);
701
-
702
- // Notify Michele via openclaw message (only if openclaw is available)
703
- try {
704
- if (!openclawMissing) {
705
- execSync('which openclaw', { stdio: 'ignore', timeout: 3000 });
706
- }
707
- spawnSync('openclaw', [
708
- 'message', 'send',
709
- '--account', 'ops',
710
- '--target', '297086793',
711
- '--text', `⚠️ Rev4a: agent timeout\n\`${session_id.slice(-36)}\`\nMissing for ${Math.round(missingFor / 60000)} min without completing.`,
712
- ], { encoding: 'utf8', timeout: 10_000, killSignal: 'SIGKILL' });
713
- } catch (e) {
714
- log(`[TIMEOUT] Telegram notification failed: ${e.message}`);
715
- }
716
695
  }
717
696
 
718
697
  // Collect system metrics after each poll
719
698
  try { collectSystemMetrics(); } catch (e) { log(`[METRICS ERROR] ${e.message}`); }
720
699
 
721
- // Force names and parents declared via lineage — overrides any previous label (including "Sub-agente")
700
+ // Force names and parents declared via lineage — overrides any previous label, including the generic placeholder older data carries
722
701
  const updateLabel = db.prepare('UPDATE sessions SET label = ?, parent_id = ? WHERE session_id = ?');
723
702
  const applyLineage = db.transaction(() => {
724
703
  for (const [child_id, agent_name] of Object.entries(declaredNames)) {
@@ -741,7 +720,7 @@ function pollSessions() {
741
720
  log(`Retention cleanup: ${r1.changes} cron sessions, ${r2.changes} cron events deleted`);
742
721
  }
743
722
 
744
- // Prune knownSessions — delete completed sessions da >30 giorni
723
+ // Prune knownSessions — delete completed sessions older than 30 days
745
724
  const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000);
746
725
  for (const [id, snap] of knownSessions) {
747
726
  if (snap.status === 'completed' && snap.updatedAt && snap.updatedAt < cutoff) {
@@ -1,7 +1,7 @@
1
1
  # Rev4a Architecture — Design & Vision
2
2
 
3
3
  > **Status:** Active — `main` branch
4
- > **Last updated:** 2026-09-22
4
+ > **Last updated:** 2026-09-26
5
5
  > **Goal:** Transform Rev4a from a monitoring dashboard into a central orchestrator for a distributed multi-container agency.
6
6
 
7
7
  ---
@@ -35,7 +35,7 @@
35
35
 
36
36
  ```
37
37
  ┌────────────────────────────────────────────────────────────────┐
38
- │ DOCKER HOST (187.77.156.41, 4 CPU, 15 GB RAM, 193 GB disk) │
38
+ │ DOCKER HOST │
39
39
  │ │
40
40
  │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
41
41
  │ │ rev4a-control │ │ agent-argus │ │ agent-atlas │ │
@@ -132,6 +132,18 @@ The central container, running the Next.js dashboard + orchestration API.
132
132
  a key), `isModelOffered()` (enforced by the proxy and the assistant) and
133
133
  `catalogueStatus()`. A failed read of `models.config.json` falls back to the last
134
134
  good copy and never deletes overrides.
135
+ - **The model data has three files and three scripts.** `models.config.json` is curated by
136
+ hand (identity, `enabled`, `deprecated`, and an optional `info` block for what no API
137
+ publishes: the vendor's size claim, benchmarks, the docs link, notes).
138
+ `model-pricing.json` and `model-details.json` are **generated** by
139
+ `npm run refresh:pricing` (`scripts/refresh-model-pricing.mjs`): prices from OpenRouter,
140
+ and per model the description, architecture and benchmarks from OpenRouter plus the size,
141
+ weight mix, licence and dates from the Hugging Face card. Two scripts back the curation
142
+ of `info`: `npm run info:suggest` (`scripts/model-info-suggest.mjs`) prints the candidate
143
+ facts from a card, and `npm run info:verify` (`scripts/model-info-verify.mjs`) refuses a
144
+ proposal the card does not support — evidence per field, every benchmark value under the
145
+ column that names this model. `GET /api/models/details` merges the three files for the
146
+ details modal (`docs/dev/GATEWAY.md`).
135
147
  - **Agent images and OpenClaw versions.** `lib/agent-versions.json` lists the supported
136
148
  OpenClaw versions, newest first, with the model `input` list for each; the server reads
137
149
  it through `lib/agent-versions.ts`, the `rev4a` CLI with `require`. The provider sync
@@ -560,19 +572,18 @@ The Rev4a daemon (`daemon.js`) is a standalone Node.js process that bridges the
560
572
 
561
573
  ### Responsibilities
562
574
 
563
- 1. Poll `openclaw sessions --json --all-agents` on a configurable interval
575
+ 1. Poll `openclaw sessions --json --all-agents` every 30 s
564
576
  2. Upsert session rows into `events.db`
565
- 3. Emit `spawn` / `complete` / `error` events into the `events` table
566
- 4. Collect system metrics (CPU, RAM, disk) every poll cycle
567
- 5. Detect anomalies (CPU > 90%, RAM > 90%) and log warnings
577
+ 3. Emit `spawn` / `complete` / `fail` / `spawn_timeout` events into the `events` table
578
+ 4. Collect system metrics (CPU, RAM, disk) after every completed poll
579
+ 5. Detect anomalies (CPU > 85%, RAM > 90%) and record them
568
580
  6. Manage DB lifecycle (WAL mode, checkpoint after each cycle)
569
581
 
570
- ### Poll Intervals
582
+ ### Poll Interval
571
583
 
572
- | Condition | Interval |
573
- |---|---|
574
- | No active sessions | 30 s |
575
- | ≥ 1 session with status `working` | 15 s |
584
+ A fixed 30 s timer, whether or not sessions are working. When the OpenClaw CLI is not on
585
+ the host, the daemon logs it once and stays idle — and then records no system metrics
586
+ either, since they are collected at the end of a poll.
576
587
 
577
588
  ### Cost Estimation
578
589
 
@@ -614,28 +625,33 @@ The daemon uses `INSERT … ON CONFLICT DO UPDATE` with these rules:
614
625
 
615
626
  ### System Metrics
616
627
 
617
- Every poll cycle the daemon records a `system_metrics` row. Data sources:
628
+ After every completed poll the daemon records a `system_metrics` row. Data sources:
618
629
 
619
- - **CPU**: cgroup v2 usage delta (`/sys/fs/cgroup/cpu.stat`) when available, falls back to `/proc/stat` host ticks
620
- - **RAM**: `/proc/meminfo`
621
- - **Disk**: `df -BG /data`
630
+ - **CPU**: cgroup v2 usage delta (`/sys/fs/cgroup/cpu.stat`) when available, falls back to host ticks from `os.cpus()`
631
+ - **RAM**: `os.totalmem()` / `os.freemem()`
632
+ - **Disk**: `fs.statfsSync()` on the directory holding the events database — the
633
+ filesystem Rev4a's own data lives on; works the same on Linux and macOS
622
634
  - **Load**: `os.loadavg()[0]`
623
635
 
624
- Metrics older than 24 h are pruned automatically each cycle.
636
+ Metrics older than 30 days are pruned automatically each cycle.
625
637
 
626
638
  ### Anomaly Detection
627
639
 
628
- The daemon compares the last two metric samples. If a threshold is exceeded and the cooldown (5 min) has passed, it logs a `WARN` line:
640
+ The daemon compares the last two metric samples. When both exceed a threshold and the
641
+ cooldown (5 min per metric) has passed, it inserts a `system_anomaly` event
642
+ (`{ metric, values, threshold, message }`) and logs `[ANOMALY] CPU high: N%` or
643
+ `[ANOMALY] RAM high: N%`. No feature consumes `system_anomaly` events yet; they only pass through the generic
644
+ event feeds.
629
645
 
630
- | Metric | Threshold |
646
+ | Metric | Threshold (two consecutive samples) |
631
647
  |---|---|
632
- | CPU | > 90% |
648
+ | CPU | > 85% |
633
649
  | RAM | > 90% |
634
650
 
635
651
  ### DB Safety
636
652
 
637
653
  - WAL mode + `PRAGMA synchronous = NORMAL` for concurrent read safety
638
- - `PRAGMA wal_checkpoint(PASSIVE)` runs after each poll cycle
654
+ - `PRAGMA wal_checkpoint(PASSIVE)` runs after each poll cycle, `FULL` every 10 cycles
639
655
  - On `uncaughtException` the daemon logs but does NOT exit — relies on systemd Restart=always for recovery
640
656
 
641
657
  ### Migrations
@@ -722,17 +738,19 @@ ALTER TABLE sessions ADD COLUMN ended_at INTEGER;
722
738
 
723
739
  Rev4a provides a real, interactive terminal for any agent container via
724
740
  a WebSocket-connected PTY. The implementation is documented in detail in
725
- [docs/container-terminal.md](container-terminal.md).
741
+ [docs/CONTAINER-TERMINAL.md](CONTAINER-TERMINAL.md).
726
742
 
727
743
  ### Two-process architecture
728
744
 
729
745
  | Process | Port | Role |
730
746
  |---|---|---|
731
- | `rev4a-next` (Next.js) | 3740 | Serves the terminal page, auth, API |
732
- | `rev4a-terminal-ws` (standalone) | 3741 | WebSocket PTY server via `node-pty` |
747
+ | Next.js (`next start`) | 3740 | Serves the terminal page, auth, API |
748
+ | `terminal-ws-server.js` | 3741 (127.0.0.1) | WebSocket PTY server via `node-pty` |
733
749
 
734
- The terminal server runs as a separate systemd service (rev4a-terminal-ws) to avoid event-loop
735
- contention with Next.js during high-throughput I/O.
750
+ Both are started by `rev4a serve`; the terminal server is a process of its own to avoid
751
+ event-loop contention with Next.js during high-throughput I/O. It has no authentication
752
+ of its own and is reachable only through a reverse proxy route (see
753
+ [CONTAINER-TERMINAL.md](CONTAINER-TERMINAL.md#routing)).
736
754
 
737
755
  ### Why custom DOM over xterm.js
738
756
 
@@ -742,7 +760,7 @@ CSS conflicts. The current implementation uses a plain `<div>` with native
742
760
  browser scrolling and a hidden `<textarea>` for input — stable under any
743
761
  output volume.
744
762
 
745
- See [container-terminal.md](container-terminal.md#terminal-client-browser)
763
+ See [CONTAINER-TERMINAL.md](CONTAINER-TERMINAL.md#terminal-client-browser)
746
764
  for the full rationale.
747
765
 
748
766
  ## 12. Technology Stack
@@ -15,7 +15,7 @@ via `node-pty` and streaming I/O over a dedicated WebSocket server.
15
15
  Browser (xterm-compatible DOM terminal)
16
16
  │ WebSocket wss://rev4a/containers/terminal/{id}
17
17
  ▼
18
- terminal-ws-server.js (port 3741, proxied by Traefik)
18
+ terminal-ws-server.js (127.0.0.1:3741, reached through a reverse proxy — see Routing)
19
19
  │ node-pty
20
20
  ▼
21
21
  docker exec -it {containerId} env TERM=xterm-256color bash
@@ -30,20 +30,30 @@ Container Shell (bash, interactive, full PTY)
30
30
 
31
31
  | Process | Port | Role |
32
32
  |------------------|-------|------|
33
- | `rev4a-next` | 3740 | Next.js app (pages, API routes, auth) |
34
- | `rev4a-terminal-ws` | 3741 | Standalone WebSocket server for terminal sessions |
33
+ | Next.js (`next start`) | 3740 | Next.js app (pages, API routes, auth) |
34
+ | `terminal-ws-server.js` | 3741 | Standalone WebSocket server for terminal sessions, bound to 127.0.0.1 |
35
35
 
36
- The terminal server is a separate systemd service (rev4a-terminal-ws). It was isolated from
36
+ Both are child processes of `rev4a serve` (with `daemon.js`), under the single
37
+ `rev4a.service` unit in production. The terminal server is its own process, isolated from
37
38
  the Next.js app to avoid event-loop contention during high-throughput I/O (fast
38
39
  `seq`, `cat` on large files, interactive shell sessions).
39
40
 
40
- ### Traefik routing
41
+ ### Routing
41
42
 
42
43
  ```
43
44
  /containers/terminal/{id} → Next.js (page serving TerminalClient)
44
- /api/terminal-ws?id={containerId} → WebSocket proxy → ws://127.0.0.1:3741
45
+ /api/terminal-ws?id={containerId} → reverse proxy → ws://127.0.0.1:3741
45
46
  ```
46
47
 
48
+ The browser opens the socket on the dashboard's own host (`/api/terminal-ws`). Next.js
49
+ has no route for that path, so the terminal works only when a reverse proxy in front of
50
+ Rev4a forwards it to port 3741; without one the socket gets a 404.
51
+
52
+ **No authentication of its own.** `terminal-ws-server.js` checks neither the session
53
+ cookie nor the token: whoever reaches it gets a shell in the container named by `id`.
54
+ It listens on 127.0.0.1 only, so the exposure is exactly what the proxy route above
55
+ opens. An open issue.
56
+
47
57
  ### Data flow
48
58
 
49
59
  1. User opens `/containers/terminal/openclaw-atlas`
@@ -257,8 +267,7 @@ app/containers/terminal/
257
267
  ├── [id]/
258
268
  │ ├── page.tsx # Next.js page (auth-protected)
259
269
  │ └── TerminalClient.tsx # Client-side terminal component
260
- terminal-ws-server.js # WebSocket PTY server (rev4a-terminal-ws.service)
261
- rev4a.service # Systemd target for all three services
270
+ terminal-ws-server.js # WebSocket PTY server, started by `rev4a serve`
262
271
  ```
263
272
 
264
273
  ## Known Limitations
@@ -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-07-03
5
+ > **Last updated:** 2026-09-26
6
6
 
7
7
  ---
8
8
 
@@ -44,19 +44,22 @@ Rev4a uses a **square/sharp** visual language:
44
44
  Defined in `app/globals.css` as CSS custom properties:
45
45
 
46
46
  ```css
47
- --bg: #0a0a0a /* Page background */
48
- --bg2: #111 /* Slightly lighter surface */
49
- --border: #222 /* Default border */
50
- --text: #e0e0e0 /* Primary text */
51
- --text-dim: #888 /* Muted/secondary text */
52
- --violet: #a78bfa /* Brand accent */
53
- --violet-bg: #1a1030 /* Violet-tinted hover/active background */
54
- --green: #22c55e /* Success */
55
- --red: #ef4444 /* Error */
56
- --yellow: #eab308 /* Warning */
57
- --blue: #60a5fa /* Info / links */
47
+ --bg: var(--color-bg-950) /* #0A0A0B Page background */
48
+ --bg2: var(--color-bg-900) /* #111114 Slightly lighter surface */
49
+ --border: var(--color-border-700) /* #222228 Default border */
50
+ --text: var(--color-text-100) /* #E8E8E8 Primary text */
51
+ --text-dim: var(--color-text-400) /* #888 Muted/secondary text */
52
+ --violet: var(--color-accent-500) /* #925BFC Brand accent */
53
+ --violet-bg: rgba(146, 91, 252, 0.08) /* Violet-tinted hover/active background */
54
+ --green: var(--color-success-500) /* #22c55e Success */
55
+ --red: var(--color-danger-500) /* #ef4444 Error */
56
+ --yellow: var(--color-warning-500) /* #f59e0b Warning */
57
+ --blue: var(--color-blue-400) /* #60a5fa Info / links */
58
58
  ```
59
59
 
60
+ The semantic tokens above point at the palette tokens (`--color-*`) defined at the top of
61
+ `app/globals.css`; the hex values are the palette's, quoted for reference.
62
+
60
63
  ### Typography
61
64
 
62
65
  - Monospace by default: `var(--font-mono-stack)`
@@ -66,6 +69,12 @@ Defined in `app/globals.css` as CSS custom properties:
66
69
 
67
70
  ## Component design rules
68
71
 
72
+ A shared component grows by **variant, never by copy**. `Metric` is the example: `size="sm"`
73
+ is the compact form for a grid inside a dialog (smaller value, tighter spacing), and it lives
74
+ on the primitive so the modal and the dashboard cannot drift apart. When a new page needs a
75
+ pattern that does not exist yet, add it here and to `docs/FRONTEND-ARCHITECTURE.md`'s catalog,
76
+ then use it — a one-off copy of markup is the thing this section exists to prevent.
77
+
69
78
  1. **Border radius: 0 everywhere.** `border-radius: 0` or sharp corners. Exceptions: `Pill`, avatar circles, status dots.
70
79
  2. **No shadows.** Flat design. Use `border: 1px solid var(--border)` to define surfaces.
71
80
  3. **Violet is the only accent.** `var(--violet)` for active states, primary actions, headings. `var(--violet-bg)` for hover/active backgrounds.
@@ -1,6 +1,6 @@
1
1
  # Rev4a Frontend Architecture
2
2
 
3
- > **Last updated:** 2026-09-22
3
+ > **Last updated:** 2026-09-26
4
4
 
5
5
  ## Layering
6
6
 
@@ -45,7 +45,7 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
45
45
  | `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). |
46
46
  | `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`. |
47
47
  | `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`. |
48
- | `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 shows the download in progress and its completion. Downloading changes no agent. |
48
+ | `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. |
49
49
  | `VersionBanner` | `app/components/VersionBanner.tsx` | "Update available" banner for Rev4a itself, when `GET /api/update-check?check=1` reports a newer published version (dismissable per version, remembered in `localStorage`). **Update now** starts `POST /api/update-check`; because that update restarts the server, the banner cannot be told the outcome by the response: it records what it asked for in `sessionStorage`, polls `/api/update-check` until the installed version moves (two minutes at most) and reloads, then on the next mount either confirms "Updated to vX" or reports that the update did not complete and points at `update.log` (the update's own output). It never reloads blindly onto the same version. |
50
50
  | `BrowserAccessSection` / `OpenControlUiButton` | `app/agents/BrowserAccessSection.tsx` | Browser access to one agent's Control UI, in its detail panel: requests waiting for approval (Approve / Reject) and approved browsers (Rename / Revoke), refreshed every 5 s while mounted. A successful approve, reject, rename or revoke updates the list at once, since the refresh behind it runs the OpenClaw CLI and takes seconds; a read started before the mutation is discarded. On agents that require approval, "Invite link" fetches `/api/agents/[id]/invite-link` and shows the link in a read-only field with Copy, which uses the Clipboard API in a secure context and the field's selection over plain HTTP, plus a warning when the link uses localhost. `OpenControlUiButton` opens `/api/agents/[id]/open-control-ui` in a new tab inside the click; that route redirects to a one-time link that pairs the browser with no approval, or to the plain token link when none can be issued. Used on the agent cards and in the panel. |
51
51
  | `ChannelManager` / `ChannelSection` | `app/agents/ChannelManager.tsx`, `ChannelSection` inline in `app/agents/PageClient.tsx` | Telegram, in the agent detail panel (`ChannelSection` is the card that opens the modal; the modal title is the agent's display name). Reads `GET /channels`; lists pending pairing requests with **Approve** (`POST /channels/pairing`) and approved senders with **Revoke** after a confirm (`DELETE /channels/pairing?senderId=`). Pending comes from `openclaw pairing list`, approved from OpenClaw's pairing store (`lib/channelManager.ts`). When that store cannot be read the panel shows the reason instead of "No approved senders" (`PairingState.error`), so an empty list is never a guess. Polls pairings every 5 s while open; one keyed busy state per action. |