@bakapiano/ccsm 0.22.2 → 0.22.4
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/CLAUDE.md +538 -538
- package/README.md +189 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +139 -139
- package/lib/codexSeed.js +183 -183
- package/lib/config.js +274 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/localCliSessions.js +519 -519
- package/lib/persistedSessions.js +129 -129
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +233 -231
- package/lib/workspace.js +233 -233
- package/package.json +57 -57
- package/public/css/base.css +99 -99
- package/public/css/cards.css +183 -183
- package/public/css/feedback.css +504 -504
- package/public/css/forms.css +453 -453
- package/public/css/layout.css +176 -176
- package/public/css/modal.css +190 -190
- package/public/css/responsive.css +176 -176
- package/public/css/sidebar.css +707 -707
- package/public/css/terminals.css +592 -592
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2725 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +371 -371
- package/public/js/backend.js +149 -149
- package/public/js/components/App.js +73 -73
- package/public/js/components/DirectoryPicker.js +203 -203
- package/public/js/components/EntityFormModal.js +153 -153
- package/public/js/components/Modal.js +57 -57
- package/public/js/components/OfflineBanner.js +67 -67
- package/public/js/components/PageTitleBar.js +13 -13
- package/public/js/components/PendingApprovalOverlay.js +128 -128
- package/public/js/components/Picker.js +179 -179
- package/public/js/components/Popover.js +55 -55
- package/public/js/components/RestartOverlay.js +36 -36
- package/public/js/components/Sidebar.js +380 -380
- package/public/js/components/TerminalInstance.js +187 -15
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/XtermTerminal.js +148 -14
- package/public/js/components/useDragSort.js +67 -67
- package/public/js/dialog.js +67 -67
- package/public/js/icons.js +212 -212
- package/public/js/main.js +296 -296
- package/public/js/pages/AboutPage.js +90 -90
- package/public/js/pages/ConfigurePage.js +713 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +100 -100
- package/public/js/state.js +335 -335
- package/public/manifest.webmanifest +25 -0
- package/public/setup/index.html +567 -0
- package/scripts/dev.js +149 -149
- package/scripts/install.js +153 -153
- package/scripts/restart-helper.js +96 -96
- package/scripts/upgrade-helper.js +687 -687
- package/server.js +1807 -1807
package/public/js/backend.js
CHANGED
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
// One source of truth for "where is the ccsm backend reachable"
|
|
2
|
-
// and "what auth token (if any) do we attach to every request".
|
|
3
|
-
//
|
|
4
|
-
// localhost / 127.0.0.1 same-origin (page IS the backend)
|
|
5
|
-
// bakapiano.github.io http://localhost:7777 (the hosted
|
|
6
|
-
// frontend talks to the user's local
|
|
7
|
-
// backend via CORS)
|
|
8
|
-
// anything else (tunnel domain) same-origin (the local backend is
|
|
9
|
-
// serving this frontend over the
|
|
10
|
-
// tunnel; API calls go to the same
|
|
11
|
-
// tunnel URL automatically)
|
|
12
|
-
//
|
|
13
|
-
// httpBase is used by fetch(); wsBase is used by WebSocket constructions.
|
|
14
|
-
// Keep both as functions rather than constants so the values reflect
|
|
15
|
-
// `location.*` at call time (matters for tests / route changes).
|
|
16
|
-
|
|
17
|
-
const HOSTED_HOST = 'bakapiano.github.io';
|
|
18
|
-
|
|
19
|
-
function isLocal() {
|
|
20
|
-
return location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
21
|
-
}
|
|
22
|
-
function isHosted() {
|
|
23
|
-
return location.hostname === HOSTED_HOST;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function httpBase() {
|
|
27
|
-
if (isHosted()) return 'http://localhost:7777';
|
|
28
|
-
// Local OR tunnel-served — both same-origin.
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function wsBase() {
|
|
33
|
-
if (isHosted()) return 'ws://localhost:7777';
|
|
34
|
-
return `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function isHostedFrontend() {
|
|
38
|
-
return isHosted();
|
|
39
|
-
}
|
|
40
|
-
// True when the page is being served via a remote tunnel — neither the
|
|
41
|
-
// host machine itself (localhost) nor the GH-Pages router. Used to gate
|
|
42
|
-
// off "wake backend" affordances that only work locally.
|
|
43
|
-
export function isRemoteAccess() {
|
|
44
|
-
return !isLocal() && !isHosted();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ── Remote-access bearer token ────────────────────────────────────
|
|
48
|
-
// Persisted in localStorage so it survives reloads on whatever device
|
|
49
|
-
// loaded the share URL. main.js captures a fresh token from `?token=`
|
|
50
|
-
// on first arrival and stashes it via setToken(), then strips the
|
|
51
|
-
// query string from the URL so the secret doesn't sit in the address
|
|
52
|
-
// bar / browser history.
|
|
53
|
-
const LS_KEY = 'ccsm.token';
|
|
54
|
-
|
|
55
|
-
export function getToken() {
|
|
56
|
-
try { return localStorage.getItem(LS_KEY) || null; } catch { return null; }
|
|
57
|
-
}
|
|
58
|
-
export function setToken(t) {
|
|
59
|
-
try {
|
|
60
|
-
if (t) localStorage.setItem(LS_KEY, t);
|
|
61
|
-
else localStorage.removeItem(LS_KEY);
|
|
62
|
-
} catch {}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── Device id ─────────────────────────────────────────────────────
|
|
66
|
-
// Per-browser-profile UUID that identifies this device to the host
|
|
67
|
-
// machine for the approval flow. Generated once, persisted in
|
|
68
|
-
// localStorage, sent on every API call as X-Device-Id. The host pairs
|
|
69
|
-
// the id with the User-Agent the server records on first sight, so
|
|
70
|
-
// the approval UI can show "iPhone · Safari" instead of a raw uuid.
|
|
71
|
-
const LS_DEVICE = 'ccsm.deviceId';
|
|
72
|
-
|
|
73
|
-
export function getDeviceId() {
|
|
74
|
-
try {
|
|
75
|
-
let id = localStorage.getItem(LS_DEVICE);
|
|
76
|
-
if (!id) {
|
|
77
|
-
id = (crypto.randomUUID && crypto.randomUUID()) || (Math.random().toString(36).slice(2) + Date.now().toString(36));
|
|
78
|
-
localStorage.setItem(LS_DEVICE, id);
|
|
79
|
-
}
|
|
80
|
-
return id;
|
|
81
|
-
} catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── Initial terminal geometry ─────────────────────────────────────
|
|
87
|
-
// Estimate how many cols/rows the live session pane can hold, so a
|
|
88
|
-
// resumed / newly-launched PTY can spawn at roughly the right size
|
|
89
|
-
// instead of node-pty's 120×30 default. Why it matters: an alt-screen
|
|
90
|
-
// TUI like claude lays its entire UI out the instant it starts, using
|
|
91
|
-
// whatever size the PTY had then. xterm only sends the real size once
|
|
92
|
-
// its WebSocket opens — a beat later — and claude, having already
|
|
93
|
-
// painted at 30 rows, doesn't re-expand to fill a tall window until
|
|
94
|
-
// something forces a redraw (e.g. the user resizing it). On a big
|
|
95
|
-
// display that strands the terminal at ~1/4 height. Seeding the spawn
|
|
96
|
-
// with the pane's real dimensions sidesteps the race; xterm's own fit
|
|
97
|
-
// still corrects any few-row estimate error when it attaches.
|
|
98
|
-
// Returns null when nothing measurable is mounted, so the caller omits
|
|
99
|
-
// the hint and the backend keeps its default.
|
|
100
|
-
export function estimateTermSize() {
|
|
101
|
-
let w, h;
|
|
102
|
-
const pane = document.querySelector('.terminal-host')
|
|
103
|
-
|| document.querySelector('.session-pane-body');
|
|
104
|
-
if (pane) {
|
|
105
|
-
const r = pane.getBoundingClientRect();
|
|
106
|
-
w = r.width; h = r.height;
|
|
107
|
-
} else {
|
|
108
|
-
// Launching from the Launch page — no pane yet. Approximate from the
|
|
109
|
-
// window minus the sidebar column and the ~70px of top chrome.
|
|
110
|
-
const sb = document.querySelector('.sidebar');
|
|
111
|
-
w = window.innerWidth - (sb ? sb.getBoundingClientRect().width : 232) - 32;
|
|
112
|
-
h = window.innerHeight - 70;
|
|
113
|
-
}
|
|
114
|
-
if (!(w > 40) || !(h > 40)) return null;
|
|
115
|
-
// Mirror TerminalView's font sizing (13px desktop / 11px mobile,
|
|
116
|
-
// lineHeight 1.2); cell advance ≈ 0.6em for the mono stack.
|
|
117
|
-
const isMobile = window.matchMedia('(max-width: 640px)').matches;
|
|
118
|
-
const fontSize = isMobile ? 11 : 13;
|
|
119
|
-
return {
|
|
120
|
-
cols: Math.max(20, Math.min(400, Math.floor(w / (fontSize * 0.6)))),
|
|
121
|
-
rows: Math.max(8, Math.min(200, Math.floor(h / (fontSize * 1.2)))),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Per-device 4-digit human-verification code. Sent alongside the
|
|
126
|
-
// device id so the operator approving on the host can match what
|
|
127
|
-
// they see in the Remote page against what the requesting user
|
|
128
|
-
// reads off their own screen — guards against approving the wrong
|
|
129
|
-
// pending request when two devices arrive in quick succession.
|
|
130
|
-
// Purely identification, NOT a credential — no secrecy assumed.
|
|
131
|
-
const LS_DEVICE_CODE = 'ccsm.deviceCode';
|
|
132
|
-
|
|
133
|
-
export function getDeviceCode() {
|
|
134
|
-
try {
|
|
135
|
-
let c = localStorage.getItem(LS_DEVICE_CODE);
|
|
136
|
-
if (!c || !/^\d{4}$/.test(c)) {
|
|
137
|
-
// 1000..9999 inclusive so the leading digit is never 0 — keeps
|
|
138
|
-
// the code visually consistent at 4 characters wherever it
|
|
139
|
-
// shows up. Random.value covers 9000 possibilities, plenty for
|
|
140
|
-
// a "which of these is yours" disambiguator.
|
|
141
|
-
const n = 1000 + Math.floor(Math.random() * 9000);
|
|
142
|
-
c = String(n);
|
|
143
|
-
localStorage.setItem(LS_DEVICE_CODE, c);
|
|
144
|
-
}
|
|
145
|
-
return c;
|
|
146
|
-
} catch {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
1
|
+
// One source of truth for "where is the ccsm backend reachable"
|
|
2
|
+
// and "what auth token (if any) do we attach to every request".
|
|
3
|
+
//
|
|
4
|
+
// localhost / 127.0.0.1 same-origin (page IS the backend)
|
|
5
|
+
// bakapiano.github.io http://localhost:7777 (the hosted
|
|
6
|
+
// frontend talks to the user's local
|
|
7
|
+
// backend via CORS)
|
|
8
|
+
// anything else (tunnel domain) same-origin (the local backend is
|
|
9
|
+
// serving this frontend over the
|
|
10
|
+
// tunnel; API calls go to the same
|
|
11
|
+
// tunnel URL automatically)
|
|
12
|
+
//
|
|
13
|
+
// httpBase is used by fetch(); wsBase is used by WebSocket constructions.
|
|
14
|
+
// Keep both as functions rather than constants so the values reflect
|
|
15
|
+
// `location.*` at call time (matters for tests / route changes).
|
|
16
|
+
|
|
17
|
+
const HOSTED_HOST = 'bakapiano.github.io';
|
|
18
|
+
|
|
19
|
+
function isLocal() {
|
|
20
|
+
return location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
21
|
+
}
|
|
22
|
+
function isHosted() {
|
|
23
|
+
return location.hostname === HOSTED_HOST;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function httpBase() {
|
|
27
|
+
if (isHosted()) return 'http://localhost:7777';
|
|
28
|
+
// Local OR tunnel-served — both same-origin.
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function wsBase() {
|
|
33
|
+
if (isHosted()) return 'ws://localhost:7777';
|
|
34
|
+
return `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isHostedFrontend() {
|
|
38
|
+
return isHosted();
|
|
39
|
+
}
|
|
40
|
+
// True when the page is being served via a remote tunnel — neither the
|
|
41
|
+
// host machine itself (localhost) nor the GH-Pages router. Used to gate
|
|
42
|
+
// off "wake backend" affordances that only work locally.
|
|
43
|
+
export function isRemoteAccess() {
|
|
44
|
+
return !isLocal() && !isHosted();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Remote-access bearer token ────────────────────────────────────
|
|
48
|
+
// Persisted in localStorage so it survives reloads on whatever device
|
|
49
|
+
// loaded the share URL. main.js captures a fresh token from `?token=`
|
|
50
|
+
// on first arrival and stashes it via setToken(), then strips the
|
|
51
|
+
// query string from the URL so the secret doesn't sit in the address
|
|
52
|
+
// bar / browser history.
|
|
53
|
+
const LS_KEY = 'ccsm.token';
|
|
54
|
+
|
|
55
|
+
export function getToken() {
|
|
56
|
+
try { return localStorage.getItem(LS_KEY) || null; } catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
export function setToken(t) {
|
|
59
|
+
try {
|
|
60
|
+
if (t) localStorage.setItem(LS_KEY, t);
|
|
61
|
+
else localStorage.removeItem(LS_KEY);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Device id ─────────────────────────────────────────────────────
|
|
66
|
+
// Per-browser-profile UUID that identifies this device to the host
|
|
67
|
+
// machine for the approval flow. Generated once, persisted in
|
|
68
|
+
// localStorage, sent on every API call as X-Device-Id. The host pairs
|
|
69
|
+
// the id with the User-Agent the server records on first sight, so
|
|
70
|
+
// the approval UI can show "iPhone · Safari" instead of a raw uuid.
|
|
71
|
+
const LS_DEVICE = 'ccsm.deviceId';
|
|
72
|
+
|
|
73
|
+
export function getDeviceId() {
|
|
74
|
+
try {
|
|
75
|
+
let id = localStorage.getItem(LS_DEVICE);
|
|
76
|
+
if (!id) {
|
|
77
|
+
id = (crypto.randomUUID && crypto.randomUUID()) || (Math.random().toString(36).slice(2) + Date.now().toString(36));
|
|
78
|
+
localStorage.setItem(LS_DEVICE, id);
|
|
79
|
+
}
|
|
80
|
+
return id;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Initial terminal geometry ─────────────────────────────────────
|
|
87
|
+
// Estimate how many cols/rows the live session pane can hold, so a
|
|
88
|
+
// resumed / newly-launched PTY can spawn at roughly the right size
|
|
89
|
+
// instead of node-pty's 120×30 default. Why it matters: an alt-screen
|
|
90
|
+
// TUI like claude lays its entire UI out the instant it starts, using
|
|
91
|
+
// whatever size the PTY had then. xterm only sends the real size once
|
|
92
|
+
// its WebSocket opens — a beat later — and claude, having already
|
|
93
|
+
// painted at 30 rows, doesn't re-expand to fill a tall window until
|
|
94
|
+
// something forces a redraw (e.g. the user resizing it). On a big
|
|
95
|
+
// display that strands the terminal at ~1/4 height. Seeding the spawn
|
|
96
|
+
// with the pane's real dimensions sidesteps the race; xterm's own fit
|
|
97
|
+
// still corrects any few-row estimate error when it attaches.
|
|
98
|
+
// Returns null when nothing measurable is mounted, so the caller omits
|
|
99
|
+
// the hint and the backend keeps its default.
|
|
100
|
+
export function estimateTermSize() {
|
|
101
|
+
let w, h;
|
|
102
|
+
const pane = document.querySelector('.terminal-host')
|
|
103
|
+
|| document.querySelector('.session-pane-body');
|
|
104
|
+
if (pane) {
|
|
105
|
+
const r = pane.getBoundingClientRect();
|
|
106
|
+
w = r.width; h = r.height;
|
|
107
|
+
} else {
|
|
108
|
+
// Launching from the Launch page — no pane yet. Approximate from the
|
|
109
|
+
// window minus the sidebar column and the ~70px of top chrome.
|
|
110
|
+
const sb = document.querySelector('.sidebar');
|
|
111
|
+
w = window.innerWidth - (sb ? sb.getBoundingClientRect().width : 232) - 32;
|
|
112
|
+
h = window.innerHeight - 70;
|
|
113
|
+
}
|
|
114
|
+
if (!(w > 40) || !(h > 40)) return null;
|
|
115
|
+
// Mirror TerminalView's font sizing (13px desktop / 11px mobile,
|
|
116
|
+
// lineHeight 1.2); cell advance ≈ 0.6em for the mono stack.
|
|
117
|
+
const isMobile = window.matchMedia('(max-width: 640px)').matches;
|
|
118
|
+
const fontSize = isMobile ? 11 : 13;
|
|
119
|
+
return {
|
|
120
|
+
cols: Math.max(20, Math.min(400, Math.floor(w / (fontSize * 0.6)))),
|
|
121
|
+
rows: Math.max(8, Math.min(200, Math.floor(h / (fontSize * 1.2)))),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Per-device 4-digit human-verification code. Sent alongside the
|
|
126
|
+
// device id so the operator approving on the host can match what
|
|
127
|
+
// they see in the Remote page against what the requesting user
|
|
128
|
+
// reads off their own screen — guards against approving the wrong
|
|
129
|
+
// pending request when two devices arrive in quick succession.
|
|
130
|
+
// Purely identification, NOT a credential — no secrecy assumed.
|
|
131
|
+
const LS_DEVICE_CODE = 'ccsm.deviceCode';
|
|
132
|
+
|
|
133
|
+
export function getDeviceCode() {
|
|
134
|
+
try {
|
|
135
|
+
let c = localStorage.getItem(LS_DEVICE_CODE);
|
|
136
|
+
if (!c || !/^\d{4}$/.test(c)) {
|
|
137
|
+
// 1000..9999 inclusive so the leading digit is never 0 — keeps
|
|
138
|
+
// the code visually consistent at 4 characters wherever it
|
|
139
|
+
// shows up. Random.value covers 9000 possibilities, plenty for
|
|
140
|
+
// a "which of these is yours" disambiguator.
|
|
141
|
+
const n = 1000 + Math.floor(Math.random() * 9000);
|
|
142
|
+
c = String(n);
|
|
143
|
+
localStorage.setItem(LS_DEVICE_CODE, c);
|
|
144
|
+
}
|
|
145
|
+
return c;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
import { html } from '../html.js';
|
|
2
|
-
import { activeTab, selectTab } from '../state.js';
|
|
3
|
-
import { useEffect } from 'preact/hooks';
|
|
4
|
-
import { isRemoteAccess } from '../backend.js';
|
|
5
|
-
import { PageTitleBar } from './PageTitleBar.js';
|
|
6
|
-
import { Sidebar } from './Sidebar.js';
|
|
7
|
-
import { Toast } from './Toast.js';
|
|
8
|
-
import { DialogHost } from './DialogHost.js';
|
|
9
|
-
import { HealthOverlay } from './HealthOverlay.js';
|
|
10
|
-
import { PendingApprovalOverlay } from './PendingApprovalOverlay.js';
|
|
11
|
-
import { RestartOverlay } from './RestartOverlay.js';
|
|
12
|
-
import { MobileNavFab } from './MobileNavFab.js';
|
|
13
|
-
import { isMobile, mobileDrawerOpen } from '../state.js';
|
|
14
|
-
import { SessionsPage } from '../pages/SessionsPage.js';
|
|
15
|
-
import { LaunchPage } from '../pages/LaunchPage.js';
|
|
16
|
-
import { ConfigurePage } from '../pages/ConfigurePage.js';
|
|
17
|
-
import { RemotePage } from '../pages/RemotePage.js';
|
|
18
|
-
import { AboutPage } from '../pages/AboutPage.js';
|
|
19
|
-
|
|
20
|
-
function Panel({ name, children }) {
|
|
21
|
-
const active = activeTab.value === name;
|
|
22
|
-
return html`<section class="tab-panel" data-panel=${name} data-active=${active || null}>${children}</section>`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Static placeholder for #remote on tunnel-served pages. Remote / device
|
|
26
|
-
// / tunnel management is loopback-only — the server returns 403 on
|
|
27
|
-
// every relevant endpoint — so even if a user navigates here via URL
|
|
28
|
-
// hash we render a clear "host machine only" message instead of a
|
|
29
|
-
// broken RemotePage spamming the console.
|
|
30
|
-
function RemoteHostOnlyPanel() {
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
// Bounce back to whatever tab they were on before, after a brief
|
|
33
|
-
// moment so the message is readable.
|
|
34
|
-
const t = setTimeout(() => selectTab('sessions'), 2500);
|
|
35
|
-
return () => clearTimeout(t);
|
|
36
|
-
}, []);
|
|
37
|
-
return html`
|
|
38
|
-
<${PageTitleBar} title="Remote" />
|
|
39
|
-
<div class="settings-scroll">
|
|
40
|
-
<p class="remote-empty" style="margin-top:var(--s-6)">
|
|
41
|
-
Remote management is only available on the host machine.
|
|
42
|
-
Bouncing back to Sessions…
|
|
43
|
-
</p>
|
|
44
|
-
</div>`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function App() {
|
|
48
|
-
const tab = activeTab.value;
|
|
49
|
-
const remoteLocked = tab === 'remote' && isRemoteAccess();
|
|
50
|
-
const mobile = isMobile.value;
|
|
51
|
-
const drawer = mobileDrawerOpen.value;
|
|
52
|
-
|
|
53
|
-
return html`
|
|
54
|
-
<div class=${`app${mobile ? ' is-mobile' : ''}${mobile && drawer ? ' drawer-open' : ''}`}>
|
|
55
|
-
<${Sidebar} />
|
|
56
|
-
<main class="main">
|
|
57
|
-
<div class="content">
|
|
58
|
-
${tab === 'sessions' ? html`<${Panel} name="sessions"><${SessionsPage} /></${Panel}>` : null}
|
|
59
|
-
${tab === 'launch' ? html`<${Panel} name="launch"><${LaunchPage} /></${Panel}>` : null}
|
|
60
|
-
${tab === 'configure' ? html`<${Panel} name="configure"><${ConfigurePage} /></${Panel}>` : null}
|
|
61
|
-
${tab === 'remote' && !remoteLocked ? html`<${Panel} name="remote"><${RemotePage} /></${Panel}>` : null}
|
|
62
|
-
${remoteLocked ? html`<${Panel} name="remote"><${RemoteHostOnlyPanel} /></${Panel}>` : null}
|
|
63
|
-
${tab === 'about' ? html`<${Panel} name="about"><${AboutPage} /></${Panel}>` : null}
|
|
64
|
-
</div>
|
|
65
|
-
</main>
|
|
66
|
-
<${Toast} />
|
|
67
|
-
<${DialogHost} />
|
|
68
|
-
<${HealthOverlay} />
|
|
69
|
-
<${RestartOverlay} />
|
|
70
|
-
<${PendingApprovalOverlay} />
|
|
71
|
-
<${MobileNavFab} />
|
|
72
|
-
</div>`;
|
|
73
|
-
}
|
|
1
|
+
import { html } from '../html.js';
|
|
2
|
+
import { activeTab, selectTab } from '../state.js';
|
|
3
|
+
import { useEffect } from 'preact/hooks';
|
|
4
|
+
import { isRemoteAccess } from '../backend.js';
|
|
5
|
+
import { PageTitleBar } from './PageTitleBar.js';
|
|
6
|
+
import { Sidebar } from './Sidebar.js';
|
|
7
|
+
import { Toast } from './Toast.js';
|
|
8
|
+
import { DialogHost } from './DialogHost.js';
|
|
9
|
+
import { HealthOverlay } from './HealthOverlay.js';
|
|
10
|
+
import { PendingApprovalOverlay } from './PendingApprovalOverlay.js';
|
|
11
|
+
import { RestartOverlay } from './RestartOverlay.js';
|
|
12
|
+
import { MobileNavFab } from './MobileNavFab.js';
|
|
13
|
+
import { isMobile, mobileDrawerOpen } from '../state.js';
|
|
14
|
+
import { SessionsPage } from '../pages/SessionsPage.js';
|
|
15
|
+
import { LaunchPage } from '../pages/LaunchPage.js';
|
|
16
|
+
import { ConfigurePage } from '../pages/ConfigurePage.js';
|
|
17
|
+
import { RemotePage } from '../pages/RemotePage.js';
|
|
18
|
+
import { AboutPage } from '../pages/AboutPage.js';
|
|
19
|
+
|
|
20
|
+
function Panel({ name, children }) {
|
|
21
|
+
const active = activeTab.value === name;
|
|
22
|
+
return html`<section class="tab-panel" data-panel=${name} data-active=${active || null}>${children}</section>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Static placeholder for #remote on tunnel-served pages. Remote / device
|
|
26
|
+
// / tunnel management is loopback-only — the server returns 403 on
|
|
27
|
+
// every relevant endpoint — so even if a user navigates here via URL
|
|
28
|
+
// hash we render a clear "host machine only" message instead of a
|
|
29
|
+
// broken RemotePage spamming the console.
|
|
30
|
+
function RemoteHostOnlyPanel() {
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// Bounce back to whatever tab they were on before, after a brief
|
|
33
|
+
// moment so the message is readable.
|
|
34
|
+
const t = setTimeout(() => selectTab('sessions'), 2500);
|
|
35
|
+
return () => clearTimeout(t);
|
|
36
|
+
}, []);
|
|
37
|
+
return html`
|
|
38
|
+
<${PageTitleBar} title="Remote" />
|
|
39
|
+
<div class="settings-scroll">
|
|
40
|
+
<p class="remote-empty" style="margin-top:var(--s-6)">
|
|
41
|
+
Remote management is only available on the host machine.
|
|
42
|
+
Bouncing back to Sessions…
|
|
43
|
+
</p>
|
|
44
|
+
</div>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function App() {
|
|
48
|
+
const tab = activeTab.value;
|
|
49
|
+
const remoteLocked = tab === 'remote' && isRemoteAccess();
|
|
50
|
+
const mobile = isMobile.value;
|
|
51
|
+
const drawer = mobileDrawerOpen.value;
|
|
52
|
+
|
|
53
|
+
return html`
|
|
54
|
+
<div class=${`app${mobile ? ' is-mobile' : ''}${mobile && drawer ? ' drawer-open' : ''}`}>
|
|
55
|
+
<${Sidebar} />
|
|
56
|
+
<main class="main">
|
|
57
|
+
<div class="content">
|
|
58
|
+
${tab === 'sessions' ? html`<${Panel} name="sessions"><${SessionsPage} /></${Panel}>` : null}
|
|
59
|
+
${tab === 'launch' ? html`<${Panel} name="launch"><${LaunchPage} /></${Panel}>` : null}
|
|
60
|
+
${tab === 'configure' ? html`<${Panel} name="configure"><${ConfigurePage} /></${Panel}>` : null}
|
|
61
|
+
${tab === 'remote' && !remoteLocked ? html`<${Panel} name="remote"><${RemotePage} /></${Panel}>` : null}
|
|
62
|
+
${remoteLocked ? html`<${Panel} name="remote"><${RemoteHostOnlyPanel} /></${Panel}>` : null}
|
|
63
|
+
${tab === 'about' ? html`<${Panel} name="about"><${AboutPage} /></${Panel}>` : null}
|
|
64
|
+
</div>
|
|
65
|
+
</main>
|
|
66
|
+
<${Toast} />
|
|
67
|
+
<${DialogHost} />
|
|
68
|
+
<${HealthOverlay} />
|
|
69
|
+
<${RestartOverlay} />
|
|
70
|
+
<${PendingApprovalOverlay} />
|
|
71
|
+
<${MobileNavFab} />
|
|
72
|
+
</div>`;
|
|
73
|
+
}
|