Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,4 @@ This avoids repeatedly reading large files and provides instant context about th
5. **Gateway spawn inherits stdio** → logs appear in wrapper output (src/server.js:134)
6. **WebSocket auth requires proxy event handlers** → Direct `req.headers` modification doesn't work for WebSocket upgrades with http-proxy; must use `proxyReqWs` event (src/server.js:741) to reliably inject Authorization header
7. **Control UI requires allowInsecureAuth to bypass pairing** → Set `gateway.controlUi.allowInsecureAuth=true` during onboarding to prevent "disconnected (1008): pairing required" errors (GitHub issue #2284). Wrapper already handles bearer token auth, so device pairing is unnecessary.
8. **Config Editor (`/setup/config`) writes `openclaw.json` directly** → unlike onboarding (which uses `openclaw config set --json`), the editor parses+validates JSON, makes a `.bak-<timestamp>` copy, then `fs.writeFileSync`s the new contents and calls `restartGateway()`. This is intentional: it's an operator-targeted advanced tool, so direct writes are simpler and let users fix arbitrarily-broken config without depending on the CLI.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ RUN apt-get update \
python3 \
build-essential \
zip \
unzip \
&& rm -rf /var/lib/apt/lists/*

RUN npm install -g openclaw@2026.5.7
Expand Down
146 changes: 146 additions & 0 deletions src/public/config.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>🦞 OpenClaw — Config Editor</title>
<link rel="stylesheet" href="/styles.css">
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('configApp', () => ({
path: '',
exists: false,
content: '',
loading: false,
saving: false,
message: '',
error: '',

async init() {
await this.load();
},

async httpJson(url, opts = {}) {
opts.credentials = 'same-origin';
const res = await fetch(url, opts);
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { /* leave null */ }
if (!res.ok) {
const err = (body && body.error) || text || res.statusText;
throw new Error('HTTP ' + res.status + ': ' + err);
}
return body;
},

async load() {
this.loading = true;
this.error = '';
this.message = '';
try {
const j = await this.httpJson('/setup/api/config/raw');
this.path = j.path || '';
this.exists = !!j.exists;
this.content = j.content || '';
} catch (e) {
this.error = String(e.message || e);
} finally {
this.loading = false;
}
},

async save() {
this.error = '';
this.message = '';
try {
JSON.parse(this.content);
} catch (e) {
this.error = 'Invalid JSON: ' + (e.message || String(e));
return;
}
if (!confirm('Save config and restart the gateway? A timestamped .bak backup will be created next to openclaw.json.')) {
return;
}
this.saving = true;
try {
const j = await this.httpJson('/setup/api/config/raw', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ content: this.content })
});
let msg = 'Saved to ' + (j.path || this.path) + '.';
if (j.backupPath) msg += '\nBackup: ' + j.backupPath;
msg += j.restarted ? '\nGateway restarted.' : '\nGateway not running — no restart needed.';
this.message = msg;
this.exists = true;
} catch (e) {
this.error = String(e.message || e);
} finally {
this.saving = false;
}
},
}));
});
</script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
</head>
<body x-data="configApp()">
<div class="container">
<div class="logo-wrap">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.png">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.png" alt="OpenClaw" class="logo-img">
</picture>
</div>

<div style="margin-bottom:1.25rem">
<a href="/setup" class="inline-link" style="font-size:0.875rem">← Back to Setup</a>
</div>

<div class="card">
<div class="card-header">
<div>
<h2 class="card-title">Config Editor</h2>
<p class="card-subtitle">Direct edit of <code>openclaw.json</code>. Saving creates a timestamped <code>.bak</code> backup and restarts the gateway.</p>
</div>
</div>

<p class="form-hint" style="margin-bottom:0.75rem">
<strong>File:</strong>
<span x-text="path || '(loading…)'"></span>
<span x-show="!loading && !exists" style="color:var(--text-muted)">(does not exist yet)</span>
</p>

<textarea
class="config-textarea"
x-model="content"
:disabled="loading || saving"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
></textarea>

