openpalm 0.9.7 → 0.9.9
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/bin/openpalm.js +5 -0
- package/package.json +4 -2
- package/playwright.config.ts +16 -0
- package/src/commands/install-file.test.ts +306 -0
- package/src/commands/install-services.test.ts +12 -7
- package/src/commands/install-services.ts +1 -1
- package/src/commands/install.ts +113 -30
- package/src/commands/restart.ts +2 -35
- package/src/commands/service.ts +0 -17
- package/src/commands/start.ts +5 -43
- package/src/commands/status.ts +0 -9
- package/src/commands/stop.ts +4 -36
- package/src/commands/uninstall.ts +23 -14
- package/src/commands/update.ts +0 -9
- package/src/lib/docker.ts +25 -7
- package/src/lib/env.ts +6 -59
- package/src/lib/paths.ts +11 -1
- package/src/lib/staging.ts +3 -3
- package/src/main.test.ts +67 -83
- package/src/main.ts +1 -1
- package/src/setup-wizard/index.html +114 -180
- package/src/setup-wizard/server-errors.test.ts +429 -0
- package/src/setup-wizard/server-integration.test.ts +511 -0
- package/src/setup-wizard/server.test.ts +6 -6
- package/src/setup-wizard/server.ts +17 -5
- package/src/setup-wizard/standalone.ts +166 -0
- package/src/setup-wizard/wizard.css +892 -299
- package/src/setup-wizard/wizard.js +1172 -559
- package/src/lib/admin.ts +0 -107
package/src/main.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { detectHostInfo, main, reconcileStackEnvImageTag, resolveRequestedImageTag, upsertEnvValue } from './main.ts';
|
|
@@ -57,92 +57,57 @@ describe('cli main', () => {
|
|
|
57
57
|
process.env.OPENPALM_ADMIN_TOKEN = originalOpenPalmAdminToken;
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it('
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log = mock(() => {}) as typeof console.log;
|
|
68
|
-
|
|
69
|
-
await main(['update']);
|
|
70
|
-
|
|
71
|
-
// tryAdminRequest attempts directly — no separate health check
|
|
72
|
-
expect(calls).toEqual([
|
|
73
|
-
'http://localhost:8100/admin/containers/pull',
|
|
74
|
-
]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('uses ADMIN_TOKEN from the environment for admin requests', async () => {
|
|
78
|
-
const adminTokens: string[] = [];
|
|
79
|
-
process.env.ADMIN_TOKEN = 'env-admin-token';
|
|
80
|
-
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
81
|
-
|
|
82
|
-
globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
|
|
83
|
-
const headers = new Headers(init?.headers);
|
|
84
|
-
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
85
|
-
return new Response('{"ok":true}', { status: 200 });
|
|
86
|
-
}) as typeof fetch;
|
|
87
|
-
console.log = mock(() => {}) as typeof console.log;
|
|
88
|
-
|
|
89
|
-
await main(['update']);
|
|
90
|
-
|
|
91
|
-
// Direct request — single call with token header
|
|
92
|
-
expect(adminTokens).toEqual(['env-admin-token']);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('falls back to the legacy parent secrets.env for admin requests', async () => {
|
|
96
|
-
const base = mkdtempSync(join(tmpdir(), 'openpalm-config-'));
|
|
97
|
-
const configHome = join(base, 'openpalm');
|
|
98
|
-
const adminTokens: string[] = [];
|
|
60
|
+
it('runs bootstrap install directly without admin delegation', async () => {
|
|
61
|
+
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
62
|
+
const configHome = join(base, 'config');
|
|
63
|
+
const dataHome = join(base, 'data');
|
|
64
|
+
const stateHome = join(base, 'state');
|
|
65
|
+
const workDir = join(base, 'work');
|
|
66
|
+
const binDir = join(stateHome, 'bin');
|
|
99
67
|
|
|
100
|
-
mkdirSync(
|
|
101
|
-
writeFileSync(join(
|
|
102
|
-
|
|
68
|
+
mkdirSync(binDir, { recursive: true });
|
|
69
|
+
writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
|
|
70
|
+
chmodSync(join(binDir, 'varlock'), 0o755);
|
|
103
71
|
|
|
104
72
|
process.env.OPENPALM_CONFIG_HOME = configHome;
|
|
73
|
+
process.env.OPENPALM_DATA_HOME = dataHome;
|
|
74
|
+
process.env.OPENPALM_STATE_HOME = stateHome;
|
|
75
|
+
process.env.OPENPALM_WORK_DIR = workDir;
|
|
105
76
|
delete process.env.ADMIN_TOKEN;
|
|
106
77
|
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
107
78
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
111
|
-
return new Response('{"ok":true}', { status: 200 });
|
|
112
|
-
}) as typeof fetch;
|
|
113
|
-
console.log = mock(() => {}) as typeof console.log;
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
await main(['update']);
|
|
117
|
-
// Direct request — single call with legacy token
|
|
118
|
-
expect(adminTokens).toEqual(['legacy-admin-token']);
|
|
119
|
-
} finally {
|
|
120
|
-
rmSync(base, { recursive: true, force: true });
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('calls admin install when stack is already running and token exists', async () => {
|
|
125
|
-
const calls: string[] = [];
|
|
126
|
-
process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
|
|
79
|
+
mockDockerCli();
|
|
80
|
+
const fetchedUrls: string[] = [];
|
|
127
81
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
128
82
|
const url = String(input);
|
|
129
|
-
|
|
83
|
+
fetchedUrls.push(url);
|
|
130
84
|
if (url.endsWith('/health')) {
|
|
131
85
|
return new Response('ok', { status: 200 });
|
|
132
86
|
}
|
|
133
|
-
|
|
87
|
+
if (url.includes('/docker-compose.yml')) {
|
|
88
|
+
return new Response('services: {}\n', { status: 200 });
|
|
89
|
+
}
|
|
90
|
+
if (url.includes('/Caddyfile')) {
|
|
91
|
+
return new Response(':80 {\n}\n', { status: 200 });
|
|
92
|
+
}
|
|
93
|
+
if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
|
|
94
|
+
return new Response('KEY=string\n', { status: 200 });
|
|
95
|
+
}
|
|
96
|
+
return new Response('', { status: 503 });
|
|
134
97
|
}) as typeof fetch;
|
|
135
98
|
console.log = mock(() => {}) as typeof console.log;
|
|
99
|
+
console.warn = mock(() => {}) as typeof console.warn;
|
|
136
100
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
|
|
101
|
+
try {
|
|
102
|
+
await main(['install', '--no-start', '--force', '--no-open']);
|
|
103
|
+
// Bootstrap runs directly, creating directories
|
|
104
|
+
expect(existsSync(join(dataHome, 'admin'))).toBe(true);
|
|
105
|
+
} finally {
|
|
106
|
+
rmSync(base, { recursive: true, force: true });
|
|
107
|
+
}
|
|
143
108
|
});
|
|
144
109
|
|
|
145
|
-
it('
|
|
110
|
+
it('creates the admin data directory during bootstrap install', async () => {
|
|
146
111
|
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
147
112
|
const configHome = join(base, 'config');
|
|
148
113
|
const dataHome = join(base, 'data');
|
|
@@ -158,16 +123,12 @@ describe('cli main', () => {
|
|
|
158
123
|
process.env.OPENPALM_DATA_HOME = dataHome;
|
|
159
124
|
process.env.OPENPALM_STATE_HOME = stateHome;
|
|
160
125
|
process.env.OPENPALM_WORK_DIR = workDir;
|
|
161
|
-
delete process.env.ADMIN_TOKEN;
|
|
162
|
-
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
163
126
|
|
|
164
127
|
mockDockerCli();
|
|
165
|
-
const fetchedUrls: string[] = [];
|
|
166
128
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
167
129
|
const url = String(input);
|
|
168
|
-
fetchedUrls.push(url);
|
|
169
130
|
if (url.endsWith('/health')) {
|
|
170
|
-
|
|
131
|
+
throw new TypeError('fetch failed');
|
|
171
132
|
}
|
|
172
133
|
if (url.includes('/docker-compose.yml')) {
|
|
173
134
|
return new Response('services: {}\n', { status: 200 });
|
|
@@ -181,24 +142,23 @@ describe('cli main', () => {
|
|
|
181
142
|
return new Response('', { status: 503 });
|
|
182
143
|
}) as typeof fetch;
|
|
183
144
|
console.log = mock(() => {}) as typeof console.log;
|
|
184
|
-
console.warn = mock(() => {}) as typeof console.warn;
|
|
185
145
|
|
|
186
146
|
try {
|
|
187
147
|
await main(['install', '--no-start', '--force', '--no-open']);
|
|
188
|
-
// Should have fallen through to bootstrap, creating directories
|
|
189
148
|
expect(existsSync(join(dataHome, 'admin'))).toBe(true);
|
|
190
149
|
} finally {
|
|
191
150
|
rmSync(base, { recursive: true, force: true });
|
|
192
151
|
}
|
|
193
152
|
});
|
|
194
153
|
|
|
195
|
-
it('
|
|
154
|
+
it('uses main as the default install ref for bootstrap asset downloads', async () => {
|
|
196
155
|
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
197
156
|
const configHome = join(base, 'config');
|
|
198
157
|
const dataHome = join(base, 'data');
|
|
199
158
|
const stateHome = join(base, 'state');
|
|
200
159
|
const workDir = join(base, 'work');
|
|
201
160
|
const binDir = join(stateHome, 'bin');
|
|
161
|
+
const fetchedUrls: string[] = [];
|
|
202
162
|
|
|
203
163
|
mkdirSync(binDir, { recursive: true });
|
|
204
164
|
writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
|
|
@@ -212,31 +172,55 @@ describe('cli main', () => {
|
|
|
212
172
|
mockDockerCli();
|
|
213
173
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
214
174
|
const url = String(input);
|
|
175
|
+
fetchedUrls.push(url);
|
|
215
176
|
if (url.endsWith('/health')) {
|
|
216
177
|
throw new TypeError('fetch failed');
|
|
217
178
|
}
|
|
218
|
-
if (url.includes('/docker-compose.yml')) {
|
|
179
|
+
if (url.includes('/main/assets/docker-compose.yml')) {
|
|
219
180
|
return new Response('services: {}\n', { status: 200 });
|
|
220
181
|
}
|
|
221
|
-
if (url.includes('/Caddyfile')) {
|
|
182
|
+
if (url.includes('/main/assets/Caddyfile')) {
|
|
222
183
|
return new Response(':80 {\n}\n', { status: 200 });
|
|
223
184
|
}
|
|
224
|
-
if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
|
|
185
|
+
if (url.includes('/main/assets/secrets.env.schema') || url.includes('/main/assets/stack.env.schema')) {
|
|
225
186
|
return new Response('KEY=string\n', { status: 200 });
|
|
226
187
|
}
|
|
227
188
|
return new Response('', { status: 503 });
|
|
228
189
|
}) as typeof fetch;
|
|
229
190
|
console.log = mock(() => {}) as typeof console.log;
|
|
191
|
+
console.warn = mock(() => {}) as typeof console.warn;
|
|
230
192
|
|
|
231
193
|
try {
|
|
232
194
|
await main(['install', '--no-start', '--force', '--no-open']);
|
|
233
|
-
|
|
195
|
+
|
|
196
|
+
expect(fetchedUrls).toContain(
|
|
197
|
+
'https://raw.githubusercontent.com/itlackey/openpalm/main/assets/docker-compose.yml',
|
|
198
|
+
);
|
|
199
|
+
expect(fetchedUrls).toContain(
|
|
200
|
+
'https://raw.githubusercontent.com/itlackey/openpalm/main/assets/Caddyfile',
|
|
201
|
+
);
|
|
234
202
|
} finally {
|
|
235
203
|
rmSync(base, { recursive: true, force: true });
|
|
236
204
|
}
|
|
237
205
|
});
|
|
238
206
|
});
|
|
239
207
|
|
|
208
|
+
describe('npm bin launcher', () => {
|
|
209
|
+
it('points the published bin to a Bun launcher script instead of a TypeScript source file', () => {
|
|
210
|
+
const cliPkg = JSON.parse(
|
|
211
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
212
|
+
) as {
|
|
213
|
+
bin?: Record<string, string>;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
expect(cliPkg.bin?.openpalm).toBe('./bin/openpalm.js');
|
|
217
|
+
|
|
218
|
+
const launcher = readFileSync(new URL('../bin/openpalm.js', import.meta.url), 'utf8');
|
|
219
|
+
|
|
220
|
+
expect(launcher.startsWith('#!/usr/bin/env bun\n')).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
240
224
|
describe('validate command', () => {
|
|
241
225
|
it('is a recognized command (does not throw Unknown command)', async () => {
|
|
242
226
|
const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
|
package/src/main.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { HostInfo } from './lib/host-info.ts';
|
|
|
8
8
|
export { upsertEnvValue, resolveRequestedImageTag, reconcileStackEnvImageTag } from './lib/env.ts';
|
|
9
9
|
export { bootstrapInstall } from './commands/install.ts';
|
|
10
10
|
|
|
11
|
-
const mainCommand = defineCommand({
|
|
11
|
+
export const mainCommand = defineCommand({
|
|
12
12
|
meta: {
|
|
13
13
|
name: 'openpalm',
|
|
14
14
|
version: cliPkg.version,
|
|
@@ -12,227 +12,149 @@
|
|
|
12
12
|
|
|
13
13
|
<!-- ── Header ─────────────────────────────────────────────── -->
|
|
14
14
|
<div class="wizard-header">
|
|
15
|
-
<
|
|
16
|
-
<
|
|
15
|
+
<div class="hdr-logo">OP</div>
|
|
16
|
+
<h1>OpenPalm <span class="hdr-suffix">Setup</span></h1>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
19
|
<div class="wizard-body">
|
|
20
20
|
|
|
21
|
-
<!-- ──
|
|
22
|
-
<nav class="
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<button class="step-dot" data-step="1" aria-label="Step 2: Connections" disabled>2</button>
|
|
26
|
-
<span class="step-line" data-line="1"></span>
|
|
27
|
-
<button class="step-dot" data-step="2" aria-label="Step 3: Models" disabled>3</button>
|
|
28
|
-
<span class="step-line" data-line="2"></span>
|
|
29
|
-
<button class="step-dot" data-step="3" aria-label="Step 4: Options" disabled>4</button>
|
|
30
|
-
<span class="step-line" data-line="3"></span>
|
|
31
|
-
<button class="step-dot" data-step="4" aria-label="Step 5: Review" disabled>5</button>
|
|
21
|
+
<!-- ── Progress Bar ────────────────────────────────────── -->
|
|
22
|
+
<nav class="prog-bar" aria-label="Wizard steps" id="step-indicators">
|
|
23
|
+
<div class="prog-segments" id="prog-segments"></div>
|
|
24
|
+
<div class="prog-labels" id="prog-labels"></div>
|
|
32
25
|
</nav>
|
|
33
26
|
|
|
34
27
|
<!-- ═══════════════════════════════════════════════════════════
|
|
35
|
-
Step 0: Welcome &
|
|
28
|
+
Step 0: Welcome & Identity
|
|
36
29
|
═══════════════════════════════════════════════════════════ -->
|
|
37
30
|
<section class="step-content" id="step-0" data-testid="step-welcome">
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
31
|
+
<!-- Welcome Hero -->
|
|
32
|
+
<div class="welcome-hero" id="welcome-hero">
|
|
33
|
+
<div class="welcome-icon">👋</div>
|
|
34
|
+
<h2>Welcome to OpenPalm</h2>
|
|
35
|
+
<p class="welcome-subtitle">Your self-hosted AI assistant. Pick your providers, choose models, and you're up and running.</p>
|
|
36
|
+
<div class="welcome-pills">
|
|
37
|
+
<span class="pill">Cloud or local</span>
|
|
38
|
+
<span class="pill">Smart defaults</span>
|
|
39
|
+
<span class="pill">Privacy first</span>
|
|
40
|
+
</div>
|
|
41
|
+
<button class="btn btn-primary-lg" id="btn-get-started">Get Started</button>
|
|
45
42
|
</div>
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
44
|
+
<!-- Identity Form (hidden until Get Started) -->
|
|
45
|
+
<div class="identity-form hidden" id="identity-form">
|
|
46
|
+
<h2>About You</h2>
|
|
47
|
+
<p class="step-description">Set up admin credentials and optional identity details.</p>
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
<div class="field-group">
|
|
50
|
+
<label for="admin-token">Admin Token</label>
|
|
51
|
+
<input id="admin-token" type="text" autocomplete="off" placeholder="Min 8 characters">
|
|
52
|
+
<p class="field-hint">Protects the admin console. A random token has been generated for you.</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="field-group">
|
|
56
|
+
<label for="owner-name">Your Name <span style="color:var(--color-text-tertiary);font-weight:normal">(optional)</span></label>
|
|
57
|
+
<input id="owner-name" type="text" placeholder="Jane Doe" autocomplete="name">
|
|
58
|
+
</div>
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
<div class="field-group">
|
|
61
|
+
<label for="owner-email">Email <span style="color:var(--color-text-tertiary);font-weight:normal">(optional)</span></label>
|
|
62
|
+
<input id="owner-email" type="email" placeholder="jane@example.com" autocomplete="email">
|
|
63
|
+
</div>
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
<div id="step0-error" class="field-error hidden" role="alert"></div>
|
|
66
|
+
|
|
67
|
+
<div class="step-actions">
|
|
68
|
+
<button class="btn btn-primary" id="btn-step0-next">Set Up Providers</button>
|
|
69
|
+
</div>
|
|
61
70
|
</div>
|
|
62
71
|
</section>
|
|
63
72
|
|
|
64
73
|
<!-- ═══════════════════════════════════════════════════════════
|
|
65
|
-
Step 1:
|
|
74
|
+
Step 1: Providers (Card Grid)
|
|
66
75
|
═══════════════════════════════════════════════════════════ -->
|
|
67
76
|
<section class="step-content hidden" id="step-1" data-testid="step-connections">
|
|
68
|
-
<h2>
|
|
69
|
-
<p class="step-description">
|
|
70
|
-
|
|
71
|
-
<!-- Existing connections list -->
|
|
72
|
-
<ul class="hub-list hidden" id="conn-hub-list" aria-label="Connections"></ul>
|
|
73
|
-
|
|
74
|
-
<!-- Empty state -->
|
|
75
|
-
<div class="hub-empty" id="conn-hub-empty">
|
|
76
|
-
<p class="hub-empty-headline">No connections yet</p>
|
|
77
|
-
<p class="hub-empty-body">Add a cloud API provider or a local model server to get started.</p>
|
|
78
|
-
</div>
|
|
77
|
+
<h2>Where should your models run?</h2>
|
|
78
|
+
<p class="step-description">Select one or more providers. Click a card to configure it.</p>
|
|
79
79
|
|
|
80
80
|
<!-- Loading state for provider detection -->
|
|
81
81
|
<div class="loading-state hidden" id="conn-detecting">
|
|
82
82
|
<span class="spinner"></span> Detecting local providers...
|
|
83
83
|
</div>
|
|
84
84
|
|
|
85
|
-
<!--
|
|
86
|
-
<div
|
|
87
|
-
<button class="conn-card" type="button" id="btn-add-cloud">
|
|
88
|
-
<div class="conn-icon conn-icon--cloud" aria-hidden="true">
|
|
89
|
-
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>
|
|
90
|
-
</div>
|
|
91
|
-
<div class="conn-body">
|
|
92
|
-
<span class="conn-label">Cloud Provider <span class="conn-badge">Hosted</span></span>
|
|
93
|
-
<span class="conn-desc">OpenAI, Anthropic, Groq, Together, Mistral, DeepSeek, xAI</span>
|
|
94
|
-
</div>
|
|
95
|
-
<div class="conn-arrow" aria-hidden="true">
|
|
96
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
97
|
-
</div>
|
|
98
|
-
</button>
|
|
99
|
-
<button class="conn-card" type="button" id="btn-add-local">
|
|
100
|
-
<div class="conn-icon conn-icon--local" aria-hidden="true">
|
|
101
|
-
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="6" cy="18" r="1" fill="currentColor" stroke="none"/></svg>
|
|
102
|
-
</div>
|
|
103
|
-
<div class="conn-body">
|
|
104
|
-
<span class="conn-label">Local Provider <span class="conn-badge conn-badge--local">On-Device</span></span>
|
|
105
|
-
<span class="conn-desc">Ollama, Docker Model Runner, LM Studio running locally</span>
|
|
106
|
-
</div>
|
|
107
|
-
<div class="conn-arrow" aria-hidden="true">
|
|
108
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
109
|
-
</div>
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<!-- Connection detail form (shared for cloud and local) -->
|
|
114
|
-
<div id="conn-detail-form" class="hidden">
|
|
115
|
-
<div class="connection-mode-card" id="conn-mode-card">
|
|
116
|
-
<div class="connection-mode-header">
|
|
117
|
-
<span class="connection-mode-badge" id="conn-mode-badge"></span>
|
|
118
|
-
<h3 id="conn-mode-title"></h3>
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
<!-- Cloud: provider quick-picks -->
|
|
123
|
-
<div class="provider-quick-picks hidden" id="cloud-provider-picks"></div>
|
|
124
|
-
|
|
125
|
-
<!-- Local: detected providers -->
|
|
126
|
-
<div id="local-provider-list" class="hidden"></div>
|
|
127
|
-
|
|
128
|
-
<div class="field-group">
|
|
129
|
-
<label for="conn-name">Connection Name</label>
|
|
130
|
-
<input id="conn-name" type="text" placeholder='e.g. "My Ollama", "Work OpenAI"'>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
<div class="field-group">
|
|
134
|
-
<label for="conn-base-url">Base URL</label>
|
|
135
|
-
<input id="conn-base-url" type="url" placeholder="https://api.openai.com">
|
|
136
|
-
<p class="field-hint">The provider endpoint. Do not include a trailing /v1.</p>
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<div class="field-group hidden" id="conn-apikey-group">
|
|
140
|
-
<label for="conn-api-key">API Key</label>
|
|
141
|
-
<input id="conn-api-key" type="password" placeholder="sk-..." autocomplete="new-password">
|
|
142
|
-
<p class="field-hint">Stored server-side only.</p>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<div id="conn-detail-error" class="field-error hidden" role="alert"></div>
|
|
146
|
-
|
|
147
|
-
<div class="connection-success hidden" id="conn-test-success" role="status">
|
|
148
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
149
|
-
<span id="conn-test-msg">Connected</span>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
<div class="step-actions">
|
|
153
|
-
<button class="btn btn-secondary" id="btn-conn-cancel">Cancel</button>
|
|
154
|
-
<button class="btn btn-outline" id="btn-conn-test">Test</button>
|
|
155
|
-
<button class="btn btn-primary" id="btn-conn-save">Save Connection</button>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
85
|
+
<!-- Provider card grid -->
|
|
86
|
+
<div class="provider-grid" id="provider-grid"></div>
|
|
158
87
|
|
|
159
|
-
<!--
|
|
88
|
+
<!-- Provider count info + nav -->
|
|
160
89
|
<div class="step-actions" id="step1-actions">
|
|
161
90
|
<button class="btn btn-secondary" id="btn-step1-back">Back</button>
|
|
162
|
-
<
|
|
163
|
-
<button class="btn btn-primary" id="btn-step1-next" disabled>
|
|
91
|
+
<span class="nav-info" id="provider-count-info"></span>
|
|
92
|
+
<button class="btn btn-primary" id="btn-step1-next" disabled>Choose Models</button>
|
|
164
93
|
</div>
|
|
165
94
|
</section>
|
|
166
95
|
|
|
167
96
|
<!-- ═══════════════════════════════════════════════════════════
|
|
168
|
-
Step 2: Model Assignment
|
|
97
|
+
Step 2: Model Assignment (Radio Options)
|
|
169
98
|
═══════════════════════════════════════════════════════════ -->
|
|
170
99
|
<section class="step-content hidden" id="step-2" data-testid="step-models">
|
|
171
|
-
<h2>
|
|
172
|
-
<p class="step-description">
|
|
173
|
-
|
|
174
|
-
<!-- LLM Card -->
|
|
175
|
-
<div class="model-card">
|
|
176
|
-
<div class="model-card-header">
|
|
177
|
-
<span class="model-card-title">Chat Model (LLM)</span>
|
|
178
|
-
<span class="model-card-help">Used for conversations, tool calls, and reasoning.</span>
|
|
179
|
-
</div>
|
|
180
|
-
<div class="field-group">
|
|
181
|
-
<label for="llm-connection">Connection</label>
|
|
182
|
-
<select id="llm-connection">
|
|
183
|
-
<option value="" disabled selected>Select a connection</option>
|
|
184
|
-
</select>
|
|
185
|
-
</div>
|
|
186
|
-
<div class="field-group">
|
|
187
|
-
<label for="llm-model">Model</label>
|
|
188
|
-
<select id="llm-model"><option value="">Loading...</option></select>
|
|
189
|
-
</div>
|
|
190
|
-
<div class="field-group">
|
|
191
|
-
<label for="llm-small-model">Small Model <span style="color:var(--color-text-tertiary);font-weight:normal">(optional)</span></label>
|
|
192
|
-
<select id="llm-small-model"><option value="">(same as chat model)</option></select>
|
|
193
|
-
<p class="field-hint">Used for lightweight tasks like memory extraction.</p>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
100
|
+
<h2>Choose Your Models</h2>
|
|
101
|
+
<p class="step-description">Pre-selected from your providers. Adjust if needed.</p>
|
|
196
102
|
|
|
197
|
-
<!--
|
|
198
|
-
<div
|
|
199
|
-
<div class="model-card-header">
|
|
200
|
-
<span class="model-card-title">Embeddings</span>
|
|
201
|
-
<span class="model-card-help">Used for vector search and memory.</span>
|
|
202
|
-
</div>
|
|
203
|
-
<div class="field-group">
|
|
204
|
-
<label for="emb-connection">Connection</label>
|
|
205
|
-
<select id="emb-connection">
|
|
206
|
-
<option value="" disabled selected>Select a connection</option>
|
|
207
|
-
</select>
|
|
208
|
-
</div>
|
|
209
|
-
<div class="field-group">
|
|
210
|
-
<label for="emb-model">Embedding Model</label>
|
|
211
|
-
<select id="emb-model"><option value="">Loading...</option></select>
|
|
212
|
-
</div>
|
|
213
|
-
<div class="field-group">
|
|
214
|
-
<label for="emb-dims">Embedding Dimensions</label>
|
|
215
|
-
<input id="emb-dims" type="number" value="1536" min="1" step="1">
|
|
216
|
-
<p class="field-hint">Set automatically for known models. Override only if needed.</p>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
103
|
+
<!-- Model groups rendered dynamically -->
|
|
104
|
+
<div id="model-groups"></div>
|
|
219
105
|
|
|
220
|
-
|
|
106
|
+
<!-- Hidden fields for backward compatibility with API payload -->
|
|
107
|
+
<input type="hidden" id="llm-connection" value="">
|
|
108
|
+
<input type="hidden" id="llm-model" value="">
|
|
109
|
+
<input type="hidden" id="llm-small-model" value="">
|
|
110
|
+
<input type="hidden" id="emb-connection" value="">
|
|
111
|
+
<input type="hidden" id="emb-model" value="">
|
|
112
|
+
<input type="hidden" id="emb-dims" value="1536">
|
|
221
113
|
|
|
222
114
|
<div id="step2-error" class="field-error hidden" role="alert"></div>
|
|
223
115
|
|
|
224
116
|
<div class="step-actions">
|
|
225
117
|
<button class="btn btn-secondary" id="btn-step2-back">Back</button>
|
|
226
|
-
<button class="btn btn-primary" id="btn-step2-next">
|
|
118
|
+
<button class="btn btn-primary" id="btn-step2-next">Voice Setup</button>
|
|
227
119
|
</div>
|
|
228
120
|
</section>
|
|
229
121
|
|
|
230
122
|
<!-- ═══════════════════════════════════════════════════════════
|
|
231
|
-
Step 3:
|
|
123
|
+
Step 3: Voice (TTS / STT)
|
|
232
124
|
═══════════════════════════════════════════════════════════ -->
|
|
233
|
-
<section class="step-content hidden" id="step-3" data-testid="step-
|
|
125
|
+
<section class="step-content hidden" id="step-3" data-testid="step-voice">
|
|
126
|
+
<h2>Voice Capabilities</h2>
|
|
127
|
+
<p class="step-description">Choose how your assistant speaks and listens.</p>
|
|
128
|
+
|
|
129
|
+
<!-- Voice groups rendered dynamically -->
|
|
130
|
+
<div id="voice-groups"></div>
|
|
131
|
+
|
|
132
|
+
<div class="step-actions">
|
|
133
|
+
<button class="btn btn-secondary" id="btn-step3-back">Back</button>
|
|
134
|
+
<button class="btn btn-primary" id="btn-step3-next">Options</button>
|
|
135
|
+
</div>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
139
|
+
Step 4: Options (Channels + Services + Memory)
|
|
140
|
+
═══════════════════════════════════════════════════════════ -->
|
|
141
|
+
<section class="step-content hidden" id="step-4" data-testid="step-options">
|
|
234
142
|
<h2>Options</h2>
|
|
235
|
-
<p class="step-description">
|
|
143
|
+
<p class="step-description">Choose channels, services, and tweak settings before review.</p>
|
|
144
|
+
|
|
145
|
+
<!-- Channels -->
|
|
146
|
+
<div class="options-section">
|
|
147
|
+
<h3 class="options-section-title">Channels</h3>
|
|
148
|
+
<p class="options-section-desc">How you talk to your assistant. Web Chat is always on.</p>
|
|
149
|
+
<div class="toggle-grid" id="channels-grid"></div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Services -->
|
|
153
|
+
<div class="options-section">
|
|
154
|
+
<h3 class="options-section-title">Services</h3>
|
|
155
|
+
<p class="options-section-desc">Extra capabilities for your stack.</p>
|
|
156
|
+
<div class="toggle-grid" id="services-grid"></div>
|
|
157
|
+
</div>
|
|
236
158
|
|
|
237
159
|
<!-- Ollama in-stack toggle (only shown when relevant) -->
|
|
238
160
|
<div class="addon-row hidden" id="ollama-addon">
|
|
@@ -251,25 +173,37 @@
|
|
|
251
173
|
<p class="field-hint">Identifies the memory owner. Defaults to your email or "default_user".</p>
|
|
252
174
|
</div>
|
|
253
175
|
|
|
176
|
+
<div id="step4-error" class="field-error hidden" role="alert"></div>
|
|
177
|
+
|
|
254
178
|
<div class="step-actions">
|
|
255
|
-
<button class="btn btn-secondary" id="btn-
|
|
256
|
-
<button class="btn btn-primary" id="btn-
|
|
179
|
+
<button class="btn btn-secondary" id="btn-step4-back">Back</button>
|
|
180
|
+
<button class="btn btn-primary" id="btn-step4-next">Review</button>
|
|
257
181
|
</div>
|
|
258
182
|
</section>
|
|
259
183
|
|
|
260
184
|
<!-- ═══════════════════════════════════════════════════════════
|
|
261
|
-
Step
|
|
185
|
+
Step 5: Review & Install
|
|
262
186
|
═══════════════════════════════════════════════════════════ -->
|
|
263
|
-
<section class="step-content hidden" id="step-
|
|
187
|
+
<section class="step-content hidden" id="step-5" data-testid="step-review">
|
|
264
188
|
<h2>Review & Install</h2>
|
|
265
189
|
<p class="step-description">Confirm your settings, then install.</p>
|
|
266
190
|
|
|
267
|
-
<div
|
|
191
|
+
<div id="review-summary"></div>
|
|
192
|
+
|
|
193
|
+
<div class="review-json-toggle" id="review-json-toggle">
|
|
194
|
+
<button class="btn-json-toggle" id="btn-toggle-json" type="button">Show Setup JSON</button>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="review-json hidden" id="review-json">
|
|
197
|
+
<pre id="review-json-pre"></pre>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<!-- Keep old review-grid for backward compat (hidden, populated for tests) -->
|
|
201
|
+
<div class="hidden" id="review-grid"></div>
|
|
268
202
|
|
|
269
203
|
<div id="install-error" class="install-error hidden" role="alert"></div>
|
|
270
204
|
|
|
271
205
|
<div class="step-actions" id="review-actions">
|
|
272
|
-
<button class="btn btn-secondary" id="btn-
|
|
206
|
+
<button class="btn btn-secondary" id="btn-step5-back">Back</button>
|
|
273
207
|
<button class="btn btn-primary" id="btn-install">Install</button>
|
|
274
208
|
</div>
|
|
275
209
|
</section>
|
|
@@ -330,7 +264,7 @@
|
|
|
330
264
|
<h2>Setup Complete</h2>
|
|
331
265
|
<p class="done-subtitle">Your OpenPalm stack is up and running.</p>
|
|
332
266
|
<ul class="service-list" id="deploy-service-list"></ul>
|
|
333
|
-
<a href="
|
|
267
|
+
<a href="http://localhost:4096" class="btn btn-primary">Open Console</a>
|
|
334
268
|
</div>
|
|
335
269
|
|
|
336
270
|
<!-- Deploy error actions -->
|