cli-jaw 1.7.34 → 1.7.36
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/dist/bin/cli-jaw.js +5 -1
- package/dist/bin/cli-jaw.js.map +1 -1
- package/dist/bin/commands/dashboard.js +78 -0
- package/dist/bin/commands/dashboard.js.map +1 -0
- package/dist/lib/quota-copilot.js +5 -0
- package/dist/lib/quota-copilot.js.map +1 -1
- package/dist/src/agent/args.js +2 -4
- package/dist/src/agent/args.js.map +1 -1
- package/dist/src/agent/events.js +43 -1
- package/dist/src/agent/events.js.map +1 -1
- package/dist/src/manager/constants.js +9 -0
- package/dist/src/manager/constants.js.map +1 -0
- package/dist/src/manager/metadata.js +35 -0
- package/dist/src/manager/metadata.js.map +1 -0
- package/dist/src/manager/proxy.js +134 -0
- package/dist/src/manager/proxy.js.map +1 -0
- package/dist/src/manager/scan.js +111 -0
- package/dist/src/manager/scan.js.map +1 -0
- package/dist/src/manager/server.js +85 -0
- package/dist/src/manager/server.js.map +1 -0
- package/dist/src/manager/types.js +2 -0
- package/dist/src/manager/types.js.map +1 -0
- package/dist/src/routes/quota.js +134 -3
- package/dist/src/routes/quota.js.map +1 -1
- package/dist/src/routes/settings.js +5 -4
- package/dist/src/routes/settings.js.map +1 -1
- package/package.json +6 -1
- package/public/css/chat.css +27 -0
- package/public/css/diagram.css +48 -2
- package/public/dist/assets/api-Cl2oljO1.js +1 -0
- package/public/dist/assets/{index-wUWc2M5K.js → app-58Z9R8qM.js} +4 -4
- package/public/dist/assets/app-DWH_NZRE.css +1 -0
- package/public/dist/assets/architecture-YZFGNWBL-D4g_rKok.js +1 -0
- package/public/dist/assets/architectureDiagram-Q4EWVU46-DVbii_Pt.js +1 -0
- package/public/dist/assets/blockDiagram-DXYQGD6D-BStipezp.js +1 -0
- package/public/dist/assets/c4Diagram-AHTNJAMY-BYno4Snw.js +1 -0
- package/public/dist/assets/classDiagram-6PBFFD2Q-QzqRo_9X.js +1 -0
- package/public/dist/assets/classDiagram-v2-HSJHXN6E-CchGqJUH.js +1 -0
- package/public/dist/assets/{constants-BU8a_R5s.js → constants-fUXYKkoK.js} +1 -1
- package/public/dist/assets/cose-bilkent-S5V4N54A-TJS1iaqp.js +1 -0
- package/public/dist/assets/dagre-KV5264BT-C_DCJZ7t.js +1 -0
- package/public/dist/assets/diagram-5BDNPKRD-CKix1NRO.js +1 -0
- package/public/dist/assets/diagram-G4DWMVQ6-CoDTDNnx.js +1 -0
- package/public/dist/assets/diagram-MMDJMWI5-DYx0PwrY.js +1 -0
- package/public/dist/assets/diagram-TYMM5635-Bx1779dG.js +1 -0
- package/public/dist/assets/{employees-p53cgGmH.js → employees-CnA_uLbJ.js} +1 -1
- package/public/dist/assets/erDiagram-SMLLAGMA-C0INYE-d.js +1 -0
- package/public/dist/assets/flowDiagram-DWJPFMVM-DfQIgEdB.js +1 -0
- package/public/dist/assets/ganttDiagram-T4ZO3ILL-B8o_Hblo.js +1 -0
- package/public/dist/assets/gitGraph-7Q5UKJZL-DBMf1L33.js +1 -0
- package/public/dist/assets/gitGraphDiagram-UUTBAWPF-B2nqnLHi.js +1 -0
- package/public/dist/assets/idb-cache-C5ilDI6r.js +1 -0
- package/public/dist/assets/info-OMHHGYJF-C3gtg5Od.js +1 -0
- package/public/dist/assets/infoDiagram-42DDH7IO-Brz9iwrL.js +1 -0
- package/public/dist/assets/ishikawaDiagram-UXIWVN3A-C0YeQJh0.js +1 -0
- package/public/dist/assets/journeyDiagram-VCZTEJTY-BypDthzR.js +1 -0
- package/public/dist/assets/kanban-definition-6JOO6SKY-LkyDUgUP.js +1 -0
- package/public/dist/assets/katex-BL3z5Bli.js +1 -0
- package/public/dist/assets/manager-D4L7R_7R.css +1 -0
- package/public/dist/assets/manager-DtTAKPgY.js +9 -0
- package/public/dist/assets/memory-BN3grLR7.js +1 -0
- package/public/dist/assets/{memory-C2i7ZIvv.js → memory-RKpZbPTW.js} +1 -1
- package/public/dist/assets/mermaid.core-D6PDYVgC.js +1 -0
- package/public/dist/assets/mindmap-definition-QFDTVHPH-rV1Hipx-.js +1 -0
- package/public/dist/assets/packet-4T2RLAQJ-BuK6y2B7.js +1 -0
- package/public/dist/assets/pie-ZZUOXDRM-x8CPu972.js +1 -0
- package/public/dist/assets/pieDiagram-DEJITSTG-DpxKH4h4.js +1 -0
- package/public/dist/assets/quadrantDiagram-34T5L4WZ-BkRLuqoa.js +1 -0
- package/public/dist/assets/radar-PYXPWWZC-CsTAKlcV.js +1 -0
- package/public/dist/assets/render-D9bnLVR_.js +30 -0
- package/public/dist/assets/requirementDiagram-MS252O5E-BdqZlJ0O.js +1 -0
- package/public/dist/assets/sankeyDiagram-XADWPNL6-b3SwMAxC.js +1 -0
- package/public/dist/assets/sequenceDiagram-FGHM5R23-BmjI6n81.js +1 -0
- package/public/dist/assets/{settings-BUEiZgkm.js → settings-B8oUfDm4.js} +8 -8
- package/public/dist/assets/settings-DvGS6X_5.js +1 -0
- package/public/dist/assets/{skills-CSuSbBWa.js → skills-CHB_WsxF.js} +1 -1
- package/public/dist/assets/skills-WhnGcvvJ.js +1 -0
- package/public/dist/assets/{slash-commands-D-v0DlbY.js → slash-commands-CbsN2LqT.js} +1 -1
- package/public/dist/assets/slash-commands-DOh3zvJ7.js +1 -0
- package/public/dist/assets/stateDiagram-FHFEXIEX-DRsIDvF8.js +1 -0
- package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-IJMg9fYC.js +1 -0
- package/public/dist/assets/timeline-definition-GMOUNBTQ-B-0E30rh.js +1 -0
- package/public/dist/assets/treeView-SZITEDCU-CRgtpI0K.js +1 -0
- package/public/dist/assets/treemap-W4RFUUIX-DB5B-smF.js +1 -0
- package/public/dist/assets/ui-Cg8zuDaT.js +131 -0
- package/public/dist/assets/ui-HckqJaBX.js +1 -0
- package/public/dist/assets/{vendor-mermaid-UktBx7L0.js → vendor-mermaid-DHVxHqbi.js} +6 -6
- package/public/dist/assets/{vendor-render-D2YP6GiF.js → vendor-render-BaFgDHeH.js} +1 -1
- package/public/dist/assets/vennDiagram-DHZGUBPP-BBd9TC4D.js +1 -0
- package/public/dist/assets/wardley-RL74JXVD-BzCH0WF9.js +1 -0
- package/public/dist/assets/wardleyDiagram-NUSXRM2D-DgenAPEz.js +1 -0
- package/public/dist/assets/ws-DSntry14.js +14 -0
- package/public/dist/assets/xychartDiagram-5P7HB3ND-BmvcJ6sg.js +1 -0
- package/public/dist/index.html +2 -2
- package/public/dist/manager/index.html +14 -0
- package/public/js/diagram/iframe-renderer.ts +196 -77
- package/public/js/features/process-block.ts +2 -2
- package/public/js/features/settings-cli-status.ts +20 -1
- package/public/js/features/settings-types.ts +1 -1
- package/public/js/render.ts +16 -3
- package/public/js/ui.ts +42 -0
- package/public/js/ws.ts +20 -4
- package/public/manager/index.html +13 -0
- package/public/manager/src/App.tsx +185 -0
- package/public/manager/src/InstancePreview.tsx +72 -0
- package/public/manager/src/api.ts +8 -0
- package/public/manager/src/main.tsx +9 -0
- package/public/manager/src/preview.ts +35 -0
- package/public/manager/src/styles.css +309 -0
- package/public/manager/src/types.ts +44 -0
- package/public/dist/assets/api-COrKYKcO.js +0 -1
- package/public/dist/assets/architecture-YZFGNWBL-B2Nc8YC_.js +0 -1
- package/public/dist/assets/architectureDiagram-Q4EWVU46-EML5rtmP.js +0 -1
- package/public/dist/assets/blockDiagram-DXYQGD6D-Ds9bBl-N.js +0 -1
- package/public/dist/assets/c4Diagram-AHTNJAMY-BVpUT3Om.js +0 -1
- package/public/dist/assets/classDiagram-6PBFFD2Q-DG8IYzhr.js +0 -1
- package/public/dist/assets/classDiagram-v2-HSJHXN6E-B5j0d5gs.js +0 -1
- package/public/dist/assets/cose-bilkent-S5V4N54A-kNrexiHE.js +0 -1
- package/public/dist/assets/dagre-KV5264BT-Ylb1W0yQ.js +0 -1
- package/public/dist/assets/diagram-5BDNPKRD-CPxqMVrp.js +0 -1
- package/public/dist/assets/diagram-G4DWMVQ6-Dm1xfj8I.js +0 -1
- package/public/dist/assets/diagram-MMDJMWI5-VsjfxFEK.js +0 -1
- package/public/dist/assets/diagram-TYMM5635-CrMZri_r.js +0 -1
- package/public/dist/assets/erDiagram-SMLLAGMA-Felxus25.js +0 -1
- package/public/dist/assets/flowDiagram-DWJPFMVM-kQrkSkw2.js +0 -1
- package/public/dist/assets/ganttDiagram-T4ZO3ILL-CulD-LkD.js +0 -1
- package/public/dist/assets/gitGraph-7Q5UKJZL-DbbbZYM5.js +0 -1
- package/public/dist/assets/gitGraphDiagram-UUTBAWPF-oTUYJsKL.js +0 -1
- package/public/dist/assets/idb-cache-C7z4qE00.js +0 -1
- package/public/dist/assets/index-CLKLbGzn.css +0 -1
- package/public/dist/assets/info-OMHHGYJF-auV0JwD8.js +0 -1
- package/public/dist/assets/infoDiagram-42DDH7IO-B2XOJXrL.js +0 -1
- package/public/dist/assets/ishikawaDiagram-UXIWVN3A-CsmszxH_.js +0 -1
- package/public/dist/assets/journeyDiagram-VCZTEJTY-MmzEBROj.js +0 -1
- package/public/dist/assets/kanban-definition-6JOO6SKY-DcbxzAKu.js +0 -1
- package/public/dist/assets/katex-Bh3QhMKY.js +0 -1
- package/public/dist/assets/memory-6zLEr-qI.js +0 -1
- package/public/dist/assets/mermaid.core-CoRN09Dx.js +0 -1
- package/public/dist/assets/mindmap-definition-QFDTVHPH-CMqZWcUf.js +0 -1
- package/public/dist/assets/packet-4T2RLAQJ-C_PFKo5G.js +0 -1
- package/public/dist/assets/pie-ZZUOXDRM-CcqLxwbG.js +0 -1
- package/public/dist/assets/pieDiagram-DEJITSTG-C9r3BQ9f.js +0 -1
- package/public/dist/assets/quadrantDiagram-34T5L4WZ-Du4pEL4T.js +0 -1
- package/public/dist/assets/radar-PYXPWWZC-BWOF5omS.js +0 -1
- package/public/dist/assets/render-CulTuvJs.js +0 -30
- package/public/dist/assets/requirementDiagram-MS252O5E-Dyz9eEFP.js +0 -1
- package/public/dist/assets/sankeyDiagram-XADWPNL6-DLgUCKs2.js +0 -1
- package/public/dist/assets/sequenceDiagram-FGHM5R23-c0nA9K_Q.js +0 -1
- package/public/dist/assets/settings-BhrOslae.js +0 -1
- package/public/dist/assets/skills-CgwxEvFx.js +0 -1
- package/public/dist/assets/slash-commands-Bo8jvBfI.js +0 -1
- package/public/dist/assets/stateDiagram-FHFEXIEX-D2aVOdby.js +0 -1
- package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-DuwdzBLR.js +0 -1
- package/public/dist/assets/timeline-definition-GMOUNBTQ-PuaZYI31.js +0 -1
- package/public/dist/assets/treeView-SZITEDCU-D8mODguI.js +0 -1
- package/public/dist/assets/treemap-W4RFUUIX-DqqBuiM1.js +0 -1
- package/public/dist/assets/ui-4JiRyxJy.js +0 -131
- package/public/dist/assets/ui-Dx0MwI23.js +0 -1
- package/public/dist/assets/vennDiagram-DHZGUBPP-4blQ8E1z.js +0 -1
- package/public/dist/assets/wardley-RL74JXVD-Dn1IRCw2.js +0 -1
- package/public/dist/assets/wardleyDiagram-NUSXRM2D-OUBAKNJu.js +0 -1
- package/public/dist/assets/ws-DKtFfZsY.js +0 -14
- package/public/dist/assets/xychartDiagram-5P7HB3ND-CyZD7ovz.js +0 -1
- /package/public/dist/assets/{api-DygAf_G_.js → api-CnBOYa3F.js} +0 -0
- /package/public/dist/assets/{idb-cache-DbK81tgv.js → idb-cache-CjnC_K0x.js} +0 -0
- /package/public/dist/assets/{locale-CxI5nTcf.js → locale-heAuYK-3.js} +0 -0
- /package/public/dist/assets/{rolldown-runtime-FhOqtrmT.js → rolldown-runtime-DE9SaGGd.js} +0 -0
- /package/public/dist/assets/{state-O6NVkWcL.js → state-CPF7TImo.js} +0 -0
- /package/public/dist/assets/{vendor-icons-Bs4t7RP2.js → vendor-icons-D2l6ddC0.js} +0 -0
package/public/js/ws.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ── WebSocket Connection ──
|
|
2
2
|
import { state } from './state.js';
|
|
3
|
-
import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAgent, addMessage, showProcessStep, cleanupToolActivity, applyQueuedOverlay, hydrateActiveRun, reconcileChatBottomAfterRestore } from './ui.js';
|
|
3
|
+
import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAgent, addMessage, showProcessStep, cleanupToolActivity, applyQueuedOverlay, hydrateActiveRun, reconcileChatBottomAfterRestore, showChatRestoreIndicator } from './ui.js';
|
|
4
4
|
import { renderPendingQueue } from './features/pending-queue.js';
|
|
5
5
|
import { t, getLang } from './features/i18n.js';
|
|
6
6
|
import { getVirtualScroll } from './virtual-scroll.js';
|
|
@@ -118,20 +118,36 @@ export function syncOrchestrateSnapshot(reason = 'manual', options: { hydrateRun
|
|
|
118
118
|
return refreshRuntimeSnapshot(options);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function syncAfterBrowserRestore(reason: string): void {
|
|
122
|
+
showChatRestoreIndicator(reason);
|
|
123
|
+
syncOrchestrateSnapshot(reason)
|
|
124
|
+
.finally(() => {
|
|
125
|
+
reconcileChatBottomAfterRestore(reason);
|
|
126
|
+
})
|
|
127
|
+
.catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
function registerOrchestrateRestoreHooks(): void {
|
|
122
131
|
if (restoreHooksRegistered) return;
|
|
123
132
|
restoreHooksRegistered = true;
|
|
124
133
|
window.addEventListener('focus', () => {
|
|
125
|
-
|
|
134
|
+
syncAfterBrowserRestore('focus');
|
|
126
135
|
});
|
|
127
136
|
window.addEventListener('pageshow', () => {
|
|
128
|
-
|
|
137
|
+
syncAfterBrowserRestore('pageshow');
|
|
129
138
|
});
|
|
130
139
|
document.addEventListener('visibilitychange', () => {
|
|
131
140
|
if (document.visibilityState === 'visible') {
|
|
132
|
-
|
|
141
|
+
syncAfterBrowserRestore('visibilitychange');
|
|
133
142
|
}
|
|
134
143
|
});
|
|
144
|
+
document.addEventListener('resume', () => {
|
|
145
|
+
syncAfterBrowserRestore('resume');
|
|
146
|
+
});
|
|
147
|
+
if ('wasDiscarded' in document
|
|
148
|
+
&& Boolean((document as Document & { wasDiscarded?: boolean }).wasDiscarded)) {
|
|
149
|
+
syncAfterBrowserRestore('discard');
|
|
150
|
+
}
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
/** Hydrate agent phase cache from snapshot (used after reconnect) */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Jaw Manager</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="manager-root"></div>
|
|
10
|
+
<script type="module" src="/manager/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { fetchInstances } from './api';
|
|
3
|
+
import { InstancePreview } from './InstancePreview';
|
|
4
|
+
import type { DashboardInstance, DashboardInstanceStatus, DashboardPreviewMode, DashboardScanResult } from './types';
|
|
5
|
+
|
|
6
|
+
const STATUS_OPTIONS: Array<'all' | DashboardInstanceStatus> = ['all', 'online', 'offline', 'timeout', 'error', 'unknown'];
|
|
7
|
+
|
|
8
|
+
function formatUptime(seconds: number | null): string {
|
|
9
|
+
if (seconds == null) return 'n/a';
|
|
10
|
+
const minutes = Math.floor(seconds / 60);
|
|
11
|
+
if (minutes < 1) return `${Math.round(seconds)}s`;
|
|
12
|
+
const hours = Math.floor(minutes / 60);
|
|
13
|
+
if (hours < 1) return `${minutes}m`;
|
|
14
|
+
return `${hours}h ${minutes % 60}m`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function instanceLabel(instance: DashboardInstance): string {
|
|
18
|
+
return instance.instanceId || instance.homeDisplay || `port-${instance.port}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function statusClass(status: DashboardInstanceStatus): string {
|
|
22
|
+
return `status status-${status}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function App() {
|
|
26
|
+
const [data, setData] = useState<DashboardScanResult | null>(null);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
const [error, setError] = useState<string | null>(null);
|
|
29
|
+
const [query, setQuery] = useState('');
|
|
30
|
+
const [status, setStatus] = useState<'all' | DashboardInstanceStatus>('all');
|
|
31
|
+
const [selectedPort, setSelectedPort] = useState<number | null>(null);
|
|
32
|
+
const [previewMode, setPreviewMode] = useState<DashboardPreviewMode>('proxy');
|
|
33
|
+
const [previewEnabled, setPreviewEnabled] = useState(true);
|
|
34
|
+
|
|
35
|
+
async function load(): Promise<void> {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
try {
|
|
39
|
+
setData(await fetchInstances());
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError((err as Error).message);
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
void load();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const instances = data?.instances || [];
|
|
52
|
+
const summary = useMemo(() => {
|
|
53
|
+
return instances.reduce((acc, instance) => {
|
|
54
|
+
acc.total += 1;
|
|
55
|
+
acc[instance.status] = (acc[instance.status] || 0) + 1;
|
|
56
|
+
return acc;
|
|
57
|
+
}, { total: 0 } as Record<string, number>);
|
|
58
|
+
}, [instances]);
|
|
59
|
+
|
|
60
|
+
const filtered = useMemo(() => {
|
|
61
|
+
const needle = query.trim().toLowerCase();
|
|
62
|
+
return instances.filter((instance) => {
|
|
63
|
+
if (status !== 'all' && instance.status !== status) return false;
|
|
64
|
+
if (!needle) return true;
|
|
65
|
+
return [
|
|
66
|
+
String(instance.port),
|
|
67
|
+
instance.url,
|
|
68
|
+
instanceLabel(instance),
|
|
69
|
+
instance.version,
|
|
70
|
+
instance.workingDir,
|
|
71
|
+
instance.currentCli,
|
|
72
|
+
instance.currentModel,
|
|
73
|
+
instance.healthReason,
|
|
74
|
+
].some(value => String(value || '').toLowerCase().includes(needle));
|
|
75
|
+
});
|
|
76
|
+
}, [instances, query, status]);
|
|
77
|
+
|
|
78
|
+
const selectedInstance = useMemo(() => {
|
|
79
|
+
if (selectedPort == null) return filtered.find(instance => instance.ok) || null;
|
|
80
|
+
return instances.find(instance => instance.port === selectedPort) || null;
|
|
81
|
+
}, [filtered, instances, selectedPort]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<main className="dashboard-shell">
|
|
85
|
+
<header className="dashboard-topbar">
|
|
86
|
+
<div>
|
|
87
|
+
<p className="eyebrow">Jaw Manager</p>
|
|
88
|
+
<h1>Instance dashboard</h1>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="topbar-meta">
|
|
91
|
+
<span>Manager {data?.manager.port || 24576}</span>
|
|
92
|
+
<span>Scan {data ? `${data.manager.rangeFrom}-${data.manager.rangeTo}` : '3457-3506'}</span>
|
|
93
|
+
<button type="button" onClick={() => void load()} disabled={loading}>
|
|
94
|
+
{loading ? 'Scanning' : 'Refresh'}
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</header>
|
|
98
|
+
|
|
99
|
+
<section className="summary-grid" aria-label="Instance summary">
|
|
100
|
+
<div><span>Total</span><strong>{summary.total || 0}</strong></div>
|
|
101
|
+
<div><span>Online</span><strong>{summary.online || 0}</strong></div>
|
|
102
|
+
<div><span>Offline</span><strong>{summary.offline || 0}</strong></div>
|
|
103
|
+
<div><span>Timeout</span><strong>{summary.timeout || 0}</strong></div>
|
|
104
|
+
</section>
|
|
105
|
+
|
|
106
|
+
<section className="toolbar" aria-label="Filters">
|
|
107
|
+
<input
|
|
108
|
+
value={query}
|
|
109
|
+
onChange={event => setQuery(event.target.value)}
|
|
110
|
+
placeholder="Search port, home, CLI, model"
|
|
111
|
+
aria-label="Search instances"
|
|
112
|
+
/>
|
|
113
|
+
<select
|
|
114
|
+
value={status}
|
|
115
|
+
onChange={event => setStatus(event.target.value as 'all' | DashboardInstanceStatus)}
|
|
116
|
+
aria-label="Filter by status"
|
|
117
|
+
>
|
|
118
|
+
{STATUS_OPTIONS.map(option => <option key={option} value={option}>{option}</option>)}
|
|
119
|
+
</select>
|
|
120
|
+
</section>
|
|
121
|
+
|
|
122
|
+
{error && <section className="state error-state">Scan failed: {error}</section>}
|
|
123
|
+
{!error && loading && <section className="state">Scanning local Jaw instances...</section>}
|
|
124
|
+
{!error && !loading && filtered.length === 0 && (
|
|
125
|
+
<section className="state">No matching instances found.</section>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{!error && filtered.length > 0 && (
|
|
129
|
+
<section className="dashboard-layout">
|
|
130
|
+
<section className="instance-table" aria-label="Jaw instances">
|
|
131
|
+
<div className="table-head">
|
|
132
|
+
<span>Status</span>
|
|
133
|
+
<span>Instance</span>
|
|
134
|
+
<span>Runtime</span>
|
|
135
|
+
<span>Last checked</span>
|
|
136
|
+
<span>Action</span>
|
|
137
|
+
</div>
|
|
138
|
+
{filtered.map(instance => (
|
|
139
|
+
<article
|
|
140
|
+
className={`instance-row ${selectedInstance?.port === instance.port ? 'is-selected' : ''}`}
|
|
141
|
+
key={instance.port}
|
|
142
|
+
>
|
|
143
|
+
<div>
|
|
144
|
+
<span className={statusClass(instance.status)}>{instance.status}</span>
|
|
145
|
+
<span className="port">:{instance.port}</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div>
|
|
148
|
+
<strong>{instanceLabel(instance)}</strong>
|
|
149
|
+
<span>{instance.workingDir || instance.url}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div>
|
|
152
|
+
<span>{instance.currentCli || 'cli n/a'} / {instance.currentModel || 'model n/a'}</span>
|
|
153
|
+
<span>v{instance.version || 'n/a'} · {formatUptime(instance.uptime)}</span>
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<span>{new Date(instance.lastCheckedAt).toLocaleTimeString()}</span>
|
|
157
|
+
<span>{instance.healthReason || 'ok'}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="instance-actions">
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={() => setSelectedPort(instance.port)}
|
|
163
|
+
disabled={!instance.ok}
|
|
164
|
+
>
|
|
165
|
+
Preview
|
|
166
|
+
</button>
|
|
167
|
+
<a className="open-link" href={instance.url} target="_blank" rel="noreferrer">Open</a>
|
|
168
|
+
</div>
|
|
169
|
+
</article>
|
|
170
|
+
))}
|
|
171
|
+
</section>
|
|
172
|
+
|
|
173
|
+
<InstancePreview
|
|
174
|
+
instance={selectedInstance}
|
|
175
|
+
data={data}
|
|
176
|
+
mode={previewMode}
|
|
177
|
+
previewEnabled={previewEnabled}
|
|
178
|
+
onModeChange={setPreviewMode}
|
|
179
|
+
onPreviewEnabledChange={setPreviewEnabled}
|
|
180
|
+
/>
|
|
181
|
+
</section>
|
|
182
|
+
)}
|
|
183
|
+
</main>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buildPreviewState } from './preview';
|
|
2
|
+
import type { DashboardInstance, DashboardPreviewMode, DashboardScanResult } from './types';
|
|
3
|
+
|
|
4
|
+
type InstancePreviewProps = {
|
|
5
|
+
instance: DashboardInstance | null;
|
|
6
|
+
data: DashboardScanResult | null;
|
|
7
|
+
mode: DashboardPreviewMode;
|
|
8
|
+
previewEnabled: boolean;
|
|
9
|
+
onModeChange: (mode: DashboardPreviewMode) => void;
|
|
10
|
+
onPreviewEnabledChange: (enabled: boolean) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function InstancePreview(props: InstancePreviewProps) {
|
|
14
|
+
const state = buildPreviewState(
|
|
15
|
+
props.previewEnabled ? props.instance : null,
|
|
16
|
+
props.data,
|
|
17
|
+
props.mode,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<aside className="preview-panel" aria-label="Instance preview">
|
|
22
|
+
<div className="preview-header">
|
|
23
|
+
<div>
|
|
24
|
+
<p className="eyebrow">Preview</p>
|
|
25
|
+
<h2>{props.instance ? `:${props.instance.port}` : 'No instance selected'}</h2>
|
|
26
|
+
</div>
|
|
27
|
+
{props.instance && (
|
|
28
|
+
<a className="open-link" href={props.instance.url} target="_blank" rel="noreferrer">
|
|
29
|
+
Open
|
|
30
|
+
</a>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="preview-controls" aria-label="Preview controls">
|
|
35
|
+
<label className="preview-toggle">
|
|
36
|
+
<input
|
|
37
|
+
type="checkbox"
|
|
38
|
+
checked={props.previewEnabled}
|
|
39
|
+
onChange={event => props.onPreviewEnabledChange(event.target.checked)}
|
|
40
|
+
/>
|
|
41
|
+
Enable preview
|
|
42
|
+
</label>
|
|
43
|
+
|
|
44
|
+
<select
|
|
45
|
+
value={props.mode}
|
|
46
|
+
onChange={event => props.onModeChange(event.target.value as DashboardPreviewMode)}
|
|
47
|
+
aria-label="Preview mode"
|
|
48
|
+
>
|
|
49
|
+
<option value="proxy">Proxy preview</option>
|
|
50
|
+
<option value="direct">Direct iframe</option>
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{!state.canPreview && <div className="preview-empty">{state.reason}</div>}
|
|
55
|
+
|
|
56
|
+
{state.canPreview && state.src && (
|
|
57
|
+
<iframe
|
|
58
|
+
title={`Jaw instance ${props.instance?.port} preview`}
|
|
59
|
+
className="preview-frame"
|
|
60
|
+
src={state.src}
|
|
61
|
+
sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts"
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{props.mode === 'direct' && (
|
|
66
|
+
<p className="preview-note">
|
|
67
|
+
Preview may be blocked by frame policy. Open the instance in a new tab or use proxy preview.
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</aside>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DashboardScanResult } from './types';
|
|
2
|
+
|
|
3
|
+
export async function fetchInstances(): Promise<DashboardScanResult> {
|
|
4
|
+
const response = await fetch('/api/dashboard/instances');
|
|
5
|
+
if (!response.ok) throw new Error(`scan failed: ${response.status}`);
|
|
6
|
+
return await response.json() as DashboardScanResult;
|
|
7
|
+
}
|
|
8
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DashboardInstance, DashboardPreviewMode, DashboardScanResult } from './types';
|
|
2
|
+
|
|
3
|
+
export type PreviewState = {
|
|
4
|
+
canPreview: boolean;
|
|
5
|
+
src: string | null;
|
|
6
|
+
reason: string | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function buildPreviewState(
|
|
10
|
+
instance: DashboardInstance | null,
|
|
11
|
+
data: DashboardScanResult | null,
|
|
12
|
+
mode: DashboardPreviewMode,
|
|
13
|
+
): PreviewState {
|
|
14
|
+
if (!instance) {
|
|
15
|
+
return { canPreview: false, src: null, reason: 'Select an online instance to preview.' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!instance.ok) {
|
|
19
|
+
return { canPreview: false, src: null, reason: 'Preview is only available for online instances.' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (mode === 'proxy') {
|
|
23
|
+
const proxy = data?.manager.proxy;
|
|
24
|
+
if (!proxy?.enabled) {
|
|
25
|
+
return { canPreview: false, src: null, reason: 'Proxy preview is not available.' };
|
|
26
|
+
}
|
|
27
|
+
if (instance.port < proxy.allowedFrom || instance.port > proxy.allowedTo) {
|
|
28
|
+
return { canPreview: false, src: null, reason: 'This port is outside the proxy allowlist.' };
|
|
29
|
+
}
|
|
30
|
+
const basePath = proxy.basePath || '/i';
|
|
31
|
+
return { canPreview: true, src: `${basePath}/${instance.port}/`, reason: null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { canPreview: true, src: instance.url, reason: null };
|
|
35
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
font-family: "Outfit", "Geist", ui-sans-serif, system-ui, sans-serif;
|
|
4
|
+
background: #111318;
|
|
5
|
+
color: #eef1f5;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-width: 320px;
|
|
15
|
+
background: #111318;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
button,
|
|
19
|
+
input,
|
|
20
|
+
select {
|
|
21
|
+
font: inherit;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.dashboard-shell {
|
|
25
|
+
min-height: 100dvh;
|
|
26
|
+
padding: 18px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dashboard-topbar {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
gap: 16px;
|
|
34
|
+
padding-bottom: 16px;
|
|
35
|
+
border-bottom: 1px solid #2b303a;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.eyebrow {
|
|
39
|
+
margin: 0 0 4px;
|
|
40
|
+
color: #7fd7ff;
|
|
41
|
+
font-size: 12px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
h1 {
|
|
45
|
+
margin: 0;
|
|
46
|
+
font-size: 24px;
|
|
47
|
+
font-weight: 500;
|
|
48
|
+
letter-spacing: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.topbar-meta,
|
|
52
|
+
.toolbar {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 10px;
|
|
56
|
+
flex-wrap: wrap;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.topbar-meta span,
|
|
60
|
+
.summary-grid div,
|
|
61
|
+
.toolbar input,
|
|
62
|
+
.toolbar select,
|
|
63
|
+
.state,
|
|
64
|
+
.instance-row,
|
|
65
|
+
.table-head {
|
|
66
|
+
border: 1px solid #2b303a;
|
|
67
|
+
background: #181c24;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.topbar-meta span {
|
|
72
|
+
padding: 8px 10px;
|
|
73
|
+
color: #aab3c2;
|
|
74
|
+
font-size: 13px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
button,
|
|
78
|
+
.open-link {
|
|
79
|
+
min-height: 38px;
|
|
80
|
+
border: 1px solid #236987;
|
|
81
|
+
background: #123244;
|
|
82
|
+
color: #d7f4ff;
|
|
83
|
+
border-radius: 7px;
|
|
84
|
+
padding: 8px 12px;
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
display: inline-flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
button:disabled {
|
|
92
|
+
opacity: 0.55;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.summary-grid {
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
98
|
+
gap: 10px;
|
|
99
|
+
margin: 16px 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.summary-grid div {
|
|
103
|
+
padding: 12px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.summary-grid span,
|
|
107
|
+
.instance-row span,
|
|
108
|
+
.table-head {
|
|
109
|
+
color: #9aa3b2;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.summary-grid strong {
|
|
114
|
+
display: block;
|
|
115
|
+
margin-top: 6px;
|
|
116
|
+
font-size: 24px;
|
|
117
|
+
font-weight: 500;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.toolbar {
|
|
121
|
+
margin-bottom: 12px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.toolbar input,
|
|
125
|
+
.toolbar select {
|
|
126
|
+
min-height: 40px;
|
|
127
|
+
color: #eef1f5;
|
|
128
|
+
padding: 8px 10px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.toolbar input {
|
|
132
|
+
flex: 1;
|
|
133
|
+
min-width: 220px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.state {
|
|
137
|
+
padding: 18px;
|
|
138
|
+
color: #aab3c2;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.error-state {
|
|
142
|
+
border-color: #6c2d35;
|
|
143
|
+
color: #ffb9c0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.instance-table {
|
|
147
|
+
display: grid;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.dashboard-layout {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-columns: minmax(0, 1fr) minmax(320px, 42vw);
|
|
154
|
+
gap: 12px;
|
|
155
|
+
align-items: start;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.table-head,
|
|
159
|
+
.instance-row {
|
|
160
|
+
display: grid;
|
|
161
|
+
grid-template-columns: 150px minmax(180px, 1.4fr) minmax(180px, 1fr) minmax(160px, 0.9fr) 90px;
|
|
162
|
+
gap: 12px;
|
|
163
|
+
align-items: center;
|
|
164
|
+
padding: 10px 12px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.instance-row strong,
|
|
168
|
+
.instance-row span {
|
|
169
|
+
display: block;
|
|
170
|
+
overflow-wrap: anywhere;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.instance-row.is-selected {
|
|
174
|
+
border-color: #7fd7ff;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.instance-actions {
|
|
178
|
+
display: flex;
|
|
179
|
+
gap: 8px;
|
|
180
|
+
flex-wrap: wrap;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.status {
|
|
184
|
+
width: fit-content;
|
|
185
|
+
padding: 4px 8px;
|
|
186
|
+
border-radius: 999px;
|
|
187
|
+
color: #111318;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.status-online {
|
|
191
|
+
background: #65d58d;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.status-offline,
|
|
195
|
+
.status-unknown {
|
|
196
|
+
background: #858e9c;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.status-timeout {
|
|
200
|
+
background: #f0bd62;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.status-error {
|
|
204
|
+
background: #ef717b;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.port {
|
|
208
|
+
margin-top: 5px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.preview-panel {
|
|
212
|
+
position: sticky;
|
|
213
|
+
top: 12px;
|
|
214
|
+
border: 1px solid #2b303a;
|
|
215
|
+
background: #181c24;
|
|
216
|
+
border-radius: 8px;
|
|
217
|
+
padding: 12px;
|
|
218
|
+
min-height: 520px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.preview-header,
|
|
222
|
+
.preview-controls {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: space-between;
|
|
226
|
+
gap: 10px;
|
|
227
|
+
flex-wrap: wrap;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.preview-header h2 {
|
|
231
|
+
margin: 0;
|
|
232
|
+
font-size: 20px;
|
|
233
|
+
font-weight: 500;
|
|
234
|
+
letter-spacing: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.preview-controls {
|
|
238
|
+
margin: 12px 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.preview-controls select {
|
|
242
|
+
min-height: 40px;
|
|
243
|
+
color: #eef1f5;
|
|
244
|
+
padding: 8px 10px;
|
|
245
|
+
border: 1px solid #2b303a;
|
|
246
|
+
background: #181c24;
|
|
247
|
+
border-radius: 8px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.preview-toggle {
|
|
251
|
+
min-height: 40px;
|
|
252
|
+
display: inline-flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 8px;
|
|
255
|
+
color: #aab3c2;
|
|
256
|
+
font-size: 13px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.preview-frame {
|
|
260
|
+
width: 100%;
|
|
261
|
+
height: min(68dvh, 720px);
|
|
262
|
+
border: 1px solid #2b303a;
|
|
263
|
+
border-radius: 8px;
|
|
264
|
+
background: #0f1218;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.preview-empty {
|
|
268
|
+
min-height: 360px;
|
|
269
|
+
display: grid;
|
|
270
|
+
place-items: center;
|
|
271
|
+
border: 1px dashed #2b303a;
|
|
272
|
+
border-radius: 8px;
|
|
273
|
+
padding: 18px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.preview-empty,
|
|
277
|
+
.preview-note {
|
|
278
|
+
color: #aab3c2;
|
|
279
|
+
font-size: 13px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (max-width: 860px) {
|
|
283
|
+
.dashboard-topbar {
|
|
284
|
+
align-items: flex-start;
|
|
285
|
+
flex-direction: column;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.summary-grid {
|
|
289
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.table-head {
|
|
293
|
+
display: none;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.instance-row {
|
|
297
|
+
grid-template-columns: 1fr;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@media (max-width: 1080px) {
|
|
302
|
+
.dashboard-layout {
|
|
303
|
+
grid-template-columns: 1fr;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.preview-panel {
|
|
307
|
+
position: static;
|
|
308
|
+
}
|
|
309
|
+
}
|