@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.
Files changed (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. package/worktree-manager.mjs +1266 -0
@@ -0,0 +1,515 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Tab: Dashboard — overview stats, executor, quick actions
3
+ * ────────────────────────────────────────────────────────────── */
4
+ import { h } from "preact";
5
+ import {
6
+ useState,
7
+ useEffect,
8
+ useCallback,
9
+ } from "preact/hooks";
10
+ import htm from "htm";
11
+
12
+ const html = htm.bind(h);
13
+
14
+ import { haptic, showConfirm, showAlert } from "../modules/telegram.js";
15
+ import { apiFetch, sendCommandToChat } from "../modules/api.js";
16
+ import {
17
+ statusData,
18
+ executorData,
19
+ tasksData,
20
+ projectSummary,
21
+ loadStatus,
22
+ loadProjectSummary,
23
+ showToast,
24
+ refreshTab,
25
+ runOptimistic,
26
+ scheduleRefresh,
27
+ getTrend,
28
+ getDashboardHistory,
29
+ } from "../modules/state.js";
30
+ import { navigateTo } from "../modules/router.js";
31
+ import { ICONS } from "../modules/icons.js";
32
+ import { cloneValue, formatRelative, truncate } from "../modules/utils.js";
33
+ import {
34
+ Card,
35
+ Badge,
36
+ StatCard,
37
+ SkeletonCard,
38
+ Modal,
39
+ EmptyState,
40
+ } from "../components/shared.js";
41
+ import { DonutChart, ProgressBar, MiniSparkline } from "../components/charts.js";
42
+ import {
43
+ SegmentedControl,
44
+ PullToRefresh,
45
+ SliderControl,
46
+ } from "../components/forms.js";
47
+ import { StartTaskModal } from "./tasks.js";
48
+
49
+ /* ─── Quick Action definitions ─── */
50
+ const QUICK_ACTIONS = [
51
+ {
52
+ label: "Status",
53
+ cmd: "/status",
54
+ icon: "📊",
55
+ color: "var(--accent)",
56
+ targetTab: "dashboard",
57
+ },
58
+ {
59
+ label: "Health",
60
+ cmd: "/health",
61
+ icon: "💚",
62
+ color: "var(--color-done)",
63
+ targetTab: "dashboard",
64
+ },
65
+ {
66
+ label: "Create Task",
67
+ action: "create",
68
+ icon: "➕",
69
+ color: "var(--color-inprogress)",
70
+ },
71
+ {
72
+ label: "Start Task",
73
+ action: "start",
74
+ icon: "▶",
75
+ color: "var(--color-todo)",
76
+ },
77
+ {
78
+ label: "Plan",
79
+ cmd: "/plan",
80
+ icon: "📋",
81
+ color: "var(--color-inreview)",
82
+ targetTab: "control",
83
+ },
84
+ {
85
+ label: "Logs",
86
+ cmd: "/logs 50",
87
+ icon: "📄",
88
+ color: "var(--text-secondary)",
89
+ targetTab: "logs",
90
+ },
91
+ {
92
+ label: "Menu",
93
+ cmd: "/menu",
94
+ icon: "☰",
95
+ color: "var(--color-todo)",
96
+ targetTab: "control",
97
+ },
98
+ ];
99
+
100
+ /* ─── CreateTaskModal ─── */
101
+ export function CreateTaskModal({ onClose }) {
102
+ const [title, setTitle] = useState("");
103
+ const [description, setDescription] = useState("");
104
+ const [priority, setPriority] = useState("medium");
105
+ const [submitting, setSubmitting] = useState(false);
106
+
107
+ const handleSubmit = useCallback(async () => {
108
+ if (!title.trim()) {
109
+ showToast("Title is required", "error");
110
+ return;
111
+ }
112
+ setSubmitting(true);
113
+ haptic("medium");
114
+ try {
115
+ await apiFetch("/api/tasks/create", {
116
+ method: "POST",
117
+ body: JSON.stringify({
118
+ title: title.trim(),
119
+ description: description.trim(),
120
+ priority,
121
+ }),
122
+ });
123
+ showToast("Task created", "success");
124
+ onClose();
125
+ await refreshTab("dashboard");
126
+ } catch {
127
+ /* toast shown by apiFetch */
128
+ }
129
+ setSubmitting(false);
130
+ }, [title, description, priority, onClose]);
131
+
132
+ /* Telegram MainButton integration */
133
+ useEffect(() => {
134
+ const tg = globalThis.Telegram?.WebApp;
135
+ if (tg?.MainButton) {
136
+ tg.MainButton.setText("Create Task");
137
+ tg.MainButton.show();
138
+ const handler = () => handleSubmit();
139
+ tg.MainButton.onClick(handler);
140
+ return () => {
141
+ tg.MainButton.hide();
142
+ tg.MainButton.offClick(handler);
143
+ };
144
+ }
145
+ }, [handleSubmit]);
146
+
147
+ return html`
148
+ <${Modal} title="Create Task" onClose=${onClose}>
149
+ <div class="flex-col gap-md">
150
+ <input
151
+ class="input"
152
+ placeholder="Task title"
153
+ value=${title}
154
+ onInput=${(e) => setTitle(e.target.value)}
155
+ />
156
+ <textarea
157
+ class="input"
158
+ rows="4"
159
+ placeholder="Description (optional)"
160
+ value=${description}
161
+ onInput=${(e) => setDescription(e.target.value)}
162
+ ></textarea>
163
+ <div class="card-subtitle">Priority</div>
164
+ <${SegmentedControl}
165
+ options=${[
166
+ { value: "low", label: "Low" },
167
+ { value: "medium", label: "Medium" },
168
+ { value: "high", label: "High" },
169
+ { value: "critical", label: "Critical" },
170
+ ]}
171
+ value=${priority}
172
+ onChange=${(v) => {
173
+ haptic();
174
+ setPriority(v);
175
+ }}
176
+ />
177
+ <button
178
+ class="btn btn-primary"
179
+ onClick=${handleSubmit}
180
+ disabled=${submitting}
181
+ >
182
+ ${submitting ? "Creating…" : "Create Task"}
183
+ </button>
184
+ </div>
185
+ <//>
186
+ `;
187
+ }
188
+
189
+ /* ─── DashboardTab ─── */
190
+ export function DashboardTab() {
191
+ const [showCreate, setShowCreate] = useState(false);
192
+ const [showStartModal, setShowStartModal] = useState(false);
193
+ const status = statusData.value;
194
+ const executor = executorData.value;
195
+ const project = projectSummary.value;
196
+ const counts = status?.counts || {};
197
+ const summary = status?.success_metrics || {};
198
+ const execData = executor?.data;
199
+ const mode = executor?.mode || "vk";
200
+ const defaultSdk = execData?.sdk || "auto";
201
+
202
+ const running = Number(counts.running || counts.inprogress || 0);
203
+ const review = Number(counts.review || counts.inreview || 0);
204
+ const blocked = Number(counts.error || 0);
205
+ const done = Number(counts.done || 0);
206
+ const backlog = Number(status?.backlog_remaining || counts.todo || 0);
207
+ const totalTasks = running + review + blocked + backlog + done;
208
+ const errorRate =
209
+ totalTasks > 0 ? ((blocked / totalTasks) * 100).toFixed(1) : "0.0";
210
+
211
+ const totalActive = running + review + blocked;
212
+ const progressPct =
213
+ backlog + totalActive > 0
214
+ ? Math.round((totalActive / (backlog + totalActive)) * 100)
215
+ : 0;
216
+
217
+ /* Trend indicator helper */
218
+ const trend = (val) =>
219
+ val > 0
220
+ ? html`<span class="stat-trend up">▲</span>`
221
+ : val < 0
222
+ ? html`<span class="stat-trend down">▼</span>`
223
+ : null;
224
+
225
+ /* Historical sparkline data */
226
+ const history = getDashboardHistory();
227
+ const sparkData = (metric) => history.map((h) => h[metric] ?? 0);
228
+
229
+ const segments = [
230
+ { label: "Running", value: running, color: "var(--color-inprogress)" },
231
+ { label: "Review", value: review, color: "var(--color-inreview)" },
232
+ { label: "Blocked", value: blocked, color: "var(--color-error)" },
233
+ { label: "Backlog", value: backlog, color: "var(--color-todo)" },
234
+ { label: "Done", value: done, color: "var(--color-done)" },
235
+ ].filter((s) => s.value > 0);
236
+
237
+ /* ── Executor controls ── */
238
+ const handlePause = async () => {
239
+ haptic("medium");
240
+ const confirmed = await showConfirm(
241
+ "Pause the executor? Active tasks will finish but no new ones will start.",
242
+ );
243
+ if (!confirmed) return;
244
+ const prev = cloneValue(executor);
245
+ await runOptimistic(
246
+ () => {
247
+ if (executorData.value)
248
+ executorData.value = { ...executorData.value, paused: true };
249
+ },
250
+ () => apiFetch("/api/executor/pause", { method: "POST" }),
251
+ () => {
252
+ executorData.value = prev;
253
+ },
254
+ ).catch(() => {});
255
+ scheduleRefresh(120);
256
+ };
257
+
258
+ const handleResume = async () => {
259
+ haptic("medium");
260
+ const prev = cloneValue(executor);
261
+ await runOptimistic(
262
+ () => {
263
+ if (executorData.value)
264
+ executorData.value = { ...executorData.value, paused: false };
265
+ },
266
+ () => apiFetch("/api/executor/resume", { method: "POST" }),
267
+ () => {
268
+ executorData.value = prev;
269
+ },
270
+ ).catch(() => {});
271
+ scheduleRefresh(120);
272
+ };
273
+
274
+ /* ── Quick-action handler ── */
275
+ const handleQuickAction = async (action, e) => {
276
+ haptic();
277
+ if (action.targetTab) {
278
+ navigateTo(action.targetTab, {
279
+ resetHistory: action.targetTab === "dashboard",
280
+ forceRefresh: true,
281
+ });
282
+ }
283
+ if (action.action === "create") {
284
+ setShowCreate(true);
285
+ } else if (action.action === "start") {
286
+ setShowStartModal(true);
287
+ } else if (action.cmd) {
288
+ try {
289
+ if (action.cmd.startsWith("/status")) {
290
+ await refreshTab("dashboard", { force: true });
291
+ showToast("Status refreshed", "success");
292
+ } else if (action.cmd.startsWith("/health")) {
293
+ const res = await apiFetch("/api/health", { _silent: true });
294
+ const uptime = Number(res?.uptime || 0);
295
+ showToast(`Health OK · uptime ${Math.round(uptime)}s`, "success");
296
+ } else if (action.cmd.startsWith("/logs")) {
297
+ await refreshTab("logs", { force: true });
298
+ showToast("Opened logs", "success");
299
+ } else if (action.cmd.startsWith("/menu")) {
300
+ await sendCommandToChat(action.cmd);
301
+ showToast("Opened control panel", "success");
302
+ scheduleRefresh(60);
303
+ } else if (action.cmd.startsWith("/plan")) {
304
+ await sendCommandToChat(action.cmd);
305
+ showToast("Planner dispatched", "success");
306
+ scheduleRefresh(120);
307
+ } else {
308
+ await sendCommandToChat(action.cmd);
309
+ showToast(`Sent: ${action.cmd}`, "success");
310
+ scheduleRefresh(120);
311
+ }
312
+ const btn = e?.currentTarget;
313
+ if (btn) {
314
+ btn.classList.add("quick-action-sent");
315
+ setTimeout(() => btn.classList.remove("quick-action-sent"), 1500);
316
+ }
317
+ } catch {
318
+ showToast("Command failed", "error");
319
+ }
320
+ }
321
+ };
322
+
323
+ const handleModalStart = useCallback(async ({ taskId, sdk, model }) => {
324
+ if (!taskId) return;
325
+ await apiFetch("/api/tasks/start", {
326
+ method: "POST",
327
+ body: JSON.stringify({
328
+ taskId,
329
+ ...(sdk ? { sdk } : {}),
330
+ ...(model ? { model } : {}),
331
+ }),
332
+ });
333
+ showToast("Task started", "success");
334
+ scheduleRefresh(150);
335
+ }, []);
336
+
337
+ /* ── Recent activity (last 5 tasks from global tasks signal) ── */
338
+ const recentTasks = (tasksData.value || []).slice(0, 5);
339
+
340
+ /* ── Loading skeleton ── */
341
+ if (!status && !executor)
342
+ return html`<${Card} title="Loading…"><${SkeletonCard} count=${4} /><//>`;
343
+
344
+ return html`
345
+ <!-- Stats Grid -->
346
+ <${Card} title="Today at a Glance">
347
+ <div class="stats-grid">
348
+ <${StatCard}
349
+ value=${totalTasks}
350
+ label="Total Tasks"
351
+ color="var(--text-primary)"
352
+ >
353
+ ${trend(getTrend('total'))}
354
+ <${MiniSparkline} data=${sparkData('total')} color="var(--text-primary)" />
355
+ <//>
356
+ <${StatCard}
357
+ value=${running}
358
+ label="In Progress"
359
+ color="var(--color-inprogress)"
360
+ >
361
+ ${trend(getTrend('running'))}
362
+ <${MiniSparkline} data=${sparkData('running')} color="var(--color-inprogress)" />
363
+ <//>
364
+ <${StatCard} value=${done} label="Done" color="var(--color-done)">
365
+ ${trend(getTrend('done'))}
366
+ <${MiniSparkline} data=${sparkData('done')} color="var(--color-done)" />
367
+ <//>
368
+ <${StatCard}
369
+ value="${errorRate}%"
370
+ label="Error Rate"
371
+ color="var(--color-error)"
372
+ >
373
+ ${trend(-getTrend('errors'))}
374
+ <${MiniSparkline} data=${sparkData('errors')} color="var(--color-error)" />
375
+ <//>
376
+ </div>
377
+ <//>
378
+
379
+ <!-- Task Distribution -->
380
+ <${Card} title="Task Distribution">
381
+ <${DonutChart} segments=${segments} />
382
+ <div class="meta-text text-center mt-sm">
383
+ Active progress · ${progressPct}% engaged
384
+ </div>
385
+ <${ProgressBar} percent=${progressPct} />
386
+ <//>
387
+
388
+ <!-- Project Summary -->
389
+ ${project &&
390
+ html`
391
+ <${Card} title="Project Summary" className="project-summary-card">
392
+ <div class="meta-text mb-sm">
393
+ ${project.name || project.id || "Current Project"}
394
+ </div>
395
+ ${project.description &&
396
+ html`<div class="meta-text">
397
+ ${truncate(project.description, 160)}
398
+ </div>`}
399
+ ${project.taskCount != null &&
400
+ html`
401
+ <div class="stats-grid mt-sm">
402
+ <${StatCard} value=${project.taskCount} label="Tasks" />
403
+ <${StatCard}
404
+ value=${project.completedCount ?? 0}
405
+ label="Completed"
406
+ color="var(--color-done)"
407
+ />
408
+ </div>
409
+ `}
410
+ <//>
411
+ `}
412
+
413
+ <!-- Executor -->
414
+ <${Card} title="Executor">
415
+ <div class="meta-text mb-sm">
416
+ Mode: <strong>${mode}</strong> · Slots:
417
+ ${execData?.activeSlots ?? 0}/${execData?.maxParallel ?? "—"} ·
418
+ ${executor?.paused
419
+ ? html`<${Badge} status="error" text="Paused" />`
420
+ : html`<${Badge} status="done" text="Running" />`}
421
+ </div>
422
+ <${ProgressBar}
423
+ percent=${execData?.maxParallel
424
+ ? ((execData.activeSlots || 0) / execData.maxParallel) * 100
425
+ : 0}
426
+ />
427
+ <div class="btn-row mt-sm">
428
+ <button class="btn btn-primary btn-sm" onClick=${handlePause}>
429
+ Pause Executor
430
+ </button>
431
+ <button class="btn btn-secondary btn-sm" onClick=${handleResume}>
432
+ Resume Executor
433
+ </button>
434
+ </div>
435
+ <//>
436
+
437
+ <!-- Quick Actions -->
438
+ <${Card} title="Quick Actions">
439
+ <div class="quick-actions-grid">
440
+ ${QUICK_ACTIONS.map(
441
+ (a) => html`
442
+ <button
443
+ key=${a.label}
444
+ class="quick-action-btn"
445
+ style="--qa-color: ${a.color}"
446
+ onClick=${(e) => handleQuickAction(a, e)}
447
+ >
448
+ <span class="quick-action-icon">${a.icon}</span>
449
+ <span class="quick-action-label">${a.label}</span>
450
+ </button>
451
+ `,
452
+ )}
453
+ </div>
454
+ <//>
455
+
456
+ <!-- Quality -->
457
+ <${Card} title="Quality">
458
+ <div class="stats-grid">
459
+ <${StatCard}
460
+ value="${summary.first_shot_rate ?? 0}%"
461
+ label="First-shot"
462
+ color="var(--color-done)"
463
+ />
464
+ <${StatCard}
465
+ value=${summary.needed_fix ?? 0}
466
+ label="Needed Fix"
467
+ color="var(--color-inreview)"
468
+ />
469
+ <${StatCard}
470
+ value=${summary.failed ?? 0}
471
+ label="Failed"
472
+ color="var(--color-error)"
473
+ />
474
+ </div>
475
+ <//>
476
+
477
+ <!-- Recent Activity -->
478
+ <${Card} title="Recent Activity">
479
+ ${recentTasks.length
480
+ ? recentTasks.map(
481
+ (task) => html`
482
+ <div key=${task.id} class="list-item">
483
+ <div class="list-item-content">
484
+ <div class="list-item-title">
485
+ ${truncate(task.title || "(untitled)", 50)}
486
+ </div>
487
+ <div class="meta-text">
488
+ ${task.id}${task.updated_at
489
+ ? ` · ${formatRelative(task.updated_at)}`
490
+ : ""}
491
+ </div>
492
+ </div>
493
+ <${Badge} status=${task.status} text=${task.status} />
494
+ </div>
495
+ `,
496
+ )
497
+ : html`<${EmptyState} message="No recent tasks" />`}
498
+ <//>
499
+
500
+ <!-- Create Task Modal -->
501
+ ${showCreate &&
502
+ html`<${CreateTaskModal} onClose=${() => setShowCreate(false)} />`}
503
+
504
+ ${showStartModal &&
505
+ html`
506
+ <${StartTaskModal}
507
+ task=${null}
508
+ defaultSdk=${defaultSdk}
509
+ allowTaskIdInput=${true}
510
+ onClose=${() => setShowStartModal(false)}
511
+ onStart=${handleModalStart}
512
+ />
513
+ `}
514
+ `;
515
+ }