<div class="btn-row" style="margin-top:1rem;display:flex;gap:0.75rem;flex-wrap:wrap">
<button @click="load()" :disabled="loading || saving" class="btn btn-secondary">
<span x-show="!loading">Reload</span>
<span x-show="loading" style="display:inline-flex;align-items:center;gap:0.5rem">
<span class="spinner spinner-inline" aria-hidden="true"></span>
Loading
</span>
</button>
<button @click="save()" :disabled="loading || saving" class="btn btn-primary">
<span x-show="!saving">Save &amp; Restart Gateway</span>
<span x-show="saving" style="display:inline-flex;align-items:center;gap:0.5rem">
<span class="spinner spinner-inline" aria-hidden="true"></span>
Saving
</span>
</button>
</div>

<div x-show="error" x-cloak class="setup-error-panel" style="margin-top:1rem" x-text="error"></div>
<div x-show="message" x-cloak class="log-output" style="margin-top:1rem" x-text="message"></div>
</div>
</div>
</body>
</html>
3 changes: 0 additions & 3 deletions src/public/loading.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenClaw</title>
<link rel="stylesheet" href="/styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
.fade-out { opacity: 0; transition: opacity 0.4s ease; }
.fade-in { opacity: 1; transition: opacity 0.4s ease; }
Expand Down
3 changes: 0 additions & 3 deletions src/public/logs.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>🦞 OpenClaw — Server Logs</title>
<link rel="stylesheet" href="/styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('logsApp', () => ({
Expand Down
128 changes: 125 additions & 3 deletions src/public/setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>🦞 OpenClaw — Personal AI Assistant</title>
<link rel="stylesheet" href="/styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('setupApp', () => ({
Expand Down Expand Up @@ -47,6 +44,10 @@
tuiEnabled: false,
exporting: false,
showExportInfo: false,
importing: false,
showImportModal: false,
pendingImportFile: null,
importArchivePassword: '',
showDevicesModal: false,
devicesLoading: false,
devices: [],
Expand Down Expand Up @@ -353,6 +354,63 @@
}
},

onImportFileSelected(event) {
const f = event.target.files && event.target.files[0];
event.target.value = '';
if (!f) return;
this.pendingImportFile = f;
this.importArchivePassword = '';
this.showImportModal = true;
},

cancelImport() {
if (this.importing) return;
this.showImportModal = false;
this.pendingImportFile = null;
this.importArchivePassword = '';
},

async confirmImport() {
const f = this.pendingImportFile;
if (!f) return;

const headers = { 'content-type': 'application/zip' };
const pw = (this.importArchivePassword || '').trim();
if (pw) {
headers['x-archive-password'] = btoa(unescape(encodeURIComponent(pw)));
}

this.importing = true;
this.log = 'Uploading ' + f.name + ' (' + f.size + ' bytes)...\n';

try {
const buf = await f.arrayBuffer();
const res = await fetch('/setup/api/import', {
method: 'POST',
credentials: 'same-origin',
headers,
body: buf,
});
const text = await res.text();
let j;
try { j = JSON.parse(text); } catch (_e) { j = { ok: res.ok, output: text }; }
if (j.output) this.log += j.output + '\n';
if (res.ok && j.ok) {
this.log += '✓ Import complete. Gateway restarted.\n';
this.showImportModal = false;
this.pendingImportFile = null;
this.importArchivePassword = '';
await this.refreshStatus();
} else {
this.log += '✗ Import failed: ' + (j.error || text) + '\n';
}
} catch (e) {
this.log += '✗ Import error: ' + String(e) + '\n';
} finally {
this.importing = false;
}
},

