@hanzlaa/rcode 3.5.0 → 3.6.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/package.json +7 -1
- package/server/dashboard.js +105 -3
- package/server/lib/html/client/agents-data.js +27 -0
- package/server/lib/html/client/app.js +15 -0
- package/server/lib/html/client/components/App.js +211 -0
- package/server/lib/html/client/components/OrchPanel.js +293 -0
- package/server/lib/html/client/components/Sidebar.js +73 -0
- package/server/lib/html/client/components/Topbar.js +53 -0
- package/server/lib/html/client/components/XtermPanel.js +220 -0
- package/server/lib/html/client/components/shared.js +330 -0
- package/server/lib/html/client/icons-client.js +85 -0
- package/server/lib/html/client/orchestrator.js +279 -0
- package/server/lib/html/client/preact.js +34 -0
- package/server/lib/html/client/store.js +91 -0
- package/server/lib/html/client/util.js +186 -0
- package/server/lib/html/client/views/AgentsView.js +83 -0
- package/server/lib/html/client/views/DecisionsView.js +102 -0
- package/server/lib/html/client/views/FilesView.js +223 -0
- package/server/lib/html/client/views/KanbanView.js +236 -0
- package/server/lib/html/client/views/MemoryView.js +157 -0
- package/server/lib/html/client/views/MilestonesView.js +136 -0
- package/server/lib/html/client/views/OrchestrationView.js +167 -0
- package/server/lib/html/client/views/OverviewView.js +221 -0
- package/server/lib/html/client/views/PhasesView.js +184 -0
- package/server/lib/html/client/views/RoadmapView.js +238 -0
- package/server/lib/html/client/views/SprintsView.js +178 -0
- package/server/lib/html/client/views/TasksView.js +148 -0
- package/server/lib/html/client.js +41 -1775
- package/server/lib/html/css.js +264 -44
- package/server/lib/html/icons.js +68 -0
- package/server/lib/html/shell.js +9 -296
- package/server/lib/scanner.js +76 -0
- package/server/orchestrator.js +237 -313
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "rcode — the AI team that never forgets. Persistent memory, specialist agents, and slash commands for AI IDEs. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -74,5 +74,11 @@
|
|
|
74
74
|
},
|
|
75
75
|
"publishConfig": {
|
|
76
76
|
"access": "public"
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"ws": "^8.20.1"
|
|
80
|
+
},
|
|
81
|
+
"optionalDependencies": {
|
|
82
|
+
"@lydell/node-pty": "1.2.0-beta.12"
|
|
77
83
|
}
|
|
78
84
|
}
|
package/server/dashboard.js
CHANGED
|
@@ -22,9 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|
const http = require('http');
|
|
24
24
|
const path = require('path');
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
25
27
|
const crypto = require('crypto');
|
|
26
28
|
const { spawn } = require('child_process');
|
|
27
29
|
|
|
30
|
+
// Client JS modules live here and are served verbatim at /js/<name>.js
|
|
31
|
+
const CLIENT_DIR = path.join(__dirname, 'lib', 'html', 'client');
|
|
32
|
+
|
|
28
33
|
const { scanState } = require('./lib/scanner');
|
|
29
34
|
const { handleApiState, handleApiFiles, handleApiFile, handleApiHierarchy, handleApiMemory } = require('./lib/api');
|
|
30
35
|
const { renderHtml } = require('./lib/html/shell');
|
|
@@ -34,8 +39,25 @@ const PORT = parseInt(process.env.PORT || '7717', 10);
|
|
|
34
39
|
const RIHAL_DIR = process.env.RIHAL_DIR || path.join(process.cwd(), '.rihal');
|
|
35
40
|
const PROJECT_ROOT = path.dirname(RIHAL_DIR);
|
|
36
41
|
|
|
37
|
-
// Shared orchestrator token —
|
|
38
|
-
|
|
42
|
+
// Shared orchestrator token — passed to the orchestrator via env and embedded
|
|
43
|
+
// in the HTML. Persisted to ~/.rihal/orch-token so it stays STABLE across
|
|
44
|
+
// dashboard restarts; otherwise every restart invalidates the token baked
|
|
45
|
+
// into already-open browser tabs and their API calls 401.
|
|
46
|
+
function loadOrchToken() {
|
|
47
|
+
if (process.env.ORCH_TOKEN) return process.env.ORCH_TOKEN;
|
|
48
|
+
const tokenFile = path.join(os.homedir(), '.rihal', 'orch-token');
|
|
49
|
+
try {
|
|
50
|
+
const existing = fs.readFileSync(tokenFile, 'utf8').trim();
|
|
51
|
+
if (existing) return existing;
|
|
52
|
+
} catch { /* not yet created */ }
|
|
53
|
+
const token = crypto.randomBytes(24).toString('hex');
|
|
54
|
+
try {
|
|
55
|
+
fs.mkdirSync(path.dirname(tokenFile), { recursive: true });
|
|
56
|
+
fs.writeFileSync(tokenFile, token, { mode: 0o600 });
|
|
57
|
+
} catch { /* non-fatal — fall back to an in-memory token */ }
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
const ORCH_TOKEN = loadOrchToken();
|
|
39
61
|
|
|
40
62
|
// ---------- HTTP Server ----------
|
|
41
63
|
const server = http.createServer((req, res) => {
|
|
@@ -72,6 +94,37 @@ const server = http.createServer((req, res) => {
|
|
|
72
94
|
return;
|
|
73
95
|
}
|
|
74
96
|
|
|
97
|
+
// Lets the client fetch the current orchestrator token at runtime, so a
|
|
98
|
+
// long-open tab can self-heal instead of 401'ing if the token ever drifts.
|
|
99
|
+
if (url === '/api/orch-token') {
|
|
100
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
101
|
+
res.end(JSON.stringify({ token: ORCH_TOKEN }));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (url.startsWith('/js/')) {
|
|
106
|
+
const name = url.slice(4).split('?')[0];
|
|
107
|
+
// Allow exactly one optional subdirectory (e.g. components/App.js, views/Foo.js)
|
|
108
|
+
// while still rejecting traversal attempts. The regex blocks `..`, encoded
|
|
109
|
+
// separators, and anything other than word chars, dots, hyphens, and one `/`.
|
|
110
|
+
if (!/^(?:[\w.-]+\/)?[\w.-]+\.js$/.test(name)) { res.writeHead(404); res.end('Not found'); return; }
|
|
111
|
+
// Defense-in-depth: resolved path must stay inside CLIENT_DIR even after
|
|
112
|
+
// any OS-level resolution (handles encoded traversal the regex might miss).
|
|
113
|
+
const resolved = path.resolve(CLIENT_DIR, name);
|
|
114
|
+
if (!resolved.startsWith(CLIENT_DIR + path.sep) && resolved !== CLIENT_DIR) {
|
|
115
|
+
res.writeHead(403); res.end('Forbidden'); return;
|
|
116
|
+
}
|
|
117
|
+
fs.readFile(resolved, (err, data) => {
|
|
118
|
+
if (err) { res.writeHead(404); res.end('Not found'); return; }
|
|
119
|
+
res.writeHead(200, {
|
|
120
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
121
|
+
'Cache-Control': 'no-cache',
|
|
122
|
+
});
|
|
123
|
+
res.end(data);
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
75
128
|
if (url === '/' || url === '/index.html') {
|
|
76
129
|
const state = scanState(RIHAL_DIR);
|
|
77
130
|
const html = renderHtml(state, ORCH_TOKEN);
|
|
@@ -96,6 +149,54 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
96
149
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
97
150
|
});
|
|
98
151
|
|
|
152
|
+
// ── Ensure interactive-terminal native module is present ─────────
|
|
153
|
+
// @lydell/node-pty is an optionalDependency, so it can be absent if the
|
|
154
|
+
// package was installed with --omit=optional or a partial CI install.
|
|
155
|
+
// It ships prebuilt binaries (no node-gyp), so fetching it is a fast,
|
|
156
|
+
// no-compile, one-time step. Runs async — the dashboard never blocks; the
|
|
157
|
+
// orchestrator is spawned via the callback once the install settles.
|
|
158
|
+
// Failure is non-fatal: the terminal just degrades with a clear message.
|
|
159
|
+
function ensurePty(done) {
|
|
160
|
+
try { require.resolve('@lydell/node-pty'); done(); return; } catch {}
|
|
161
|
+
|
|
162
|
+
const pkgRoot = path.join(__dirname, '..');
|
|
163
|
+
|
|
164
|
+
// @lydell/node-pty is already declared in optionalDependencies, so a plain
|
|
165
|
+
// lockfile-respecting `install` pulls it in without mutating package.json.
|
|
166
|
+
// Use pnpm when the repo is pnpm-managed — `npm install` fights pnpm's
|
|
167
|
+
// symlinked node_modules and stalls. End-user installs use npm.
|
|
168
|
+
const usePnpm = fs.existsSync(path.join(pkgRoot, 'pnpm-lock.yaml'));
|
|
169
|
+
const cmd = usePnpm ? 'pnpm' : 'npm';
|
|
170
|
+
const args = usePnpm
|
|
171
|
+
? ['install', '--ignore-scripts']
|
|
172
|
+
: ['install', '--ignore-scripts', '--no-audit', '--no-fund'];
|
|
173
|
+
|
|
174
|
+
console.log('[setup] Installing interactive-terminal support (@lydell/node-pty)…');
|
|
175
|
+
let settled = false;
|
|
176
|
+
const finish = (ok) => {
|
|
177
|
+
if (settled) return;
|
|
178
|
+
settled = true;
|
|
179
|
+
console.log(ok ? '[setup] Interactive terminal ready.'
|
|
180
|
+
: '[setup] node-pty install incomplete — terminal stays unavailable.');
|
|
181
|
+
done();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
let child;
|
|
185
|
+
try {
|
|
186
|
+
child = spawn(cmd, args, {
|
|
187
|
+
cwd: pkgRoot, stdio: 'inherit', shell: process.platform === 'win32',
|
|
188
|
+
});
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.log('[setup] node-pty install could not start:', err.message);
|
|
191
|
+
finish(false);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const timer = setTimeout(() => { try { child.kill(); } catch {} }, 180000);
|
|
195
|
+
child.on('exit', code => { clearTimeout(timer); finish(code === 0); });
|
|
196
|
+
child.on('error', err => { clearTimeout(timer);
|
|
197
|
+
console.log('[setup] node-pty install error:', err.message); finish(false); });
|
|
198
|
+
}
|
|
199
|
+
|
|
99
200
|
// ── Auto-spawn orchestrator (port 7718) ──────────────────────────
|
|
100
201
|
const ORCH_BIN = path.join(__dirname, 'orchestrator.js');
|
|
101
202
|
let _orchProc = null;
|
|
@@ -132,7 +233,8 @@ function spawnOrchestrator() {
|
|
|
132
233
|
}
|
|
133
234
|
}
|
|
134
235
|
|
|
135
|
-
|
|
236
|
+
// Orchestrator spawns only once node-pty is settled (present or installed).
|
|
237
|
+
ensurePty(spawnOrchestrator);
|
|
136
238
|
|
|
137
239
|
// Graceful shutdown
|
|
138
240
|
function shutdown() {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents-data.js — the 18-agent roster, moved client-side from shell.js.
|
|
3
|
+
*
|
|
4
|
+
* Previously lived in shell.js:17-36 as a server-rendered array.
|
|
5
|
+
* Now exported as a pure ESM constant so AgentsView can render it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const AGENTS = [
|
|
9
|
+
{ name: 'Sadiq Damani', arabic: 'صادق', role: 'Director of Strategy', real: true, type: 'leadership' },
|
|
10
|
+
{ name: 'Waleed Al Harthi', arabic: 'وليد', role: 'CTO', real: true, type: 'leadership' },
|
|
11
|
+
{ name: 'Ahmed Al Hassani', arabic: 'أحمد الحسني', role: 'Technology & Development Director', real: true, type: 'leadership' },
|
|
12
|
+
{ name: 'Nasser', arabic: 'ناصر', role: 'Engineering Manager', real: true, type: 'leadership' },
|
|
13
|
+
{ name: 'Hussain', arabic: 'حسين', role: 'PM + Scrum Master', type: 'product' },
|
|
14
|
+
{ name: 'Layla', arabic: 'ليلى', role: 'Lead UX Designer', type: 'design' },
|
|
15
|
+
{ name: 'Zahra', arabic: 'زهرة', role: 'Branding & Creative Director', type: 'design' },
|
|
16
|
+
{ name: 'Omar', arabic: 'عمر', role: 'Full-Stack Engineer', type: 'engineering' },
|
|
17
|
+
{ name: 'Haitham Al Khamiyasi', arabic: 'هيثم', role: 'Senior Frontend', real: true, type: 'engineering' },
|
|
18
|
+
{ name: 'Yousef', arabic: 'يوسف', role: 'Senior Backend', type: 'engineering' },
|
|
19
|
+
{ name: 'Zayd', arabic: 'زيد', role: 'ML Engineer', type: 'engineering' },
|
|
20
|
+
{ name: 'Fatima', arabic: 'فاطمة', role: 'QA Lead', type: 'quality' },
|
|
21
|
+
{ name: 'Khalid', arabic: 'خالد', role: 'DevOps', type: 'engineering' },
|
|
22
|
+
{ name: 'Noor', arabic: 'نور', role: 'Scribe', type: 'support' },
|
|
23
|
+
{ name: 'Mariam', arabic: 'مريم', role: 'Marketing Lead', type: 'product' },
|
|
24
|
+
{ name: 'Raees', arabic: 'رئيس', role: 'Orchestration Director', type: 'system' },
|
|
25
|
+
{ name: 'Majlis', arabic: 'مجلس', role: 'Consulting Council', type: 'system' },
|
|
26
|
+
{ name: 'Diwan', arabic: 'ديوان', role: 'Dashboard Registry', type: 'system' },
|
|
27
|
+
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESM entry point — mounts the Preact App into #app-root.
|
|
3
|
+
*
|
|
4
|
+
* Loaded via <script type="module" src="/js/app.js"> from client.js,
|
|
5
|
+
* AFTER the legacy <script src> modules (which fill the 10 un-migrated
|
|
6
|
+
* view host divs). Legacy modules remain active during coexistence phase.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { render, html } from './preact.js';
|
|
10
|
+
import { App } from './components/App.js';
|
|
11
|
+
|
|
12
|
+
const root = document.getElementById('app-root');
|
|
13
|
+
if (root) {
|
|
14
|
+
render(html`<${App}/>`, root);
|
|
15
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — root Preact component.
|
|
3
|
+
*
|
|
4
|
+
* Owns:
|
|
5
|
+
* - Hash router (view + subId state, hashchange listener)
|
|
6
|
+
* - Layout: Sidebar + content area + Topbar + all 12 Preact view components
|
|
7
|
+
* - 30s auto-refresh: polls /api/state, diffs lastScanned, calls setState
|
|
8
|
+
* - Theme toggle: reads/persists localStorage('majlis-theme')
|
|
9
|
+
*
|
|
10
|
+
* Sprint 31.4 completed the Preact migration. All 12 views are Preact
|
|
11
|
+
* components. Legacy client-main.js, client-render.js, and client-kanban.js
|
|
12
|
+
* are deleted. No coexistence seam remains.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { html, useState, useEffect, useRef, useCallback } from '../preact.js';
|
|
16
|
+
import { getState, setState, subscribe } from '../store.js';
|
|
17
|
+
import { startSessionsPoll, refreshOrchToken } from '../orchestrator.js';
|
|
18
|
+
import { Sidebar } from './Sidebar.js';
|
|
19
|
+
import { Topbar } from './Topbar.js';
|
|
20
|
+
import { XtermPanel } from './XtermPanel.js';
|
|
21
|
+
import { OrchPanel } from './OrchPanel.js';
|
|
22
|
+
import { OverviewView } from '../views/OverviewView.js';
|
|
23
|
+
import { DecisionsView } from '../views/DecisionsView.js';
|
|
24
|
+
import { RoadmapView } from '../views/RoadmapView.js';
|
|
25
|
+
import { MilestonesView } from '../views/MilestonesView.js';
|
|
26
|
+
import { PhasesView } from '../views/PhasesView.js';
|
|
27
|
+
import { SprintsView } from '../views/SprintsView.js';
|
|
28
|
+
import { TasksView } from '../views/TasksView.js';
|
|
29
|
+
import { KanbanView } from '../views/KanbanView.js';
|
|
30
|
+
import { FilesView } from '../views/FilesView.js';
|
|
31
|
+
import { AgentsView } from '../views/AgentsView.js';
|
|
32
|
+
import { MemoryView } from '../views/MemoryView.js';
|
|
33
|
+
import { OrchestrationView } from '../views/OrchestrationView.js';
|
|
34
|
+
|
|
35
|
+
// Views served by Preact components (migrated)
|
|
36
|
+
// Sprint 31.4: +orchestration → all 12 views Preact. Migration complete.
|
|
37
|
+
const PREACT_VIEWS = {
|
|
38
|
+
overview: OverviewView,
|
|
39
|
+
decisions: DecisionsView,
|
|
40
|
+
roadmap: RoadmapView,
|
|
41
|
+
milestones: MilestonesView,
|
|
42
|
+
phases: PhasesView,
|
|
43
|
+
sprints: SprintsView,
|
|
44
|
+
tasks: TasksView,
|
|
45
|
+
kanban: KanbanView,
|
|
46
|
+
files: FilesView,
|
|
47
|
+
agents: AgentsView,
|
|
48
|
+
memory: MemoryView,
|
|
49
|
+
orchestration: OrchestrationView,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// All views are now Preact — no legacy placeholder hosts needed.
|
|
53
|
+
const LEGACY_VIEWS = [];
|
|
54
|
+
|
|
55
|
+
const ALL_VIEWS = Object.keys(PREACT_VIEWS).concat(LEGACY_VIEWS);
|
|
56
|
+
|
|
57
|
+
/** Parse location.hash into { view, subId } — port of client-main.js:45-49. */
|
|
58
|
+
function parseHash() {
|
|
59
|
+
const raw = location.hash.slice(1) || 'overview';
|
|
60
|
+
const slash = raw.indexOf('/');
|
|
61
|
+
const view = slash === -1 ? raw : raw.slice(0, slash);
|
|
62
|
+
const subId = slash === -1 ? null : raw.slice(slash + 1);
|
|
63
|
+
// #263: unknown hash falls back to overview
|
|
64
|
+
const resolvedView = ALL_VIEWS.includes(view) ? view : 'overview';
|
|
65
|
+
return { view: resolvedView, subId };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Root App component. No props needed — reads everything from the store. */
|
|
69
|
+
export function App() {
|
|
70
|
+
// ---- Router state ----
|
|
71
|
+
const [{ view, subId }, setRoute] = useState(parseHash);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
function onHashChange() { setRoute(parseHash()); }
|
|
75
|
+
window.addEventListener('hashchange', onHashChange);
|
|
76
|
+
return () => window.removeEventListener('hashchange', onHashChange);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// ---- Store state (for projectName and pass-through to views) ----
|
|
80
|
+
const [storeState, setStoreState] = useState(getState);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const unsub = subscribe(newState => setStoreState({ ...newState }));
|
|
83
|
+
return unsub;
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
// ---- Theme ----
|
|
87
|
+
const [theme, setTheme] = useState(() => {
|
|
88
|
+
const saved = localStorage.getItem('majlis-theme') || 'dark';
|
|
89
|
+
if (saved === 'light') {
|
|
90
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
91
|
+
}
|
|
92
|
+
return saved;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const toggleTheme = useCallback(() => {
|
|
96
|
+
const next = theme === 'light' ? 'dark' : 'light';
|
|
97
|
+
document.documentElement.setAttribute('data-theme', next === 'dark' ? '' : next);
|
|
98
|
+
localStorage.setItem('majlis-theme', next);
|
|
99
|
+
setTheme(next);
|
|
100
|
+
}, [theme]);
|
|
101
|
+
|
|
102
|
+
// ---- Sidebar collapse ----
|
|
103
|
+
const toggleSidebar = useCallback(() => {
|
|
104
|
+
const sidebar = document.querySelector('.sidebar');
|
|
105
|
+
const backdrop = document.getElementById('sidebar-backdrop');
|
|
106
|
+
if (!sidebar) return;
|
|
107
|
+
const open = sidebar.classList.toggle('sidebar-open');
|
|
108
|
+
if (backdrop) backdrop.classList.toggle('active', open);
|
|
109
|
+
document.body.classList.toggle('sidebar-visible', open);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// ---- Updated-ago display ----
|
|
113
|
+
const [updatedAgo, setUpdatedAgo] = useState('just now');
|
|
114
|
+
const scanTimeRef = useRef(Date.now());
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const id = setInterval(() => {
|
|
118
|
+
const s = Math.floor((Date.now() - scanTimeRef.current) / 1000);
|
|
119
|
+
setUpdatedAgo(s < 5 ? 'just now' : s < 60 ? s + 's ago' : Math.floor(s / 60) + 'm ago');
|
|
120
|
+
}, 1000);
|
|
121
|
+
return () => clearInterval(id);
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
// ---- Manual refresh ----
|
|
125
|
+
const lastScannedRef = useRef(null);
|
|
126
|
+
|
|
127
|
+
const fetchAndRerender = useCallback(async () => {
|
|
128
|
+
const btn = document.getElementById('refresh-btn');
|
|
129
|
+
if (btn) btn.textContent = '↺ …';
|
|
130
|
+
try {
|
|
131
|
+
const r = await fetch('/api/state');
|
|
132
|
+
if (!r.ok) return;
|
|
133
|
+
const newState = await r.json();
|
|
134
|
+
lastScannedRef.current = newState.lastScanned;
|
|
135
|
+
scanTimeRef.current = Date.now();
|
|
136
|
+
setUpdatedAgo('just now');
|
|
137
|
+
if (newState.raw) {
|
|
138
|
+
setState({
|
|
139
|
+
phases: newState.phaseTree || newState.raw.phases || [],
|
|
140
|
+
milestone: newState.raw.milestone || '',
|
|
141
|
+
currentPhase: newState.raw.current_phase || null,
|
|
142
|
+
currentSprint: newState.raw.current_sprint || null,
|
|
143
|
+
decisions: newState.raw.decisions || [],
|
|
144
|
+
blockers: newState.raw.blockers || [],
|
|
145
|
+
council_sessions: newState.raw.council_sessions || [],
|
|
146
|
+
last_session: newState.raw.last_session || null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} catch { /* network errors ignored */ }
|
|
150
|
+
if (btn) btn.textContent = '↺ Refresh';
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
// ---- 30s auto-refresh ----
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const id = setInterval(async () => {
|
|
156
|
+
try {
|
|
157
|
+
const r = await fetch('/api/state');
|
|
158
|
+
if (!r.ok) return;
|
|
159
|
+
const s = await r.json();
|
|
160
|
+
if (s.lastScanned !== lastScannedRef.current) await fetchAndRerender();
|
|
161
|
+
} catch { /* ignore */ }
|
|
162
|
+
}, 30000);
|
|
163
|
+
return () => clearInterval(id);
|
|
164
|
+
}, [fetchAndRerender]);
|
|
165
|
+
|
|
166
|
+
// Expose manualRefresh globally for any legacy onclick="manualRefresh()" callers
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
window._preactRefresh = fetchAndRerender;
|
|
169
|
+
}, [fetchAndRerender]);
|
|
170
|
+
|
|
171
|
+
// Start the global session poll and refresh the orchestrator token on boot.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
refreshOrchToken();
|
|
174
|
+
startSessionsPoll();
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
// ---- View rendering ----
|
|
178
|
+
const PreactView = PREACT_VIEWS[view] || null;
|
|
179
|
+
|
|
180
|
+
return html`
|
|
181
|
+
<div class="app-shell">
|
|
182
|
+
<${Sidebar} activeView=${view} projectName=${storeState.projectName || ''} />
|
|
183
|
+
|
|
184
|
+
<div id="sidebar-backdrop" onClick=${() => {
|
|
185
|
+
const sidebar = document.querySelector('.sidebar');
|
|
186
|
+
const backdrop = document.getElementById('sidebar-backdrop');
|
|
187
|
+
if (sidebar) sidebar.classList.remove('sidebar-open');
|
|
188
|
+
if (backdrop) backdrop.classList.remove('active');
|
|
189
|
+
document.body.classList.remove('sidebar-visible');
|
|
190
|
+
}}></div>
|
|
191
|
+
|
|
192
|
+
<div class="content-area" id="main-content">
|
|
193
|
+
<${Topbar}
|
|
194
|
+
projectName=${storeState.projectName || ''}
|
|
195
|
+
updatedAgo=${updatedAgo}
|
|
196
|
+
onRefresh=${fetchAndRerender}
|
|
197
|
+
onToggleTheme=${toggleTheme}
|
|
198
|
+
onToggleSidebar=${toggleSidebar}
|
|
199
|
+
themeLabel=${theme}
|
|
200
|
+
/>
|
|
201
|
+
|
|
202
|
+
<div class="main-scroll" id="main-scroll">
|
|
203
|
+
${PreactView ? html`<${PreactView} subId=${subId} />` : null}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<${XtermPanel} />
|
|
208
|
+
<${OrchPanel} />
|
|
209
|
+
</div>
|
|
210
|
+
`;
|
|
211
|
+
}
|