@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.
- package/LICENSE +21 -0
- package/README.md +5 -2
- package/agent-templates/atlas/HEARTBEAT.md +1 -1
- package/app/agents/ImageDownloadBanner.tsx +171 -37
- package/app/api/agents/download-image/route.ts +29 -5
- package/app/api/agents/image-status/route.ts +17 -2
- package/app/api/assistant/route.ts +13 -4
- package/app/api/auth/login/route.ts +2 -2
- package/app/api/setup/agent-image/route.ts +6 -4
- package/app/api/system-health/route.ts +3 -3
- package/app/components/DashboardToolbar.tsx +2 -2
- package/app/components/LineageGraphPage.tsx +4 -4
- package/app/components/SessionDrawer.tsx +8 -8
- package/app/components/SystemCockpit.tsx +1 -1
- package/app/globals.css +1 -1
- package/app/setup/PageClient.tsx +1 -1
- package/bin/postinstall.js +5 -1
- package/daemon.js +13 -34
- package/docs/ARCHITECTURE.md +44 -26
- package/docs/CONTAINER-TERMINAL.md +17 -8
- package/docs/DESIGN-SYSTEM.md +21 -12
- package/docs/FRONTEND-ARCHITECTURE.md +2 -2
- package/docs/REV4A.md +35 -85
- package/docs/dev/API-REFERENCE.md +73 -19
- package/docs/dev/DATABASE.md +18 -7
- package/docs/dev/GATEWAY.md +23 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +6 -6
- package/docs/rag/DATA-FRESHNESS.md +20 -9
- package/docs/rag/GLOSSARY.md +2 -2
- package/docs/rag/REV4A-OVERVIEW.md +10 -6
- package/docs/rag/WHAT-I-CAN-ANSWER.md +3 -3
- package/lib/agent-images.ts +43 -14
- package/lib/buildAgentImage.ts +142 -6
- package/lib/patterns/sessionPresentation.ts +4 -2
- package/lib/rev4a-auth.d.ts +1 -0
- package/lib/rev4a-auth.js +18 -2
- package/models.config.json +937 -45
- package/next.config.mjs +9 -1
- package/package.json +5 -3
- package/scripts/backup.sh +48 -54
- package/scripts/check-language.mjs +21 -3
- package/scripts/model-info-verify.mjs +194 -0
- 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="
|
|
183
|
+
<Row label="STATUS">
|
|
184
184
|
<span style={{ color: statusColor(session.status) }}>●</span>{' '}
|
|
185
185
|
{session.status ?? 'idle'}
|
|
186
186
|
</Row>
|
|
187
|
-
<Row label="
|
|
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="
|
|
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="
|
|
196
|
-
<Row label="
|
|
197
|
-
<Row label="
|
|
198
|
-
<Row label="
|
|
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="
|
|
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 }}>
|
|
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
|
-
/*
|
|
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 {
|
package/app/setup/PageClient.tsx
CHANGED
|
@@ -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
|
|
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)' }}>
|
package/bin/postinstall.js
CHANGED
|
@@ -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
|
|
306
|
-
|
|
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
|
|
354
|
+
message: `CPU high: ${recent[0].cpu_percent}% for 2 consecutive samples`,
|
|
361
355
|
}),
|
|
362
356
|
});
|
|
363
|
-
log(`[ANOMALY] CPU
|
|
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
|
|
374
|
+
message: `RAM high: ${ram0pct}% for 2 consecutive samples`,
|
|
381
375
|
}),
|
|
382
376
|
});
|
|
383
|
-
log(`[ANOMALY] RAM
|
|
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
|
|
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
|
|
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
|
|
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) {
|
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-09-
|
|
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
|
|
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`
|
|
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` / `
|
|
566
|
-
4. Collect system metrics (CPU, RAM, disk) every poll
|
|
567
|
-
5. Detect anomalies (CPU >
|
|
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
|
|
582
|
+
### Poll Interval
|
|
571
583
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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
|
|
620
|
-
- **RAM**:
|
|
621
|
-
- **Disk**: `
|
|
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
|
|
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.
|
|
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 | >
|
|
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/
|
|
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
|
-
| `
|
|
732
|
-
| `
|
|
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
|
-
|
|
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 [
|
|
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 (
|
|
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
|
-
| `
|
|
34
|
-
| `
|
|
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
|
-
|
|
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
|
-
###
|
|
41
|
+
### Routing
|
|
41
42
|
|
|
42
43
|
```
|
|
43
44
|
/containers/terminal/{id} → Next.js (page serving TerminalClient)
|
|
44
|
-
/api/terminal-ws?id={containerId} →
|
|
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
|
|
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
|
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-
|
|
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:
|
|
48
|
-
--bg2:
|
|
49
|
-
--border:
|
|
50
|
-
--text:
|
|
51
|
-
--text-dim: #888
|
|
52
|
-
--violet:
|
|
53
|
-
--violet-bg:
|
|
54
|
-
--green:
|
|
55
|
-
--red:
|
|
56
|
-
--yellow:
|
|
57
|
-
--blue:
|
|
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-
|
|
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
|
|
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. |
|