@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* VirtEngine Control Center – Reactive State Layer
|
|
3
|
+
* All signals, data loaders, toast system, and tab refresh logic
|
|
4
|
+
* ────────────────────────────────────────────────────────────── */
|
|
5
|
+
|
|
6
|
+
import { signal } from "@preact/signals";
|
|
7
|
+
import { apiFetch, onWsMessage } from "./api.js";
|
|
8
|
+
import { cloneValue } from "./utils.js";
|
|
9
|
+
import { generateId } from "./utils.js";
|
|
10
|
+
|
|
11
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
12
|
+
* CLOUD STORAGE HELPER — mirrors settings.js pattern
|
|
13
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
14
|
+
|
|
15
|
+
/** @param {string} key @returns {Promise<any>} */
|
|
16
|
+
function _cloudGet(key) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const tg = globalThis.Telegram?.WebApp;
|
|
19
|
+
if (tg?.CloudStorage) {
|
|
20
|
+
tg.CloudStorage.getItem(key, (err, val) => {
|
|
21
|
+
if (err || val == null) resolve(null);
|
|
22
|
+
else {
|
|
23
|
+
try { resolve(JSON.parse(val)); }
|
|
24
|
+
catch { resolve(val); }
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
try {
|
|
29
|
+
const v = localStorage.getItem("ve_settings_" + key);
|
|
30
|
+
resolve(v != null ? JSON.parse(v) : null);
|
|
31
|
+
} catch { resolve(null); }
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
37
|
+
* API RESPONSE CACHE — stale-while-revalidate
|
|
38
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
39
|
+
|
|
40
|
+
const _apiCache = new Map();
|
|
41
|
+
const CACHE_TTL = {
|
|
42
|
+
status: 5000, executor: 5000, tasks: 10000, agents: 5000,
|
|
43
|
+
threads: 5000, logs: 15000, worktrees: 30000, workspaces: 30000,
|
|
44
|
+
presence: 30000, config: 60000, projects: 60000, git: 20000,
|
|
45
|
+
infra: 30000,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function _cacheKey(url) { return url; }
|
|
49
|
+
function _cacheGet(url) { return _apiCache.get(url) || null; }
|
|
50
|
+
function _cacheSet(url, data) { _apiCache.set(url, { data, fetchedAt: Date.now() }); }
|
|
51
|
+
function _cacheFresh(url, group) {
|
|
52
|
+
const e = _apiCache.get(url);
|
|
53
|
+
return e ? (Date.now() - e.fetchedAt) < (CACHE_TTL[group] || 10000) : false;
|
|
54
|
+
}
|
|
55
|
+
function _cacheClearGroup(group) {
|
|
56
|
+
for (const k of _apiCache.keys()) {
|
|
57
|
+
if (k.includes(group) || group === '*') _apiCache.delete(k);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Tracks last-fetch timestamps per data group for "Updated Xs ago" UI. */
|
|
62
|
+
export const dataFreshness = signal({});
|
|
63
|
+
function _markFresh(group) {
|
|
64
|
+
dataFreshness.value = { ...dataFreshness.value, [group]: Date.now() };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
68
|
+
* SIGNALS — Single source of truth for UI state
|
|
69
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
70
|
+
|
|
71
|
+
// ── Overall connectivity
|
|
72
|
+
export const connected = signal(false);
|
|
73
|
+
|
|
74
|
+
// ── Dashboard
|
|
75
|
+
export const statusData = signal(null);
|
|
76
|
+
export const executorData = signal(null);
|
|
77
|
+
export const projectSummary = signal(null);
|
|
78
|
+
|
|
79
|
+
// ── Tasks
|
|
80
|
+
export const tasksLoaded = signal(false);
|
|
81
|
+
export const tasksData = signal([]);
|
|
82
|
+
export const tasksPage = signal(0);
|
|
83
|
+
export const tasksPageSize = signal(20);
|
|
84
|
+
export const tasksFilter = signal("all");
|
|
85
|
+
export const tasksPriority = signal("all");
|
|
86
|
+
export const tasksSearch = signal("");
|
|
87
|
+
export const tasksSort = signal("updated");
|
|
88
|
+
export const tasksTotalPages = signal(1);
|
|
89
|
+
|
|
90
|
+
// ── Agents
|
|
91
|
+
export const agentsData = signal([]);
|
|
92
|
+
export const agentWorkspaceTarget = signal(null);
|
|
93
|
+
|
|
94
|
+
// ── Infra
|
|
95
|
+
export const worktreeData = signal([]);
|
|
96
|
+
export const sharedWorkspaces = signal([]);
|
|
97
|
+
export const presenceInstances = signal([]);
|
|
98
|
+
export const coordinatorInfo = signal(null);
|
|
99
|
+
export const infraData = signal(null);
|
|
100
|
+
|
|
101
|
+
// ── Logs
|
|
102
|
+
export const logsData = signal(null);
|
|
103
|
+
export const logsLines = signal(100);
|
|
104
|
+
export const gitDiff = signal(null);
|
|
105
|
+
export const gitBranches = signal([]);
|
|
106
|
+
export const agentLogFiles = signal([]);
|
|
107
|
+
export const agentLogFile = signal("");
|
|
108
|
+
export const agentLogTail = signal(null);
|
|
109
|
+
export const agentLogLines = signal(200);
|
|
110
|
+
export const agentLogQuery = signal("");
|
|
111
|
+
export const agentContext = signal(null);
|
|
112
|
+
|
|
113
|
+
// ── Config (routing, regions, etc.)
|
|
114
|
+
export const configData = signal(null);
|
|
115
|
+
|
|
116
|
+
// ── Toasts
|
|
117
|
+
export const toasts = signal([]);
|
|
118
|
+
|
|
119
|
+
// ── Notification Preferences (loaded from CloudStorage)
|
|
120
|
+
export const notificationPrefs = signal({
|
|
121
|
+
notifyUpdates: true,
|
|
122
|
+
notifyErrors: true,
|
|
123
|
+
notifyCompletion: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
127
|
+
* NOTIFICATION PREFERENCES
|
|
128
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
129
|
+
|
|
130
|
+
/** Load notification preferences from CloudStorage into signal */
|
|
131
|
+
export async function loadNotificationPrefs() {
|
|
132
|
+
const [nu, ne, nc] = await Promise.all([
|
|
133
|
+
_cloudGet("notifyUpdates"),
|
|
134
|
+
_cloudGet("notifyErrors"),
|
|
135
|
+
_cloudGet("notifyCompletion"),
|
|
136
|
+
]);
|
|
137
|
+
notificationPrefs.value = {
|
|
138
|
+
notifyUpdates: nu != null ? nu : true,
|
|
139
|
+
notifyErrors: ne != null ? ne : true,
|
|
140
|
+
notifyCompletion: nc != null ? nc : true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Critical message patterns that always show regardless of prefs */
|
|
145
|
+
const CRITICAL_PATTERNS = /\b(connection\s*lost|auth\s*(fail|error)|authentication\s*(fail|error)|unauthorized)\b/i;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Determine if a toast should be rendered based on notification prefs.
|
|
149
|
+
* Filtering happens at render time so toasts are still logged even if hidden.
|
|
150
|
+
* @param {{ id: string, message: string, type: string }} toast
|
|
151
|
+
* @returns {boolean}
|
|
152
|
+
*/
|
|
153
|
+
export function shouldShowToast(toast) {
|
|
154
|
+
if (CRITICAL_PATTERNS.test(toast.message)) return true;
|
|
155
|
+
|
|
156
|
+
const prefs = notificationPrefs.value;
|
|
157
|
+
|
|
158
|
+
if (!prefs.notifyUpdates && (toast.type === "info" || toast.type === "success")) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!prefs.notifyErrors && toast.type === "error") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (!prefs.notifyCompletion && /\b(complete[ds]?|done)\b/i.test(toast.message)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
172
|
+
* EXECUTOR DEFAULTS — apply stored settings on first load
|
|
173
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
174
|
+
|
|
175
|
+
let _defaultsApplied = false;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Read stored executor defaults from CloudStorage and POST them to
|
|
179
|
+
* the server if they differ from the current config.
|
|
180
|
+
* Only runs once per app lifecycle (not on tab switches).
|
|
181
|
+
*/
|
|
182
|
+
export async function applyStoredDefaults() {
|
|
183
|
+
if (_defaultsApplied) return;
|
|
184
|
+
_defaultsApplied = true;
|
|
185
|
+
|
|
186
|
+
const [maxP, sdk, region] = await Promise.all([
|
|
187
|
+
_cloudGet("defaultMaxParallel"),
|
|
188
|
+
_cloudGet("defaultSdk"),
|
|
189
|
+
_cloudGet("defaultRegion"),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const promises = [];
|
|
193
|
+
|
|
194
|
+
if (maxP != null) {
|
|
195
|
+
const current = executorData.value;
|
|
196
|
+
const currentMax =
|
|
197
|
+
current?.data?.maxParallel ??
|
|
198
|
+
current?.maxParallel ??
|
|
199
|
+
null;
|
|
200
|
+
const isPaused = Boolean(current?.paused || current?.data?.paused);
|
|
201
|
+
if (!isPaused && currentMax !== maxP) {
|
|
202
|
+
promises.push(
|
|
203
|
+
apiFetch("/api/executor/maxparallel", {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: JSON.stringify({ maxParallel: maxP }),
|
|
206
|
+
_silent: true,
|
|
207
|
+
}).catch(() => {}),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const configUpdates = {};
|
|
213
|
+
if (sdk && sdk !== "auto") configUpdates.sdk = sdk;
|
|
214
|
+
if (region && region !== "auto") configUpdates.region = region;
|
|
215
|
+
|
|
216
|
+
if (Object.keys(configUpdates).length) {
|
|
217
|
+
promises.push(
|
|
218
|
+
apiFetch("/api/config/update", {
|
|
219
|
+
method: "POST",
|
|
220
|
+
body: JSON.stringify(configUpdates),
|
|
221
|
+
_silent: true,
|
|
222
|
+
}).catch(() => {}),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (promises.length) await Promise.all(promises);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
230
|
+
* TOAST SYSTEM
|
|
231
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Show a toast notification that auto-dismisses after 3 s.
|
|
235
|
+
* @param {string} message
|
|
236
|
+
* @param {'info'|'success'|'error'|'warning'} type
|
|
237
|
+
*/
|
|
238
|
+
export function showToast(message, type = "info") {
|
|
239
|
+
const id = generateId();
|
|
240
|
+
toasts.value = [...toasts.value, { id, message, type }];
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
toasts.value = toasts.value.filter((t) => t.id !== id);
|
|
243
|
+
}, 3000);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Listen for api-error events dispatched by the api module
|
|
247
|
+
if (typeof globalThis !== "undefined") {
|
|
248
|
+
try {
|
|
249
|
+
globalThis.addEventListener("ve:api-error", (e) => {
|
|
250
|
+
showToast(e.detail?.message || "Request failed", "error");
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
/* SSR guard */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
258
|
+
* DATA LOADERS — each calls apiFetch and updates its signal(s)
|
|
259
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
260
|
+
|
|
261
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
262
|
+
* DASHBOARD HISTORY — localStorage-backed trend tracking
|
|
263
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
264
|
+
|
|
265
|
+
const HISTORY_KEY = "ve-dashboard-history";
|
|
266
|
+
const HISTORY_MAX = 50;
|
|
267
|
+
const HISTORY_MIN_INTERVAL_MS = 30_000;
|
|
268
|
+
|
|
269
|
+
/** Read stored history from localStorage. */
|
|
270
|
+
export function getDashboardHistory() {
|
|
271
|
+
try {
|
|
272
|
+
const raw = localStorage.getItem(HISTORY_KEY);
|
|
273
|
+
return raw ? JSON.parse(raw) : [];
|
|
274
|
+
} catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Compute trend delta between latest and previous snapshot for a metric.
|
|
281
|
+
* Returns 0 when insufficient history.
|
|
282
|
+
* @param {string} metric - one of: total, running, done, errors, successRate
|
|
283
|
+
* @returns {number}
|
|
284
|
+
*/
|
|
285
|
+
export function getTrend(metric) {
|
|
286
|
+
const hist = getDashboardHistory();
|
|
287
|
+
if (hist.length < 2) return 0;
|
|
288
|
+
const latest = hist[hist.length - 1];
|
|
289
|
+
const prev = hist[hist.length - 2];
|
|
290
|
+
return (latest[metric] ?? 0) - (prev[metric] ?? 0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Save a dashboard snapshot after a successful status load. */
|
|
294
|
+
function _saveDashboardSnapshot() {
|
|
295
|
+
try {
|
|
296
|
+
const s = statusData.value;
|
|
297
|
+
if (!s) return;
|
|
298
|
+
const counts = s.counts || {};
|
|
299
|
+
const running = Number(counts.running || counts.inprogress || 0);
|
|
300
|
+
const review = Number(counts.review || counts.inreview || 0);
|
|
301
|
+
const blocked = Number(counts.error || 0);
|
|
302
|
+
const done = Number(counts.done || 0);
|
|
303
|
+
const backlog = Number(s.backlog_remaining || counts.todo || 0);
|
|
304
|
+
const total = running + review + blocked + backlog + done;
|
|
305
|
+
const successRate = total > 0 ? +((done / total) * 100).toFixed(1) : 0;
|
|
306
|
+
|
|
307
|
+
const snap = { ts: Date.now(), total, running, done, errors: blocked, successRate };
|
|
308
|
+
|
|
309
|
+
const hist = getDashboardHistory();
|
|
310
|
+
|
|
311
|
+
// Deduplicate: skip if too recent and values unchanged
|
|
312
|
+
if (hist.length > 0) {
|
|
313
|
+
const last = hist[hist.length - 1];
|
|
314
|
+
const elapsed = snap.ts - (last.ts || 0);
|
|
315
|
+
const same =
|
|
316
|
+
last.total === snap.total &&
|
|
317
|
+
last.running === snap.running &&
|
|
318
|
+
last.done === snap.done &&
|
|
319
|
+
last.errors === snap.errors;
|
|
320
|
+
if (elapsed < HISTORY_MIN_INTERVAL_MS && same) return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
hist.push(snap);
|
|
324
|
+
// Trim to max length
|
|
325
|
+
while (hist.length > HISTORY_MAX) hist.shift();
|
|
326
|
+
|
|
327
|
+
localStorage.setItem(HISTORY_KEY, JSON.stringify(hist));
|
|
328
|
+
} catch {
|
|
329
|
+
/* localStorage may be unavailable */
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Load system status → statusData */
|
|
334
|
+
export async function loadStatus() {
|
|
335
|
+
const url = "/api/status";
|
|
336
|
+
const cached = _cacheGet(url);
|
|
337
|
+
if (_cacheFresh(url, "status")) return;
|
|
338
|
+
if (cached) { statusData.value = cached.data; connected.value = true; }
|
|
339
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
340
|
+
data: null,
|
|
341
|
+
}));
|
|
342
|
+
statusData.value = res.data ?? res ?? null;
|
|
343
|
+
connected.value = true;
|
|
344
|
+
_cacheSet(url, statusData.value);
|
|
345
|
+
_markFresh("status");
|
|
346
|
+
_saveDashboardSnapshot();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Load executor state → executorData */
|
|
350
|
+
export async function loadExecutor() {
|
|
351
|
+
const url = "/api/executor";
|
|
352
|
+
const cached = _cacheGet(url);
|
|
353
|
+
if (_cacheFresh(url, "executor")) return;
|
|
354
|
+
if (cached) executorData.value = cached.data;
|
|
355
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
356
|
+
data: null,
|
|
357
|
+
}));
|
|
358
|
+
executorData.value = res ?? null;
|
|
359
|
+
_cacheSet(url, executorData.value);
|
|
360
|
+
_markFresh("executor");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Load tasks with current filter/page/sort → tasksData + tasksTotalPages */
|
|
364
|
+
export async function loadTasks() {
|
|
365
|
+
const params = new URLSearchParams({
|
|
366
|
+
page: String(tasksPage.value),
|
|
367
|
+
pageSize: String(tasksPageSize.value),
|
|
368
|
+
});
|
|
369
|
+
if (tasksFilter.value && tasksFilter.value !== "all")
|
|
370
|
+
params.set("status", tasksFilter.value);
|
|
371
|
+
if (tasksPriority.value && tasksPriority.value !== "all")
|
|
372
|
+
params.set("priority", tasksPriority.value);
|
|
373
|
+
if (tasksSearch.value) params.set("search", tasksSearch.value);
|
|
374
|
+
if (tasksSort.value) params.set("sort", tasksSort.value);
|
|
375
|
+
|
|
376
|
+
const res = await apiFetch(`/api/tasks?${params}`, { _silent: true }).catch(
|
|
377
|
+
() => ({
|
|
378
|
+
data: [],
|
|
379
|
+
total: 0,
|
|
380
|
+
totalPages: 1,
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
tasksData.value = res.data || [];
|
|
384
|
+
tasksTotalPages.value =
|
|
385
|
+
res.totalPages ||
|
|
386
|
+
Math.max(1, Math.ceil((res.total || 0) / tasksPageSize.value));
|
|
387
|
+
tasksLoaded.value = true;
|
|
388
|
+
_cacheSet(`/api/tasks?${params}`, { data: tasksData.value, totalPages: tasksTotalPages.value });
|
|
389
|
+
_markFresh("tasks");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Load active agents → agentsData */
|
|
393
|
+
export async function loadAgents() {
|
|
394
|
+
const url = "/api/agents";
|
|
395
|
+
const cached = _cacheGet(url);
|
|
396
|
+
if (_cacheFresh(url, "agents")) return;
|
|
397
|
+
if (cached) agentsData.value = cached.data;
|
|
398
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
399
|
+
data: [],
|
|
400
|
+
}));
|
|
401
|
+
agentsData.value = res.data || [];
|
|
402
|
+
_cacheSet(url, agentsData.value);
|
|
403
|
+
_markFresh("agents");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Load worktrees → worktreeData */
|
|
407
|
+
export async function loadWorktrees() {
|
|
408
|
+
const url = "/api/worktrees";
|
|
409
|
+
const cached = _cacheGet(url);
|
|
410
|
+
if (_cacheFresh(url, "worktrees")) return;
|
|
411
|
+
if (cached) worktreeData.value = cached.data;
|
|
412
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
413
|
+
data: [],
|
|
414
|
+
stats: null,
|
|
415
|
+
}));
|
|
416
|
+
worktreeData.value = res.data || [];
|
|
417
|
+
_cacheSet(url, worktreeData.value);
|
|
418
|
+
_markFresh("worktrees");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Load infrastructure overview → infraData */
|
|
422
|
+
export async function loadInfra() {
|
|
423
|
+
const url = "/api/infra";
|
|
424
|
+
const cached = _cacheGet(url);
|
|
425
|
+
if (_cacheFresh(url, "infra")) return;
|
|
426
|
+
if (cached) infraData.value = cached.data;
|
|
427
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
428
|
+
data: null,
|
|
429
|
+
}));
|
|
430
|
+
infraData.value = res.data ?? res ?? null;
|
|
431
|
+
_cacheSet(url, infraData.value);
|
|
432
|
+
_markFresh("infra");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Load system logs → logsData */
|
|
436
|
+
export async function loadLogs() {
|
|
437
|
+
const url = `/api/logs?lines=${logsLines.value}`;
|
|
438
|
+
if (_cacheFresh(url, "logs")) return;
|
|
439
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({ data: null }));
|
|
440
|
+
logsData.value = res.data ?? res ?? null;
|
|
441
|
+
_cacheSet(url, logsData.value);
|
|
442
|
+
_markFresh("logs");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Load git branches + diff → gitBranches, gitDiff */
|
|
446
|
+
export async function loadGit() {
|
|
447
|
+
const [branches, diff] = await Promise.all([
|
|
448
|
+
apiFetch("/api/git/branches", { _silent: true }).catch(() => ({
|
|
449
|
+
data: [],
|
|
450
|
+
})),
|
|
451
|
+
apiFetch("/api/git/diff", { _silent: true }).catch(() => ({ data: "" })),
|
|
452
|
+
]);
|
|
453
|
+
gitBranches.value = branches.data || [];
|
|
454
|
+
gitDiff.value = diff.data || "";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Load agent log file list → agentLogFiles */
|
|
458
|
+
export async function loadAgentLogFileList() {
|
|
459
|
+
const params = new URLSearchParams();
|
|
460
|
+
if (agentLogQuery.value) params.set("query", agentLogQuery.value);
|
|
461
|
+
const path = params.toString()
|
|
462
|
+
? `/api/agent-logs?${params}`
|
|
463
|
+
: "/api/agent-logs";
|
|
464
|
+
const res = await apiFetch(path, { _silent: true }).catch(() => ({
|
|
465
|
+
data: [],
|
|
466
|
+
}));
|
|
467
|
+
agentLogFiles.value = res.data || [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Load tail of the currently selected agent log → agentLogTail */
|
|
471
|
+
export async function loadAgentLogTailData() {
|
|
472
|
+
if (!agentLogFile.value) {
|
|
473
|
+
agentLogTail.value = null;
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const params = new URLSearchParams({
|
|
477
|
+
file: agentLogFile.value,
|
|
478
|
+
lines: String(agentLogLines.value),
|
|
479
|
+
});
|
|
480
|
+
const res = await apiFetch(`/api/agent-logs/tail?${params}`, {
|
|
481
|
+
_silent: true,
|
|
482
|
+
}).catch(() => ({ data: null }));
|
|
483
|
+
agentLogTail.value = res.data ?? res ?? null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Load worktree context for a branch/query → agentContext
|
|
488
|
+
* @param {string} query
|
|
489
|
+
*/
|
|
490
|
+
export async function loadAgentContextData(query) {
|
|
491
|
+
if (!query) {
|
|
492
|
+
agentContext.value = null;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const res = await apiFetch(
|
|
496
|
+
`/api/agent-context?query=${encodeURIComponent(query)}`,
|
|
497
|
+
{ _silent: true },
|
|
498
|
+
).catch(() => ({ data: null }));
|
|
499
|
+
agentContext.value = res.data ?? res ?? null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Load shared workspaces → sharedWorkspaces */
|
|
503
|
+
export async function loadSharedWorkspaces() {
|
|
504
|
+
const url = "/api/shared-workspaces";
|
|
505
|
+
if (_cacheFresh(url, "workspaces")) return;
|
|
506
|
+
const res = await apiFetch(url, { _silent: true }).catch(
|
|
507
|
+
() => ({
|
|
508
|
+
data: [],
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
sharedWorkspaces.value = res.data || res.workspaces || [];
|
|
512
|
+
_cacheSet(url, sharedWorkspaces.value);
|
|
513
|
+
_markFresh("workspaces");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Load presence / coordinator → presenceInstances, coordinatorInfo */
|
|
517
|
+
export async function loadPresence() {
|
|
518
|
+
const url = "/api/presence";
|
|
519
|
+
if (_cacheFresh(url, "presence")) return;
|
|
520
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
521
|
+
data: null,
|
|
522
|
+
}));
|
|
523
|
+
const data = res.data || res || {};
|
|
524
|
+
presenceInstances.value = data.instances || [];
|
|
525
|
+
coordinatorInfo.value = data.coordinator || null;
|
|
526
|
+
_cacheSet(url, data);
|
|
527
|
+
_markFresh("presence");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Load project summary → projectSummary */
|
|
531
|
+
export async function loadProjectSummary() {
|
|
532
|
+
const url = "/api/project-summary";
|
|
533
|
+
if (_cacheFresh(url, "projects")) return;
|
|
534
|
+
const res = await apiFetch(url, { _silent: true }).catch(
|
|
535
|
+
() => ({
|
|
536
|
+
data: null,
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
projectSummary.value = res.data ?? res ?? null;
|
|
540
|
+
_cacheSet(url, projectSummary.value);
|
|
541
|
+
_markFresh("projects");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Load config (routing, regions, etc.) → configData */
|
|
545
|
+
export async function loadConfig() {
|
|
546
|
+
const url = "/api/config";
|
|
547
|
+
if (_cacheFresh(url, "config")) return;
|
|
548
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
549
|
+
ok: false,
|
|
550
|
+
}));
|
|
551
|
+
configData.value = res?.ok ? res : null;
|
|
552
|
+
_cacheSet(url, configData.value);
|
|
553
|
+
_markFresh("config");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
557
|
+
* TAB REFRESH — map tab names to their required loaders
|
|
558
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
559
|
+
|
|
560
|
+
const TAB_LOADERS = {
|
|
561
|
+
dashboard: () =>
|
|
562
|
+
Promise.all([loadStatus(), loadExecutor(), loadProjectSummary()]),
|
|
563
|
+
tasks: () => loadTasks(),
|
|
564
|
+
agents: () => Promise.all([loadAgents(), loadExecutor(), import("../components/session-list.js").then((m) => m.loadSessions()).catch(() => {})]),
|
|
565
|
+
infra: () =>
|
|
566
|
+
Promise.all([
|
|
567
|
+
loadWorktrees(),
|
|
568
|
+
loadInfra(),
|
|
569
|
+
loadSharedWorkspaces(),
|
|
570
|
+
loadPresence(),
|
|
571
|
+
]),
|
|
572
|
+
control: () => Promise.all([loadExecutor(), loadConfig()]),
|
|
573
|
+
logs: () =>
|
|
574
|
+
Promise.all([loadLogs(), loadAgentLogFileList(), loadAgentLogTailData()]),
|
|
575
|
+
settings: () => Promise.all([loadStatus(), loadConfig()]),
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Refresh all data for a given tab.
|
|
580
|
+
* @param {string} tabName
|
|
581
|
+
* @param {{ force?: boolean }} [opts]
|
|
582
|
+
*/
|
|
583
|
+
export async function refreshTab(tabName, opts = {}) {
|
|
584
|
+
if (opts.force) _apiCache.clear();
|
|
585
|
+
const loader = TAB_LOADERS[tabName];
|
|
586
|
+
if (loader) {
|
|
587
|
+
try {
|
|
588
|
+
await loader();
|
|
589
|
+
} catch {
|
|
590
|
+
/* errors handled by individual loaders */
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
596
|
+
* HELPERS
|
|
597
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Optimistic update pattern:
|
|
601
|
+
* 1. Apply the optimistic change immediately
|
|
602
|
+
* 2. Run the async fetch
|
|
603
|
+
* 3. On error, revert via rollback
|
|
604
|
+
*
|
|
605
|
+
* @param {() => void} applyFn – mutate signals optimistically
|
|
606
|
+
* @param {() => Promise<any>} fetchFn – the actual API call
|
|
607
|
+
* @param {() => void} revertFn – undo the optimistic change on error
|
|
608
|
+
* @returns {Promise<any>}
|
|
609
|
+
*/
|
|
610
|
+
export async function runOptimistic(applyFn, fetchFn, revertFn) {
|
|
611
|
+
try {
|
|
612
|
+
applyFn();
|
|
613
|
+
return await fetchFn();
|
|
614
|
+
} catch (err) {
|
|
615
|
+
if (typeof revertFn === "function") revertFn();
|
|
616
|
+
throw err;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
621
|
+
let _scheduleTimer = null;
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Schedule a tab refresh after a short delay (debounced).
|
|
625
|
+
* Uses the the current activeTab from the router layer via import.
|
|
626
|
+
* Falls back to refreshing 'dashboard'.
|
|
627
|
+
*
|
|
628
|
+
* @param {number} ms
|
|
629
|
+
*/
|
|
630
|
+
export function scheduleRefresh(ms = 5000) {
|
|
631
|
+
if (_scheduleTimer) clearTimeout(_scheduleTimer);
|
|
632
|
+
_scheduleTimer = setTimeout(async () => {
|
|
633
|
+
_scheduleTimer = null;
|
|
634
|
+
// Dynamic import to avoid circular dependency at module load time
|
|
635
|
+
try {
|
|
636
|
+
const { activeTab } = await import("./router.js");
|
|
637
|
+
await refreshTab(activeTab.value);
|
|
638
|
+
} catch {
|
|
639
|
+
await refreshTab("dashboard");
|
|
640
|
+
}
|
|
641
|
+
}, ms);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/* ─── WebSocket invalidation listener ─── */
|
|
645
|
+
|
|
646
|
+
const WS_CHANNEL_MAP = {
|
|
647
|
+
dashboard: ["overview", "executor", "tasks", "agents"],
|
|
648
|
+
tasks: ["tasks"],
|
|
649
|
+
agents: ["agents", "executor"],
|
|
650
|
+
infra: ["worktrees", "workspaces", "presence"],
|
|
651
|
+
control: ["executor", "overview"],
|
|
652
|
+
logs: ["*"],
|
|
653
|
+
settings: ["overview"],
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
/** Start listening for WS invalidation messages and auto-refreshing. */
|
|
657
|
+
export function initWsInvalidationListener() {
|
|
658
|
+
onWsMessage((msg) => {
|
|
659
|
+
if (msg?.type !== "invalidate") return;
|
|
660
|
+
const channels = Array.isArray(msg.channels) ? msg.channels : [];
|
|
661
|
+
// Clear cache for invalidated channels so next fetch is fresh
|
|
662
|
+
channels.forEach((ch) => _cacheClearGroup(ch));
|
|
663
|
+
|
|
664
|
+
// Determine interested channels based on active tab
|
|
665
|
+
import("./router.js")
|
|
666
|
+
.then(({ activeTab }) => {
|
|
667
|
+
const interested = WS_CHANNEL_MAP[activeTab.value] || ["*"];
|
|
668
|
+
if (
|
|
669
|
+
channels.includes("*") ||
|
|
670
|
+
channels.some((c) => interested.includes(c))
|
|
671
|
+
) {
|
|
672
|
+
scheduleRefresh(150);
|
|
673
|
+
}
|
|
674
|
+
})
|
|
675
|
+
.catch(() => {
|
|
676
|
+
/* noop */
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
}
|