@flame0510/project-aether 1.2.0 → 1.3.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 +2 -1
- package/app/agents/ModelSection.tsx +313 -0
- package/app/agents/PageClient.tsx +83 -4
- package/app/agents/create/page.tsx +8 -21
- package/app/api/agents/[id]/model/route.ts +113 -0
- package/app/api/agents/[id]/recreate/route.ts +10 -34
- package/app/api/agents/[id]/route.ts +10 -29
- package/app/api/agents/create/route.ts +59 -57
- package/app/api/agents/models-summary/route.ts +163 -0
- package/app/api/assistant/route.ts +36 -15
- package/app/api/gateway/agent/route.ts +23 -6
- package/app/api/gateway/provider/keys.ts +13 -1
- package/app/api/gateway/provider/route.ts +43 -12
- package/app/api/gateway/sync.ts +248 -72
- package/app/api/models/route.ts +28 -34
- package/app/api/provider/auth.ts +65 -0
- package/app/api/provider/upstream.ts +9 -2
- package/app/api/provider/v1/chat/completions/route.ts +22 -16
- package/app/api/provider/v1/models/route.ts +26 -133
- package/app/components/PulseChat.tsx +25 -39
- package/app/components/ui/RemoveButton.tsx +46 -0
- package/app/components/ui/Select.tsx +3 -2
- package/app/components/ui/index.ts +1 -0
- package/app/credentials/PageClient.tsx +2 -2
- package/app/gateway/PageClient.tsx +257 -673
- package/app/globals.css +8 -0
- package/app/lib/models-context.tsx +43 -7
- package/app/wizard/useWizard.ts +6 -1
- package/bin/rev4a.js +73 -9
- package/docs/ARCHITECTURE.md +16 -4
- package/docs/FRONTEND-ARCHITECTURE.md +24 -2
- package/docs/REV4A.md +40 -17
- package/docs/dev/API-REFERENCE.md +170 -79
- package/docs/dev/GATEWAY.md +231 -89
- package/docs/dev/PROVIDERS.md +26 -13
- package/docs/rag/DATA-FRESHNESS.md +57 -28
- package/docs/rag/GLOSSARY.md +16 -14
- package/docs/rag/REV4A-OVERVIEW.md +23 -24
- package/docs/rag/WHAT-I-CAN-ANSWER.md +5 -7
- package/instrumentation.ts +9 -1
- package/lib/agent-readiness.ts +110 -0
- package/lib/channelManager.ts +64 -22
- package/lib/container-file.ts +27 -0
- package/lib/model-catalogue.ts +140 -27
- package/lib/rev4a-paths.ts +0 -21
- package/model-pricing.json +118 -110
- package/models.config.json +27 -12
- package/package.json +1 -1
- package/app/api/gateway/route.ts +0 -191
package/app/globals.css
CHANGED
|
@@ -1296,6 +1296,14 @@ html, body { height: 100%; height: 100dvh; background: var(--bg); color: var(--t
|
|
|
1296
1296
|
background: rgba(var(--red-rgb,239,68,68),0.1);
|
|
1297
1297
|
color: var(--red);
|
|
1298
1298
|
}
|
|
1299
|
+
/* Saved, but not fully applied — e.g. a change stored while an agent sync failed. */
|
|
1300
|
+
.msg-warning {
|
|
1301
|
+
padding: 8px 12px;
|
|
1302
|
+
border-radius: 0;
|
|
1303
|
+
font-size: 12px;
|
|
1304
|
+
background: rgba(245,158,11,0.1);
|
|
1305
|
+
color: var(--yellow);
|
|
1306
|
+
}
|
|
1299
1307
|
|
|
1300
1308
|
/* --- Mobile Bottom Navigation --- */
|
|
1301
1309
|
.mobile-bottom-nav {
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The model list, fetched once and shared by every client that needs it.
|
|
5
|
+
*
|
|
6
|
+
* There used to be two independent copies: this provider, and a private fetch
|
|
7
|
+
* inside PulseChat. Both ran on mount with an empty dependency list, and neither
|
|
8
|
+
* component ever unmounts — so unchecking a model on the Gateway page left the
|
|
9
|
+
* chat panel offering it for the rest of the session, and only a full page reload
|
|
10
|
+
* cleared it.
|
|
11
|
+
*
|
|
12
|
+
* The fix is not to refetch more often. It is to refetch when the answer
|
|
13
|
+
* changes: `refresh()` is called by whoever changes it — the Gateway page after a
|
|
14
|
+
* model toggle, a key save or removal, and a sync; the first-run wizard after
|
|
15
|
+
* saving keys. Everything else just reads.
|
|
16
|
+
*/
|
|
17
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
4
18
|
|
|
5
19
|
interface ProviderEntry {
|
|
6
20
|
provider: string;
|
|
@@ -11,26 +25,48 @@ interface ProviderEntry {
|
|
|
11
25
|
interface ModelsContextValue {
|
|
12
26
|
providers: ProviderEntry[];
|
|
13
27
|
loaded: boolean;
|
|
28
|
+
/** Re-read the catalogue. Call after changing which models are enabled. */
|
|
29
|
+
refresh: () => void;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
|
-
const ModelsContext = createContext<ModelsContextValue>({
|
|
32
|
+
const ModelsContext = createContext<ModelsContextValue>({
|
|
33
|
+
providers: [],
|
|
34
|
+
loaded: false,
|
|
35
|
+
refresh: () => {},
|
|
36
|
+
});
|
|
17
37
|
|
|
18
38
|
export function ModelsProvider({ children }: { children: React.ReactNode }) {
|
|
19
39
|
const [providers, setProviders] = useState<ProviderEntry[]>([]);
|
|
20
40
|
const [loaded, setLoaded] = useState(false);
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
// Refreshes can overlap — two quick toggles — and responses can arrive out of
|
|
43
|
+
// order. Only the most recent request may write.
|
|
44
|
+
const seq = useRef(0);
|
|
45
|
+
|
|
46
|
+
const refresh = useCallback(() => {
|
|
47
|
+
const mine = ++seq.current;
|
|
23
48
|
fetch('/api/models')
|
|
24
|
-
.then((r) =>
|
|
49
|
+
.then((r) => {
|
|
50
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
51
|
+
return r.json();
|
|
52
|
+
})
|
|
25
53
|
.then((data) => {
|
|
26
|
-
if (
|
|
54
|
+
if (mine !== seq.current) return;
|
|
55
|
+
// An empty array is a real answer — every model disabled, or no provider
|
|
56
|
+
// key — so it must replace the list rather than be discarded as a failure.
|
|
57
|
+
if (Array.isArray(data)) setProviders(data);
|
|
27
58
|
setLoaded(true);
|
|
28
59
|
})
|
|
29
|
-
.catch(() =>
|
|
60
|
+
.catch(() => {
|
|
61
|
+
// Keep the previous list: a failed refresh is not an empty catalogue.
|
|
62
|
+
if (mine === seq.current) setLoaded(true);
|
|
63
|
+
});
|
|
30
64
|
}, []);
|
|
31
65
|
|
|
66
|
+
useEffect(() => { refresh(); }, [refresh]);
|
|
67
|
+
|
|
32
68
|
return (
|
|
33
|
-
<ModelsContext.Provider value={{ providers, loaded }}>
|
|
69
|
+
<ModelsContext.Provider value={{ providers, loaded, refresh }}>
|
|
34
70
|
{children}
|
|
35
71
|
</ModelsContext.Provider>
|
|
36
72
|
);
|
package/app/wizard/useWizard.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import { useModels } from '../lib/models-context';
|
|
4
5
|
import {
|
|
5
6
|
isWizardComplete,
|
|
6
7
|
markWizardComplete,
|
|
@@ -45,6 +46,9 @@ export function useWizard() {
|
|
|
45
46
|
// Step tracking is in-memory only; the cookie is the source of truth
|
|
46
47
|
}, []);
|
|
47
48
|
|
|
49
|
+
// Saving a key changes which models are offered; the shared list must follow.
|
|
50
|
+
const { refresh: refreshOfferedModels } = useModels();
|
|
51
|
+
|
|
48
52
|
const saveProviders = useCallback(async () => {
|
|
49
53
|
const entries = Object.entries(providerKeys).filter(([, v]) => v.trim());
|
|
50
54
|
if (entries.length === 0) {
|
|
@@ -60,10 +64,11 @@ export function useWizard() {
|
|
|
60
64
|
body: JSON.stringify({ provider, apiKey: apiKey.trim() }),
|
|
61
65
|
});
|
|
62
66
|
}
|
|
67
|
+
refreshOfferedModels();
|
|
63
68
|
markStep('providers');
|
|
64
69
|
} catch { /* API call failed — do not mark step as complete */ }
|
|
65
70
|
setSaving(false);
|
|
66
|
-
}, [providerKeys, markStep]);
|
|
71
|
+
}, [providerKeys, markStep, refreshOfferedModels]);
|
|
67
72
|
|
|
68
73
|
const goNext = useCallback(async () => {
|
|
69
74
|
if (step === 'welcome') { markStep('welcome'); setStep('providers'); }
|
package/bin/rev4a.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* rev4a serve:
|
|
11
11
|
* 1. Ensures .env exists (auto-generates on first run)
|
|
12
12
|
* 2. Ensures .next build exists (builds if missing)
|
|
13
|
-
* 3. Ensures Docker is installed (installs if missing + root)
|
|
13
|
+
* 3. Ensures Docker is installed (installs if missing + root) and waits for its daemon
|
|
14
14
|
* 4. Ensures agent base image is built (builds if missing)
|
|
15
15
|
* 5. Starts Next.js + daemon + terminal WS as a single process group
|
|
16
16
|
*
|
|
@@ -44,6 +44,17 @@ const DOCKERFILE = join(ROOT, 'agent-templates', 'base-image', 'Dockerfile');
|
|
|
44
44
|
const AGENT_IMAGE = 'openclaw-agent-base:latest';
|
|
45
45
|
const AGENT_IMAGE_REGISTRY = 'ghcr.io/flame0510/rev4a/openclaw-agent-base:latest';
|
|
46
46
|
const DOCKER_NETWORK = 'rev4a-network';
|
|
47
|
+
// How long `serve` waits for the Docker daemon before starting without it. Like
|
|
48
|
+
// REV4A_DATA_DIR it is read from the real process environment (a systemd
|
|
49
|
+
// Environment= line, a shell export), because the .env file is loaded later.
|
|
50
|
+
const DOCKER_WAIT_SECONDS = (() => {
|
|
51
|
+
const raw = (process.env.REV4A_DOCKER_WAIT_SECONDS || '').trim();
|
|
52
|
+
if (!raw) return 90;
|
|
53
|
+
const n = Number(raw);
|
|
54
|
+
// 0 means a single check. Non-numeric or infinite values fall back to the
|
|
55
|
+
// default, and the ceiling keeps a typo from holding start-up for hours.
|
|
56
|
+
return Number.isFinite(n) && n >= 0 ? Math.min(n, 600) : 90;
|
|
57
|
+
})();
|
|
47
58
|
|
|
48
59
|
// Fallback used only when package.json cannot be read — the published npm
|
|
49
60
|
// package name is the single source of truth, so never hardcode it elsewhere.
|
|
@@ -97,25 +108,70 @@ function whichDistro() {
|
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
// ── Docker auto-install
|
|
111
|
+
// ── Docker auto-install and daemon wait ─────────────────────────────────────
|
|
101
112
|
|
|
113
|
+
/** Block this thread for `ms` without spawning a process (`sleep` is not on every OS). */
|
|
114
|
+
function sleepSync(ms) {
|
|
115
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Wait until the Docker daemon answers, up to DOCKER_WAIT_SECONDS.
|
|
120
|
+
*
|
|
121
|
+
* Finding the `docker` command only proves the CLI is installed. At boot the
|
|
122
|
+
* daemon can still be starting — on the production host systemd started this
|
|
123
|
+
* service a second before Docker. Start-up still worked, by accident: the first
|
|
124
|
+
* `docker` call blocked on docker.socket until the daemon was up. With a daemon
|
|
125
|
+
* slower than a later call's 10 s timeout, the image check would have concluded
|
|
126
|
+
* the image was missing and tried to pull and rebuild it. Asking the daemon
|
|
127
|
+
* directly, with a short per-attempt timeout, makes the wait explicit, bounded
|
|
128
|
+
* and logged, whatever kind of Docker install this is.
|
|
129
|
+
*/
|
|
130
|
+
function waitForDockerDaemon() {
|
|
131
|
+
const deadline = Date.now() + DOCKER_WAIT_SECONDS * 1000;
|
|
132
|
+
let lastError = '';
|
|
133
|
+
let announced = false;
|
|
134
|
+
for (;;) {
|
|
135
|
+
try {
|
|
136
|
+
const version = execSync('docker info --format "{{.ServerVersion}}"', { stdio: 'pipe', timeout: 10_000 })
|
|
137
|
+
.toString().trim();
|
|
138
|
+
if (version) {
|
|
139
|
+
log('DOCKER', `Docker daemon ready (server ${version})`);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
lastError = ((err.stderr && err.stderr.toString()) || err.message || '').trim().split('\n')[0];
|
|
144
|
+
}
|
|
145
|
+
if (Date.now() >= deadline) break;
|
|
146
|
+
if (!announced) {
|
|
147
|
+
log('DOCKER', `Waiting for the Docker daemon (up to ${DOCKER_WAIT_SECONDS}s)…`);
|
|
148
|
+
announced = true;
|
|
149
|
+
}
|
|
150
|
+
sleepSync(2000);
|
|
151
|
+
}
|
|
152
|
+
log('DOCKER', `⚠ Docker daemon not reachable after ${DOCKER_WAIT_SECONDS}s: ${lastError || 'no answer'}`);
|
|
153
|
+
log('DOCKER', ' Starting without it. Agents are unavailable and the startup sync reaches none; run Sync All Agents once Docker is up.');
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** True when the Docker daemon is reachable and the Docker-dependent steps can run. */
|
|
102
158
|
function ensureDocker() {
|
|
103
159
|
if (hasCmd('docker')) {
|
|
104
|
-
log('DOCKER', 'Docker
|
|
105
|
-
return;
|
|
160
|
+
log('DOCKER', 'Docker CLI found');
|
|
161
|
+
return waitForDockerDaemon();
|
|
106
162
|
}
|
|
107
163
|
|
|
108
164
|
if (!isLinux()) {
|
|
109
165
|
log('DOCKER', '⚠ Docker not found. Rev4a needs Docker to create agent containers.');
|
|
110
166
|
log('DOCKER', ' Install Docker: https://docs.docker.com/get-docker/');
|
|
111
167
|
log('DOCKER', ' Then run: rev4a serve');
|
|
112
|
-
return;
|
|
168
|
+
return false;
|
|
113
169
|
}
|
|
114
170
|
|
|
115
171
|
if (!isRoot()) {
|
|
116
172
|
log('DOCKER', '⚠ Docker not found. Run Rev4a as root to auto-install:');
|
|
117
173
|
log('DOCKER', ' sudo rev4a serve');
|
|
118
|
-
return;
|
|
174
|
+
return false;
|
|
119
175
|
}
|
|
120
176
|
|
|
121
177
|
const distro = whichDistro();
|
|
@@ -134,10 +190,12 @@ function ensureDocker() {
|
|
|
134
190
|
);
|
|
135
191
|
|
|
136
192
|
log('DOCKER', 'Docker installed successfully');
|
|
193
|
+
return waitForDockerDaemon();
|
|
137
194
|
} catch (err) {
|
|
138
195
|
log('DOCKER', '⚠ Docker auto-install failed. Install manually:');
|
|
139
196
|
log('DOCKER', ' https://docs.docker.com/engine/install/');
|
|
140
197
|
log('DOCKER', ' Then run: rev4a serve');
|
|
198
|
+
return false;
|
|
141
199
|
}
|
|
142
200
|
}
|
|
143
201
|
|
|
@@ -414,9 +472,15 @@ function checkVersion() {
|
|
|
414
472
|
function preflight() {
|
|
415
473
|
log('PREFLIGHT', 'Running pre-flight checks…');
|
|
416
474
|
ensureEnv();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
475
|
+
// Network and image need the daemon. Without it the image check fails and
|
|
476
|
+
// would start a pull and then a local build — up to fifteen minutes, before the
|
|
477
|
+
// dashboard is even up — for an image that is most likely already there.
|
|
478
|
+
if (ensureDocker()) {
|
|
479
|
+
ensureNetwork();
|
|
480
|
+
ensureAgentImage();
|
|
481
|
+
} else {
|
|
482
|
+
log('PREFLIGHT', 'Skipping network and agent image checks: Docker daemon not reachable.');
|
|
483
|
+
}
|
|
420
484
|
ensureRev4aRules();
|
|
421
485
|
|
|
422
486
|
if (!existsSync(NEXT_DIR)) {
|
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-09-14
|
|
5
5
|
> **Goal:** Transform Rev4a from a monitoring dashboard into a central orchestrator for a distributed multi-container agency.
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -119,6 +119,17 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
119
119
|
- Injected into every session via the `bootstrap-extra-files` hook (glob `.rev4a/*.md`)
|
|
120
120
|
- Immutable by agents — enforced by the `:ro` mount
|
|
121
121
|
- See `lib/agent-setup.ts` for the centralized volume + config guarantee logic
|
|
122
|
+
- **Waiting for a new container** is `waitForGatewayReady()` in
|
|
123
|
+
`lib/agent-readiness.ts`, used by create, recreate and the agent route. On
|
|
124
|
+
OpenClaw 9.x it waits for `/startupz` to report `started` (503 while starting).
|
|
125
|
+
The 2026.7.1-2 image has no `/startupz` — its gateway serves the web UI with 200
|
|
126
|
+
for unknown paths — so there it falls back to `/health`, which only shows the
|
|
127
|
+
server is listening. Do not reimplement the wait in a route.
|
|
128
|
+
- **The model catalogue** is read only through `lib/model-catalogue.ts`:
|
|
129
|
+
`loadModelsConfig()`, `loadOfferedModels()` (enabled after overrides, provider has
|
|
130
|
+
a key), `isModelOffered()` (enforced by the proxy and the assistant) and
|
|
131
|
+
`catalogueStatus()`. A failed read of `models.config.json` falls back to the last
|
|
132
|
+
good copy and never deletes overrides.
|
|
122
133
|
|
|
123
134
|
**Running commands inside containers** (`lib/docker-exec.ts`):
|
|
124
135
|
|
|
@@ -136,7 +147,8 @@ the rules for anything you touch, not as a description of the whole tree:
|
|
|
136
147
|
computed for other containers are discarded. Fanning out over a fleet requires
|
|
137
148
|
the `…NoFail` variants, or an explicit per-item try/catch, so one unreachable
|
|
138
149
|
container cannot blank an entire page. Callers: `app/api/skills/route.js`,
|
|
139
|
-
`app/api/
|
|
150
|
+
`app/api/agents/models-summary/route.ts`,
|
|
151
|
+
`app/api/agents/channels-summary/route.ts`,
|
|
140
152
|
`lib/openclaw-cron.ts`, `app/api/credentials/detect/route.ts`,
|
|
141
153
|
`lib/credentials/delivery.ts`.
|
|
142
154
|
- **Never interpolate a secret into a command string.** Pass it through the `env`
|
|
@@ -207,7 +219,7 @@ Agent Container Rev4a Gateway Provider API
|
|
|
207
219
|
│ │ │
|
|
208
220
|
│ Authorization: Bearer <gateway-token> │
|
|
209
221
|
│ POST /api/provider/v1/chat/completions │
|
|
210
|
-
│ model: rev4a/deepseek-
|
|
222
|
+
│ model: rev4a/deepseek-flash │
|
|
211
223
|
│────────────────────────>│ │
|
|
212
224
|
│ │ POST /v1/chat/completions│
|
|
213
225
|
│ │ Authorization: Bearer <real-key>
|
|
@@ -526,7 +538,7 @@ ALTER TABLE sessions ADD COLUMN ended_at INTEGER;
|
|
|
526
538
|
### Phase 2 — Provider Gateway
|
|
527
539
|
- [x] Implement proxy API on Rev4a (`/api/provider/v1/`)
|
|
528
540
|
- [x] Gateway Page (`/gateway`) for provider key management
|
|
529
|
-
- [x] Agent model configuration
|
|
541
|
+
- [x] Agent model configuration in the agent detail panel
|
|
530
542
|
- [x] Provider sync to agent containers (`PUT /api/gateway/provider`)
|
|
531
543
|
- [x] Auth via rev4a token in `data/provider-keys.json`
|
|
532
544
|
- [ ] Rate limiting per agent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a Frontend Architecture
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
3
|
+
> **Last updated:** 2026-09-14
|
|
4
4
|
|
|
5
5
|
## Layering
|
|
6
6
|
|
|
@@ -24,7 +24,8 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
24
24
|
|-----------|------|---------|
|
|
25
25
|
| `Button` | `Button.tsx` | Action button, 5 variants (`primary`, `secondary`, `danger`, `ghost`, `success`), 2 sizes (`sm`, `md`), loading spinner. |
|
|
26
26
|
| `Input` | `Input.tsx` | Text input with label, error state, placeholder. |
|
|
27
|
-
| `Select` | `Select.tsx` | Native select with typed options, label, error state. |
|
|
27
|
+
| `Select` | `Select.tsx` | Native select with typed options, label, error state. An option can be `disabled`, to display a current value that may not be chosen again. |
|
|
28
|
+
| `RemoveButton` | `RemoveButton.tsx` | The `×` that removes an item from a list — one `danger` style everywhere. Whether the removal is immediate or pending goes in `title`, not in the colour. Not for dismissing dialogs — see rule 8. |
|
|
28
29
|
| `Modal` | `Modal.tsx` | Overlay modal, Escape-to-close, maxWidth prop, `type="button"` on close. |
|
|
29
30
|
| `Toast` | `Toast.tsx` | Lightweight toast notification with auto-dismiss (4s), `success` / `error` variants. |
|
|
30
31
|
| `LoadingSpinner` | `LoadingSpinner.tsx` | Inline or fullscreen spinner. |
|
|
@@ -41,6 +42,8 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
41
42
|
| `Page` / `PageHeader` | `Page.tsx` | Full-page layout shell. |
|
|
42
43
|
| `Icons` | `Icons.tsx` | SVG icons (`EyeIcon`, `EyeOffIcon`), 16/20px shared. |
|
|
43
44
|
| `PasswordInput` | `PasswordInput.tsx` | Password input with inline show/hide toggle (`<button type="button">` with `aria-label`). |
|
|
45
|
+
| `ModelSection` | `app/agents/ModelSection.tsx` | Primary model and fallbacks for one agent, in its detail panel. Explicit save, no restart. The model is a property of the agent, not of the gateway. Tags models the catalogue marks `deprecated`. |
|
|
46
|
+
| `ModelsProvider` / `useModels` | `app/lib/models-context.tsx` | The client's single model list, from `/api/models`. Whatever changes what is offered calls `refresh()`: the Gateway page after a toggle, a key save or removal, or a sync, and the first-run wizard after saving keys. Everything else only reads, PulseChat included. |
|
|
44
47
|
| `WizardPageClient` + step components | `app/wizard/PageClient.tsx` | Multi-step first-run wizard plus its frame shell, with mobile-first CSS. |
|
|
45
48
|
| `useWizard` | `app/wizard/useWizard.ts` | Shared hook: wizard state, step transitions, provider save, restart. Receives server-side initial state to avoid a loading flash. |
|
|
46
49
|
| Wizard icons | `app/wizard/icons.tsx` | Shared SVG icons (flyweight pattern): `ArrowRightIcon`, `CheckIcon`, `CheckCircleIcon`, `DockerIcon`, `GatewayIcon`, `AgentIcon`, `TemplateIcon`, `LinkIcon`, `ConfigIcon`, `InformationIcon`. |
|
|
@@ -62,6 +65,25 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
62
65
|
6c. **An optimistic update moves every field the render derives from, together.** A row that decides its state by reading two fields against each other flips to a third, wrong state if the update touches only one of them — and holds it until the slow refresh lands. Update the whole set the derivation reads, or none of it.
|
|
63
66
|
7. **New UI component** — add it to `app/components/ui/`, export from `index.ts`, document it here. If it's specific to one page, keep it page-local unless another page needs it.
|
|
64
67
|
|
|
68
|
+
8. **`×` means remove, `✕` means dismiss.** They look alike and are not the same
|
|
69
|
+
action. Removing an item from a list uses `RemoveButton`; dismissing a dialog
|
|
70
|
+
is the `Modal` component's own close control. Do not build a third variant of
|
|
71
|
+
either, and do not reuse one for the other.
|
|
72
|
+
|
|
73
|
+
*Outstanding:* four dialogs predate the shared `Modal` and roll their own close
|
|
74
|
+
control — the edit dialog and the `openclaw.json` dialog in
|
|
75
|
+
`app/agents/PageClient.tsx` (both a `ghost` Button with `✕`), the drawer in
|
|
76
|
+
`app/components/SessionDrawer.tsx` (a bare `<button>`), and
|
|
77
|
+
`app/components/ModelPickerModal.tsx` (`model-picker__close`, which compounds
|
|
78
|
+
the problem by using `×`, the *remove* glyph, to dismiss, and omits
|
|
79
|
+
`type="button"`). The fix is to move them onto `Modal`, not to extract a
|
|
80
|
+
`CloseButton`: they also reimplement Escape-to-close and overlay behaviour.
|
|
81
|
+
`ModelPickerModal` has no importers at all, so deleting it is the cheaper fix
|
|
82
|
+
there.
|
|
83
|
+
|
|
84
|
+
Separately, `app/agents/PageClient.tsx` has a raw `×` delete-backup button with
|
|
85
|
+
no accessible name that should be `RemoveButton`.
|
|
86
|
+
|
|
65
87
|
## GoF pattern mapping
|
|
66
88
|
|
|
67
89
|
Already used:
|
package/docs/REV4A.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a — VPS Dashboard
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-09-
|
|
3
|
+
> **Last updated:** 2026-09-14
|
|
4
4
|
|
|
5
5
|
A Next.js 16 dashboard for monitoring and managing the OpenClaw ecosystem.
|
|
6
6
|
|
|
@@ -120,7 +120,7 @@ When environment variables are saved via the Config page (Save & Restart):
|
|
|
120
120
|
| `/plugins` | Plugin manager |
|
|
121
121
|
| `/skills` | Skill registry — browse/edit all skills (shared, per-agent workspace, bundled). Promote agent skills to shared with one click. |
|
|
122
122
|
| `/tools` | Tool configuration |
|
|
123
|
-
| `/gateway` | LLM provider
|
|
123
|
+
| `/gateway` | LLM provider keys, model catalogue and sync — see [GATEWAY.md](dev/GATEWAY.md) |
|
|
124
124
|
| `/config` | Environment management and server restart |
|
|
125
125
|
| `/login` | Authentication page |
|
|
126
126
|
|
|
@@ -188,6 +188,18 @@ sudo systemctl status rev4a.service
|
|
|
188
188
|
sudo journalctl -u rev4a -f
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
### Start-up and Docker
|
|
192
|
+
|
|
193
|
+
`rev4a serve` waits for the Docker daemon before its Docker-dependent steps, up to
|
|
194
|
+
`REV4A_DOCKER_WAIT_SECONDS` (default 90, capped at 600; `0` checks once without
|
|
195
|
+
waiting; a negative or non-numeric value means 90). It asks the daemon itself with
|
|
196
|
+
`docker info`, so the wait works the same on any Docker install. The variable is
|
|
197
|
+
read from the process environment, not from the `.env` file.
|
|
198
|
+
|
|
199
|
+
If the daemon never answers, `serve` starts anyway, skips the network and image
|
|
200
|
+
checks, and says so in the log. The startup sync then reaches no agent, and logs
|
|
201
|
+
that too; run Sync All Agents once Docker is up.
|
|
202
|
+
|
|
191
203
|
### Systemd environment override
|
|
192
204
|
|
|
193
205
|
File: `/etc/systemd/system/rev4a-next.service` (EnvironmentFile)
|
|
@@ -311,15 +323,16 @@ Available templates:
|
|
|
311
323
|
**Post-creation pipeline:**
|
|
312
324
|
1. The container boots with the **openclaw-agent-base:latest** image — see [Agent Templates](#agent-templates) below.
|
|
313
325
|
2. OpenClaw gateway starts automatically with `--allow-unconfigured`, generating its own default config.
|
|
314
|
-
3. The route waits for the gateway to
|
|
326
|
+
3. The route waits for the gateway to finish starting with `waitForGatewayReady()`, up
|
|
327
|
+
to 60 s. On OpenClaw 9.x it waits for `/startupz` to report `started`. The
|
|
328
|
+
2026.7.1-2 image has no `/startupz`, so there it waits for `/health`, which only
|
|
329
|
+
shows the server is listening.
|
|
315
330
|
4. Once ready, the route writes:
|
|
316
331
|
- `gateway.controlUi.allowedOrigins` — Rev4a dashboard URL + Traefik hostname (required for browser Control UI access)
|
|
317
332
|
- `agents.defaults.model.primary` + fallbacks — the primary model selected in the wizard
|
|
318
|
-
5.
|
|
333
|
+
5. The route builds `models.providers.rev4a` with `buildRev4aProviderConfig()` and writes it with `openclaw config patch --stdin`.
|
|
319
334
|
6. The agent's control UI is immediately accessible at `https://<name>.<your-domain>.com#token=<gateway-token>`.
|
|
320
335
|
|
|
321
|
-
**Key difference from the old pipeline:** the entrypoint no longer generates `openclaw.json`. The bootstrap config (including `controlUi.allowedOrigins`) is written by the create route after the gateway has already started. This eliminates the race condition where the entrypoint would overwrite synced provider config on boot.
|
|
322
|
-
|
|
323
336
|
---
|
|
324
337
|
|
|
325
338
|
## Agent persistence & lifecycle
|
|
@@ -385,23 +398,33 @@ repo. The copy is skipped when the workspace is already populated (see
|
|
|
385
398
|
|
|
386
399
|
## Gateway Page (`/gateway`)
|
|
387
400
|
|
|
388
|
-
|
|
401
|
+
One job: provider configuration. API keys, which catalogue models this deployment
|
|
402
|
+
offers, and pushing that catalogue to every agent.
|
|
403
|
+
|
|
404
|
+
- Reads the model catalogue from `models.config.json`, merged with user toggles in
|
|
405
|
+
`<data dir>/model-overrides.json`
|
|
406
|
+
- Scans providers with keys in `<data dir>/provider-keys.json`
|
|
407
|
+
- `PUT /api/gateway/provider` runs `syncAllAgents()`, which writes
|
|
408
|
+
`models.providers.rev4a` into every agent container
|
|
409
|
+
- **Does NOT touch** `agents.defaults.model` or `agents.list[].model` — those are
|
|
410
|
+
per-agent settings
|
|
411
|
+
|
|
412
|
+
### Per-agent model, elsewhere
|
|
389
413
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
414
|
+
Choosing which model one agent runs is not on this page. It lives in the Model
|
|
415
|
+
section of that agent's detail panel on the Agents page
|
|
416
|
+
(`app/agents/ModelSection.tsx`), which calls `PUT /api/gateway/agent` to write the
|
|
417
|
+
model ref into that container's `openclaw.json`. No gateway restart: OpenClaw
|
|
418
|
+
watches the file and hot-applies the change.
|
|
395
419
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
- Does NOT restart the gateway — writes are live via file write
|
|
420
|
+
The model is a property of the agent, not of the gateway. See
|
|
421
|
+
[GATEWAY.md](dev/GATEWAY.md) for the write semantics: omitting `fallbacks` keeps the
|
|
422
|
+
agent's current list, `[]` clears it.
|
|
400
423
|
|
|
401
424
|
### Model Catalogue (`models.config.json`)
|
|
402
425
|
File-based, tracked in git. Each entry:
|
|
403
426
|
```json
|
|
404
|
-
{ "id": "deepseek/deepseek-
|
|
427
|
+
{ "id": "deepseek/deepseek-flash", "name": "DeepSeek Flash", "provider": "deepseek", "enabled": true }
|
|
405
428
|
```
|
|
406
429
|
Models with `enabled: false` are ignored.
|
|
407
430
|
Only models whose provider has a key in `data/provider-keys.json` are synced.
|