async openDevicesModal() {
this.showDevicesModal = true;
await this.loadDevices();
Expand Down Expand Up @@ -543,6 +601,24 @@ <h3 class="card-title" style="font-size:1rem">Maintenance</h3>
</button>
</div>

<div class="action-card action-violet">
<div class="action-card-header">
<div class="action-icon">
<svg class="icon-md" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
</div>
<span class="action-title">Import Data</span>
</div>
<p class="action-desc">Restore from a password-protected ZIP. Overwrites config and workspace, then restarts the gateway.</p>
<input x-ref="importInput" @change="onImportFileSelected($event)" type="file" accept=".zip,application/zip" style="display:none" />
<button @click="$refs.importInput.click()" :disabled="loading || importing || exporting" class="action-btn">
<span x-show="!importing">Import Data</span>
<span x-show="importing" style="display:inline-flex;align-items:center;justify-content:center;gap:0.5rem">
<svg class="spinner-inline" fill="none" viewBox="0 0 24 24"><circle style="opacity:0.25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path style="opacity:0.75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
Importing...
</span>
</button>
</div>

<div class="action-card action-blue">
<div class="action-card-header">
<div class="action-icon">
Expand All @@ -555,6 +631,19 @@ <h3 class="card-title" style="font-size:1rem">Maintenance</h3>
View Logs
</a>
</div>

<div class="action-card action-violet">
<div class="action-card-header">
<div class="action-icon">
<svg class="icon-md" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</div>
<span class="action-title">Edit Config</span>
</div>
<p class="action-desc">Directly edit <code>openclaw.json</code>. Creates a backup and restarts the gateway on save.</p>
<a href="/setup/config" class="action-btn" style="display:block;text-align:center;text-decoration:none">
Edit Config
</a>
</div>
</div>

<div x-show="log" class="log-output" x-text="log"></div>
Expand Down Expand Up @@ -871,6 +960,39 @@ <h3 class="modal-title mb-4">Unzip Instructions</h3>
</div>
</div>

<div x-show="showImportModal" x-cloak class="modal-overlay" @click.self="cancelImport()">
<div class="modal-card modal-md">
<h3 class="modal-title mb-4">Import Backup</h3>
<p class="text-sm text-muted mb-4">
Restoring <strong x-text="pendingImportFile?.name"></strong>
(<span x-text="pendingImportFile ? Math.round(pendingImportFile.size / 1024) + ' KB' : ''"></span>).
This overwrites your config and workspace under <code>/data</code>, then restarts the gateway.
</p>
<div class="form-group">
<label class="label">Archive password</label>
<input
x-model="importArchivePassword"
type="password"
autocomplete="off"
placeholder="Leave blank to use this instance's SETUP_PASSWORD"
:disabled="importing"
class="input"
/>
<p class="text-sm text-muted" style="margin-top:0.25rem">Use the SETUP_PASSWORD of the instance the backup was exported from. Leave blank if it was this instance.</p>
</div>
<div class="btn-pair">
<button @click="confirmImport()" :disabled="importing" class="btn btn-primary">
<span x-show="!importing">Import &amp; Restart</span>
<span x-show="importing" style="display:inline-flex;align-items:center;gap:0.5rem">
<span class="spinner spinner-inline" aria-hidden="true"></span>
Importing
</span>
</button>
<button @click="cancelImport()" :disabled="importing" class="btn btn-close-full" style="margin-top:0">Cancel</button>
</div>
</div>
</div>

<div x-show="showPairingModal" x-cloak class="modal-overlay" @click.self="if (!pairingSubmitting) showPairingModal = false">
<div class="modal-card modal-sm">
<h3 class="modal-title mb-4">Approve Channel Access</h3>
Expand Down
19 changes: 18 additions & 1 deletion src/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
}

:root {
--font-sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--bg: #f5f5f5;
--bg-card: #fff;
--bg-input: #fafafa;
Expand Down Expand Up @@ -157,6 +157,23 @@ img { max-width: 100%; height: auto; }

.form-group + .form-group { margin-top: 1.25rem; }

.config-textarea {
width: 100%;
min-height: 460px;
background: var(--bg-input);
border: 1px solid var(--border-input);
border-radius: 0.5rem;
padding: 0.875rem 1rem;
font-size: 0.8125rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: var(--text);
outline: none;
resize: vertical;
line-height: 1.5;
tab-size: 2;
}
.config-textarea:focus { border-color: #dc2626; }

.btn {
display: inline-flex;
align-items: center;
Expand Down
3 changes: 0 additions & 3 deletions src/public/tui.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
<title>🦞 OpenClaw TUI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
<link rel="stylesheet" href="/styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="tui-body">
<div class="tui-header">
Expand Down
Loading
Loading