@bakapiano/ccsm 0.8.4 → 0.10.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/CLAUDE.md +222 -195
- package/README.md +78 -80
- package/bin/ccsm.js +1 -1
- package/lib/cliSessionWatcher.js +249 -0
- package/lib/config.js +101 -19
- package/lib/folders.js +96 -0
- package/lib/localCliSessions.js +177 -0
- package/lib/persistedSessions.js +134 -0
- package/lib/webTerminal.js +48 -13
- package/lib/workspace.js +26 -4
- package/package.json +4 -4
- package/public/assets/claude-color.svg +1 -0
- package/public/assets/codex-color.svg +1 -0
- package/public/assets/copilot-color.svg +1 -0
- package/public/css/base.css +22 -5
- package/public/css/cards.css +37 -3
- package/public/css/feedback.css +127 -43
- package/public/css/forms.css +133 -10
- package/public/css/layout.css +79 -26
- package/public/css/modal.css +40 -26
- package/public/css/responsive.css +2 -2
- package/public/css/sidebar.css +456 -20
- package/public/css/terminals.css +182 -0
- package/public/css/tokens.css +28 -12
- package/public/css/wco.css +47 -19
- package/public/css/widgets.css +1177 -6
- package/public/index.html +39 -4
- package/public/js/api.js +194 -37
- package/public/js/components/AdoptModal.js +171 -0
- package/public/js/components/App.js +1 -11
- package/public/js/components/DirectoryPicker.js +203 -0
- package/public/js/components/EntityFormModal.js +105 -0
- package/public/js/components/Modal.js +51 -0
- package/public/js/components/OfflineBanner.js +29 -23
- package/public/js/components/PageTitleBar.js +13 -0
- package/public/js/components/Picker.js +179 -0
- package/public/js/components/Popover.js +55 -0
- package/public/js/components/Sidebar.js +244 -26
- package/public/js/components/TerminalView.js +192 -2
- package/public/js/components/useDragSort.js +67 -0
- package/public/js/dialog.js +10 -2
- package/public/js/icons.js +66 -3
- package/public/js/main.js +54 -3
- package/public/js/pages/AboutPage.js +81 -1
- package/public/js/pages/ConfigurePage.js +452 -159
- package/public/js/pages/LaunchPage.js +328 -76
- package/public/js/pages/SessionsPage.js +91 -41
- package/public/js/state.js +179 -35
- package/public/manifest.webmanifest +2 -2
- package/scripts/install.js +1 -1
- package/server.js +763 -407
- package/lib/favorites.js +0 -51
- package/lib/focus.js +0 -369
- package/lib/labels.js +0 -29
- package/lib/launcher.js +0 -219
- package/lib/sessions.js +0 -272
- package/lib/snapshot.js +0 -141
- package/public/js/actions.js +0 -87
- package/public/js/components/Fab.js +0 -11
- package/public/js/components/FavoritesTable.js +0 -81
- package/public/js/components/Footer.js +0 -12
- package/public/js/components/NewSessionModal.js +0 -142
- package/public/js/components/PageHead.js +0 -33
- package/public/js/components/Pagination.js +0 -27
- package/public/js/components/RecentTable.js +0 -68
- package/public/js/components/SessionsTable.js +0 -71
- package/public/js/components/SnapshotPanel.js +0 -77
- package/public/js/components/TitleCell.js +0 -40
- package/public/js/components/WorkspacesGrid.js +0 -41
- package/public/js/pages/TerminalsPage.js +0 -74
package/public/js/dialog.js
CHANGED
|
@@ -18,11 +18,19 @@ function push(entry) {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const CLOSE_X = html`
|
|
22
|
+
<button class="modal-close" type="button" aria-label="Close" data-action="cancel">
|
|
23
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
24
|
+
<line x1="3" y1="3" x2="13" y2="13"/>
|
|
25
|
+
<line x1="13" y1="3" x2="3" y2="13"/>
|
|
26
|
+
</svg>
|
|
27
|
+
</button>`;
|
|
28
|
+
|
|
21
29
|
export function ccsmConfirm(message, opts = {}) {
|
|
22
30
|
const { title = 'Confirm', okLabel = 'Confirm', cancelLabel = 'Cancel', danger = false } = opts;
|
|
23
31
|
return push({
|
|
24
32
|
render: () => html`<div class="modal modal-dialog">
|
|
25
|
-
<header class="modal-head"><h2>${title}</h2
|
|
33
|
+
<header class="modal-head"><h2>${title}</h2>${CLOSE_X}</header>
|
|
26
34
|
<div class="modal-body"><p class="dialog-msg">${message}</p></div>
|
|
27
35
|
<footer class="modal-foot">
|
|
28
36
|
<button class="action" data-action="cancel">${cancelLabel}</button>
|
|
@@ -37,7 +45,7 @@ export function ccsmPrompt(message, defaultValue = '', opts = {}) {
|
|
|
37
45
|
const { title, okLabel = 'Save', cancelLabel = 'Cancel', placeholder = '' } = opts;
|
|
38
46
|
return push({
|
|
39
47
|
render: () => html`<div class="modal modal-dialog">
|
|
40
|
-
<header class="modal-head"><h2>${title || message}</h2
|
|
48
|
+
<header class="modal-head"><h2>${title || message}</h2>${CLOSE_X}</header>
|
|
41
49
|
<div class="modal-body">
|
|
42
50
|
${title ? html`<p class="dialog-msg">${message}</p>` : null}
|
|
43
51
|
<input type="text" class="input" placeholder=${placeholder} value=${defaultValue} />
|
package/public/js/icons.js
CHANGED
|
@@ -29,7 +29,33 @@ export const IconRefresh = ic('0 0 24 24', html`
|
|
|
29
29
|
`, 16);
|
|
30
30
|
|
|
31
31
|
export const IconChevronLeft = ic('0 0 24 24', html`<polyline points="15 18 9 12 15 6"/>`, 14);
|
|
32
|
+
export const IconChevronRight = ic('0 0 24 24', html`<polyline points="9 18 15 12 9 6"/>`, 14);
|
|
33
|
+
export const IconChevronUp = ic('0 0 24 24', html`<polyline points="18 15 12 9 6 15"/>`, 14);
|
|
32
34
|
export const IconChevronDown = ic('0 0 24 24', html`<polyline points="6 9 12 15 18 9"/>`, 14);
|
|
35
|
+
export const IconArrowRight = ic('0 0 24 24', html`<line x1="5" y1="12" x2="19" y2="12"/><polyline points="13 6 19 12 13 18"/>`, 14);
|
|
36
|
+
export const IconHome = ic('0 0 24 24', html`
|
|
37
|
+
<path d="M3 11l9-8 9 8"/>
|
|
38
|
+
<path d="M5 10v10a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V10"/>
|
|
39
|
+
`, 14);
|
|
40
|
+
export const IconSparkle = ic('0 0 24 24', html`
|
|
41
|
+
<path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z"/>
|
|
42
|
+
<path d="M19 17l.8 1.6L21 20l-1.2.4L19 22l-.8-1.6L17 20l1.2-.4z"/>
|
|
43
|
+
`, 18);
|
|
44
|
+
// "Workspace" — stacked layers / cube. Used for the launch-page
|
|
45
|
+
// destination pill so it doesn't clash with the folder-tag pill that
|
|
46
|
+
// uses IconFolder.
|
|
47
|
+
export const IconWorkspace = ic('0 0 24 24', html`
|
|
48
|
+
<path d="M12 2l9 5-9 5-9-5z"/>
|
|
49
|
+
<path d="M3 12l9 5 9-5"/>
|
|
50
|
+
<path d="M3 17l9 5 9-5"/>
|
|
51
|
+
`, 16);
|
|
52
|
+
// Sidebar-toggle icon (panel-left). A rectangle with a vertical divider
|
|
53
|
+
// near the left — universally recognised "show/hide sidebar" affordance
|
|
54
|
+
// (Notion, Codex, Linear all use this shape).
|
|
55
|
+
export const IconSidebarToggle = ic('0 0 24 24', html`
|
|
56
|
+
<rect x="3" y="4" width="18" height="16" rx="2"/>
|
|
57
|
+
<line x1="9" y1="4" x2="9" y2="20"/>
|
|
58
|
+
`, 14);
|
|
33
59
|
|
|
34
60
|
export const IconSearch = ic('0 0 24 24', html`
|
|
35
61
|
<circle cx="11" cy="11" r="7"/>
|
|
@@ -46,6 +72,17 @@ export const IconPlus = ic('0 0 24 24', html`
|
|
|
46
72
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
47
73
|
`, 22);
|
|
48
74
|
|
|
75
|
+
// Folder + folder-open. Used in the sidebar session tree to mirror the
|
|
76
|
+
// icon-first style of the top nav items. Open variant for expanded
|
|
77
|
+
// folders so the chevron isn't doing double duty.
|
|
78
|
+
export const IconFolder = ic('0 0 24 24', html`
|
|
79
|
+
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>
|
|
80
|
+
`, 16);
|
|
81
|
+
export const IconFolderOpen = ic('0 0 24 24', html`
|
|
82
|
+
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v1H3V7z"/>
|
|
83
|
+
<path d="M3 10h18l-2 7a2 2 0 0 1-2 1.5H5A2 2 0 0 1 3 17V10z"/>
|
|
84
|
+
`, 16);
|
|
85
|
+
|
|
49
86
|
export const IconPencil = ic('0 0 24 24', html`
|
|
50
87
|
<path d="M12 20h9"/>
|
|
51
88
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
|
@@ -81,6 +118,31 @@ export const IconTerminal = ic('0 0 24 24', html`
|
|
|
81
118
|
<line x1="12" y1="19" x2="20" y2="19"/>
|
|
82
119
|
`, 18);
|
|
83
120
|
|
|
121
|
+
// Git branch — for repo selection
|
|
122
|
+
export const IconBranch = ic('0 0 24 24', html`
|
|
123
|
+
<line x1="6" y1="3" x2="6" y2="15"/>
|
|
124
|
+
<circle cx="18" cy="6" r="3"/>
|
|
125
|
+
<circle cx="6" cy="18" r="3"/>
|
|
126
|
+
<path d="M18 9a9 9 0 0 1-9 9"/>
|
|
127
|
+
`, 18);
|
|
128
|
+
|
|
129
|
+
// Brand-colored CLI marks. These use external SVG assets (full color),
|
|
130
|
+
// rendered as <img> so the gradients / fills in the file are preserved.
|
|
131
|
+
export const IconClaudeColor = () => html`
|
|
132
|
+
<img src="./assets/claude-color.svg" alt="" width="18" height="18" style="display:block" />`;
|
|
133
|
+
export const IconCodexColor = () => html`
|
|
134
|
+
<img src="./assets/codex-color.svg" alt="" width="18" height="18" style="display:block" />`;
|
|
135
|
+
export const IconCopilotColor = () => html`
|
|
136
|
+
<img src="./assets/copilot-color.svg" alt="" width="18" height="18" style="display:block" />`;
|
|
137
|
+
|
|
138
|
+
// Pick the right icon for a CLI based on its type field.
|
|
139
|
+
export const IconForCliType = (type) => {
|
|
140
|
+
if (type === 'claude') return IconClaudeColor;
|
|
141
|
+
if (type === 'codex') return IconCodexColor;
|
|
142
|
+
if (type === 'copilot') return IconCopilotColor;
|
|
143
|
+
return IconTerminal;
|
|
144
|
+
};
|
|
145
|
+
|
|
84
146
|
// Two variants used in the StarButton.
|
|
85
147
|
export const StarOutline = ({ size = 15 } = {}) => html`
|
|
86
148
|
<svg viewBox="0 0 24 24" width=${size} height=${size} fill="none" stroke="currentColor"
|
|
@@ -105,9 +167,10 @@ export const BrandMark = () => html`
|
|
|
105
167
|
<svg viewBox="0 0 32 32" width="32" height="32">
|
|
106
168
|
<rect x="2" y="4" width="28" height="24" rx="3" fill="#1a1815"/>
|
|
107
169
|
<line x1="2" y1="10" x2="30" y2="10" stroke="#faf9f5" stroke-width="0.6" opacity="0.45"/>
|
|
108
|
-
|
|
109
|
-
<circle cx="
|
|
110
|
-
<circle cx="
|
|
170
|
+
<!-- macOS traffic-light style: red / yellow / green -->
|
|
171
|
+
<circle cx="6" cy="7" r="1" fill="#ed6a5e"/>
|
|
172
|
+
<circle cx="9.5" cy="7" r="1" fill="#f4be4f"/>
|
|
173
|
+
<circle cx="13" cy="7" r="1" fill="#62c554"/>
|
|
111
174
|
<text x="16" y="19.5" text-anchor="middle" dominant-baseline="central"
|
|
112
175
|
font-family="'JetBrains Mono', 'Cascadia Mono', 'Consolas', monospace"
|
|
113
176
|
font-weight="700" font-size="10" fill="#faf9f5">ccsm</text>
|
package/public/js/main.js
CHANGED
|
@@ -4,13 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
import { render } from 'preact';
|
|
6
6
|
import { html } from './html.js';
|
|
7
|
-
import { loadPersisted, clockTick, lastRefreshAt, installPrompt, isInstalledPwa } from './state.js';
|
|
7
|
+
import { loadPersisted, clockTick, lastRefreshAt, installPrompt, isInstalledPwa, sidebarForcedCollapsed } from './state.js';
|
|
8
8
|
import { httpBase } from './backend.js';
|
|
9
|
-
import { loadConfig, refreshAll, loadSessions,
|
|
9
|
+
import { loadConfig, refreshAll, loadSessions, loadFolders, loadWorkspaces, pollHealth } from './api.js';
|
|
10
10
|
import { setToast } from './toast.js';
|
|
11
11
|
import { App } from './components/App.js';
|
|
12
12
|
|
|
13
13
|
loadPersisted();
|
|
14
|
+
// Pin the document title to "CCSM" — some Chromium builds will inject the
|
|
15
|
+
// current URL or path into the standalone window title bar if the page
|
|
16
|
+
// title is empty / changes; locking it here keeps the OS title bar text
|
|
17
|
+
// stable across navigation, tab switches, and PWA-install refresh.
|
|
18
|
+
const lockTitle = () => { if (document.title !== 'CCSM') document.title = 'CCSM'; };
|
|
19
|
+
lockTitle();
|
|
20
|
+
new MutationObserver(lockTitle).observe(
|
|
21
|
+
document.querySelector('title') || document.head,
|
|
22
|
+
{ childList: true, subtree: true, characterData: true }
|
|
23
|
+
);
|
|
14
24
|
render(html`<${App} />`, document.getElementById('app'));
|
|
15
25
|
|
|
16
26
|
// PWA install affordance — Chromium fires `beforeinstallprompt` when the
|
|
@@ -43,7 +53,22 @@ function applyIsAppClass() {
|
|
|
43
53
|
applyIsAppClass();
|
|
44
54
|
matchMedia('(display-mode: browser)').addEventListener('change', applyIsAppClass);
|
|
45
55
|
|
|
56
|
+
// Force-collapse the sidebar on narrow viewports. Mirrors the responsive
|
|
57
|
+
// CSS so JS state (toggle visibility, tree-render gating) agrees with the
|
|
58
|
+
// rendered layout.
|
|
59
|
+
const narrowMq = matchMedia('(max-width: 900px)');
|
|
60
|
+
function applyNarrow() { sidebarForcedCollapsed.value = narrowMq.matches; }
|
|
61
|
+
applyNarrow();
|
|
62
|
+
narrowMq.addEventListener('change', applyNarrow);
|
|
63
|
+
|
|
46
64
|
(async () => {
|
|
65
|
+
// Version-mismatch guard runs FIRST. If the user's backend has been
|
|
66
|
+
// upgraded since this per-version frontend was loaded, bounce back to
|
|
67
|
+
// the router immediately — no point loading config from a server that
|
|
68
|
+
// speaks a different API revision. Runs in dev too (it no-ops without
|
|
69
|
+
// the build-time <meta>).
|
|
70
|
+
await bootVersionGuard();
|
|
71
|
+
|
|
47
72
|
try {
|
|
48
73
|
await loadConfig();
|
|
49
74
|
await refreshAll();
|
|
@@ -61,7 +86,7 @@ matchMedia('(display-mode: browser)').addEventListener('change', applyIsAppClass
|
|
|
61
86
|
// move in/out of a workspace silently and the grid stays stale.
|
|
62
87
|
setInterval(async () => {
|
|
63
88
|
try {
|
|
64
|
-
await Promise.all([loadSessions(),
|
|
89
|
+
await Promise.all([loadSessions(), loadFolders(), loadWorkspaces()]);
|
|
65
90
|
lastRefreshAt.value = Date.now();
|
|
66
91
|
} catch { /* swallow — next tick retries */ }
|
|
67
92
|
pollHealth();
|
|
@@ -79,3 +104,29 @@ matchMedia('(display-mode: browser)').addEventListener('change', applyIsAppClass
|
|
|
79
104
|
setInterval(ping, 10_000);
|
|
80
105
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) ping(); });
|
|
81
106
|
})();
|
|
107
|
+
|
|
108
|
+
// ─── version routing guard ───────────────────────────────────────────
|
|
109
|
+
// Each deployed frontend is pinned to one backend version. The GH-Pages
|
|
110
|
+
// workflow bakes the version into <meta name="ccsm-frontend-version">
|
|
111
|
+
// so we can detect "backend has been upgraded since this frontend was
|
|
112
|
+
// loaded" and bounce back through the router at /ccsm/ for a fresh
|
|
113
|
+
// match. In dev (no meta tag, same-origin served-by-backend), the check
|
|
114
|
+
// no-ops — we're always running the frontend that ships with this
|
|
115
|
+
// backend by definition.
|
|
116
|
+
async function bootVersionGuard() {
|
|
117
|
+
const meta = document.querySelector('meta[name="ccsm-frontend-version"]');
|
|
118
|
+
if (!meta) return; // dev mode
|
|
119
|
+
const myVer = meta.getAttribute('content');
|
|
120
|
+
if (!myVer) return;
|
|
121
|
+
let backendVer = null;
|
|
122
|
+
try {
|
|
123
|
+
const r = await fetch(httpBase() + '/api/health', { cache: 'no-store' });
|
|
124
|
+
if (!r.ok) return;
|
|
125
|
+
backendVer = (await r.json()).version;
|
|
126
|
+
} catch { return; } // offline → OfflineBanner takes over
|
|
127
|
+
if (!backendVer || backendVer === myVer) return;
|
|
128
|
+
// Mismatch. Bounce up one level to the router. The router will
|
|
129
|
+
// probe /api/health again and redirect to ./<backendVer>/.
|
|
130
|
+
console.warn(`[ccsm] frontend ${myVer} ≠ backend ${backendVer} — re-routing`);
|
|
131
|
+
location.replace('../');
|
|
132
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { html } from '../html.js';
|
|
2
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
3
|
import { serverHealth, installPrompt, isInstalledPwa } from '../state.js';
|
|
3
4
|
import { setToast } from '../toast.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
4
6
|
import { Card } from '../components/Card.js';
|
|
7
|
+
import { PageTitleBar } from '../components/PageTitleBar.js';
|
|
5
8
|
import { BrandMark, IconGithub, IconExternal } from '../icons.js';
|
|
6
9
|
|
|
7
|
-
const REPO_URL = 'https://github.com/bakapiano/
|
|
10
|
+
const REPO_URL = 'https://github.com/bakapiano/ccsm';
|
|
8
11
|
const NPM_URL = 'https://www.npmjs.com/package/@bakapiano/ccsm';
|
|
9
12
|
|
|
10
13
|
async function onInstall() {
|
|
@@ -40,10 +43,87 @@ function InstallCard() {
|
|
|
40
43
|
</${Card}>`;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
function UpgradeCard() {
|
|
47
|
+
const [info, setInfo] = useState(null); // { current, latest, updateAvailable, fetchedAt, error? }
|
|
48
|
+
const [checking, setChecking] = useState(true);
|
|
49
|
+
const [upgrading, setUpgrading] = useState(false);
|
|
50
|
+
|
|
51
|
+
const refresh = async (force = false) => {
|
|
52
|
+
setChecking(true);
|
|
53
|
+
try {
|
|
54
|
+
const r = await api('GET', '/api/version' + (force ? '?refresh=1' : ''));
|
|
55
|
+
setInfo(r);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
setInfo({ error: e.message });
|
|
58
|
+
} finally {
|
|
59
|
+
setChecking(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
useEffect(() => { refresh(false); }, []);
|
|
63
|
+
|
|
64
|
+
const onUpgrade = async () => {
|
|
65
|
+
if (!info?.updateAvailable) return;
|
|
66
|
+
setUpgrading(true);
|
|
67
|
+
try {
|
|
68
|
+
await api('POST', '/api/upgrade', { target: 'latest' });
|
|
69
|
+
setToast(`upgrading to v${info.latest} · backend will restart`);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
setUpgrading(false);
|
|
72
|
+
setToast(e.message, 'error');
|
|
73
|
+
}
|
|
74
|
+
// No "finally" reset — the server is about to shut down, and the
|
|
75
|
+
// OfflineBanner takes over UI. When the router reroutes us to the new
|
|
76
|
+
// version's frontend, this component re-mounts fresh.
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const current = info?.current || serverHealth.value.version || '';
|
|
80
|
+
const latest = info?.latest;
|
|
81
|
+
const updateAvailable = !!info?.updateAvailable;
|
|
82
|
+
|
|
83
|
+
return html`
|
|
84
|
+
<${Card} title="Version">
|
|
85
|
+
<div class="about-version-row">
|
|
86
|
+
<div>
|
|
87
|
+
<div class="about-version-line">
|
|
88
|
+
Installed · <span class="mono">v${current || '?'}</span>
|
|
89
|
+
</div>
|
|
90
|
+
${latest && !updateAvailable ? html`
|
|
91
|
+
<div class="muted-text" style="margin-top:4px">You're on the latest release.</div>
|
|
92
|
+
` : null}
|
|
93
|
+
${updateAvailable ? html`
|
|
94
|
+
<div class="about-update-line">
|
|
95
|
+
Update available · <span class="mono">v${latest}</span>
|
|
96
|
+
</div>
|
|
97
|
+
` : null}
|
|
98
|
+
${info?.error ? html`
|
|
99
|
+
<div class="muted-text" style="margin-top:4px">Couldn't reach npm registry.</div>
|
|
100
|
+
` : null}
|
|
101
|
+
</div>
|
|
102
|
+
<div class="about-version-actions">
|
|
103
|
+
<button class="action subtle" onClick=${() => refresh(true)} disabled=${checking || upgrading}>
|
|
104
|
+
${checking ? 'Checking…' : 'Check'}
|
|
105
|
+
</button>
|
|
106
|
+
${updateAvailable ? html`
|
|
107
|
+
<button class="action primary" onClick=${onUpgrade} disabled=${upgrading}>
|
|
108
|
+
${upgrading ? 'Upgrading…' : `Upgrade to v${latest}`}
|
|
109
|
+
</button>
|
|
110
|
+
` : null}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
${upgrading ? html`
|
|
114
|
+
<p class="muted-text" style="margin-top:var(--s-3)">
|
|
115
|
+
Running <code>npm i -g @bakapiano/ccsm@latest</code>. The backend will restart automatically — you'll see the "Backend not running" screen briefly.
|
|
116
|
+
</p>
|
|
117
|
+
` : null}
|
|
118
|
+
</${Card}>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
43
121
|
export function AboutPage() {
|
|
44
122
|
const version = serverHealth.value.version;
|
|
45
123
|
|
|
46
124
|
return html`
|
|
125
|
+
<${PageTitleBar} title="About" />
|
|
126
|
+
<${UpgradeCard} />
|
|
47
127
|
<${InstallCard} />
|
|
48
128
|
<${Card} title="ccsm">
|
|
49
129
|
<div class="about-block">
|