@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
package/ui/tabs/infra.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* Tab: Infra — worktrees, shared workspaces, presence
|
|
3
|
+
* ────────────────────────────────────────────────────────────── */
|
|
4
|
+
import { h } from "preact";
|
|
5
|
+
import { useState } from "preact/hooks";
|
|
6
|
+
import htm from "htm";
|
|
7
|
+
|
|
8
|
+
const html = htm.bind(h);
|
|
9
|
+
|
|
10
|
+
import { haptic, showConfirm } from "../modules/telegram.js";
|
|
11
|
+
import { apiFetch } from "../modules/api.js";
|
|
12
|
+
import {
|
|
13
|
+
worktreeData,
|
|
14
|
+
sharedWorkspaces,
|
|
15
|
+
presenceInstances,
|
|
16
|
+
coordinatorInfo,
|
|
17
|
+
showToast,
|
|
18
|
+
refreshTab,
|
|
19
|
+
runOptimistic,
|
|
20
|
+
scheduleRefresh,
|
|
21
|
+
} from "../modules/state.js";
|
|
22
|
+
import { ICONS } from "../modules/icons.js";
|
|
23
|
+
import { cloneValue, formatRelative, formatBytes, downloadFile } from "../modules/utils.js";
|
|
24
|
+
import {
|
|
25
|
+
Card,
|
|
26
|
+
Badge,
|
|
27
|
+
StatCard,
|
|
28
|
+
SkeletonCard,
|
|
29
|
+
EmptyState,
|
|
30
|
+
} from "../components/shared.js";
|
|
31
|
+
import { ProgressBar } from "../components/charts.js";
|
|
32
|
+
import { Collapsible } from "../components/forms.js";
|
|
33
|
+
|
|
34
|
+
/* ─── Worktree health indicator ─── */
|
|
35
|
+
function healthColor(wt) {
|
|
36
|
+
if (wt.status === "stale" || wt.status === "error")
|
|
37
|
+
return "var(--color-error)";
|
|
38
|
+
const ageMin = Math.round((wt.age || 0) / 60000);
|
|
39
|
+
if (ageMin > 180) return "var(--color-inreview)"; // yellow — old
|
|
40
|
+
return "var(--color-done)"; // green — healthy
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function HealthDot({ wt }) {
|
|
44
|
+
return html`<span
|
|
45
|
+
class="health-dot"
|
|
46
|
+
style="background:${healthColor(wt)}"
|
|
47
|
+
></span>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ageString(ms) {
|
|
51
|
+
const min = Math.round((ms || 0) / 60000);
|
|
52
|
+
if (min >= 1440) return `${Math.round(min / 1440)}d`;
|
|
53
|
+
if (min >= 60) return `${Math.round(min / 60)}h`;
|
|
54
|
+
return `${min}m`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ─── InfraTab ─── */
|
|
58
|
+
export function InfraTab() {
|
|
59
|
+
/* Worktrees — work with either the new signal name or a compatible shape */
|
|
60
|
+
const wtRaw = worktreeData?.value;
|
|
61
|
+
const wts = Array.isArray(wtRaw)
|
|
62
|
+
? wtRaw
|
|
63
|
+
: wtRaw?.worktrees || wtRaw?.data || [];
|
|
64
|
+
const wStats = (wtRaw && !Array.isArray(wtRaw) ? wtRaw.stats : null) || {};
|
|
65
|
+
|
|
66
|
+
/* Shared workspaces */
|
|
67
|
+
const swRaw = sharedWorkspaces?.value;
|
|
68
|
+
const registry = swRaw || {};
|
|
69
|
+
const workspaces =
|
|
70
|
+
registry?.workspaces || (Array.isArray(swRaw) ? swRaw : []);
|
|
71
|
+
const availability = registry?.availability || {};
|
|
72
|
+
|
|
73
|
+
/* Presence */
|
|
74
|
+
const instances = presenceInstances?.value || [];
|
|
75
|
+
const coordinator = coordinatorInfo?.value || null;
|
|
76
|
+
|
|
77
|
+
/* Local form state */
|
|
78
|
+
const [releaseInput, setReleaseInput] = useState("");
|
|
79
|
+
const [sharedOwner, setSharedOwner] = useState("");
|
|
80
|
+
const [sharedTtl, setSharedTtl] = useState("");
|
|
81
|
+
const [sharedNote, setSharedNote] = useState("");
|
|
82
|
+
const [expandedWt, setExpandedWt] = useState(null);
|
|
83
|
+
|
|
84
|
+
/* ── Worktree actions ── */
|
|
85
|
+
const handlePrune = async () => {
|
|
86
|
+
const ok = await showConfirm("Prune all stale worktrees?");
|
|
87
|
+
if (!ok) return;
|
|
88
|
+
haptic("medium");
|
|
89
|
+
await apiFetch("/api/worktrees/prune", { method: "POST" }).catch(() => {});
|
|
90
|
+
showToast("Prune initiated", "success");
|
|
91
|
+
scheduleRefresh(120);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleRelease = async (key, branch) => {
|
|
95
|
+
haptic("medium");
|
|
96
|
+
const prev = cloneValue(wts);
|
|
97
|
+
await runOptimistic(
|
|
98
|
+
() => {
|
|
99
|
+
const setter = worktreeData || {};
|
|
100
|
+
if (Array.isArray(setter.value)) {
|
|
101
|
+
setter.value = setter.value.filter(
|
|
102
|
+
(w) => w.taskKey !== key && w.branch !== branch,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
() =>
|
|
107
|
+
apiFetch("/api/worktrees/release", {
|
|
108
|
+
method: "POST",
|
|
109
|
+
body: JSON.stringify({ taskKey: key, branch }),
|
|
110
|
+
}),
|
|
111
|
+
() => {
|
|
112
|
+
if (worktreeData) worktreeData.value = prev;
|
|
113
|
+
},
|
|
114
|
+
).catch(() => {});
|
|
115
|
+
scheduleRefresh(120);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleReleaseInput = async () => {
|
|
119
|
+
if (!releaseInput.trim()) return;
|
|
120
|
+
haptic("medium");
|
|
121
|
+
await apiFetch("/api/worktrees/release", {
|
|
122
|
+
method: "POST",
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
taskKey: releaseInput.trim(),
|
|
125
|
+
branch: releaseInput.trim(),
|
|
126
|
+
}),
|
|
127
|
+
}).catch(() => {});
|
|
128
|
+
setReleaseInput("");
|
|
129
|
+
scheduleRefresh(120);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/* ── Shared workspace actions ── */
|
|
133
|
+
const handleClaim = async (wsId) => {
|
|
134
|
+
haptic("medium");
|
|
135
|
+
const prev = cloneValue(sharedWorkspaces?.value);
|
|
136
|
+
await runOptimistic(
|
|
137
|
+
() => {
|
|
138
|
+
const w = (sharedWorkspaces?.value?.workspaces || []).find(
|
|
139
|
+
(x) => x.id === wsId,
|
|
140
|
+
);
|
|
141
|
+
if (w) {
|
|
142
|
+
w.availability = "leased";
|
|
143
|
+
w.lease = {
|
|
144
|
+
owner: sharedOwner || "telegram-ui",
|
|
145
|
+
lease_expires_at: new Date(
|
|
146
|
+
Date.now() + (Number(sharedTtl) || 60) * 60000,
|
|
147
|
+
).toISOString(),
|
|
148
|
+
note: sharedNote,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
() =>
|
|
153
|
+
apiFetch("/api/shared-workspaces/claim", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
workspaceId: wsId,
|
|
157
|
+
owner: sharedOwner,
|
|
158
|
+
ttlMinutes: Number(sharedTtl) || undefined,
|
|
159
|
+
note: sharedNote,
|
|
160
|
+
}),
|
|
161
|
+
}),
|
|
162
|
+
() => {
|
|
163
|
+
if (sharedWorkspaces) sharedWorkspaces.value = prev;
|
|
164
|
+
},
|
|
165
|
+
).catch(() => {});
|
|
166
|
+
scheduleRefresh(120);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleRenew = async (wsId) => {
|
|
170
|
+
haptic("medium");
|
|
171
|
+
const prev = cloneValue(sharedWorkspaces?.value);
|
|
172
|
+
await runOptimistic(
|
|
173
|
+
() => {
|
|
174
|
+
const w = (sharedWorkspaces?.value?.workspaces || []).find(
|
|
175
|
+
(x) => x.id === wsId,
|
|
176
|
+
);
|
|
177
|
+
if (w?.lease) {
|
|
178
|
+
w.lease.owner = sharedOwner || w.lease.owner;
|
|
179
|
+
w.lease.lease_expires_at = new Date(
|
|
180
|
+
Date.now() + (Number(sharedTtl) || 60) * 60000,
|
|
181
|
+
).toISOString();
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
() =>
|
|
185
|
+
apiFetch("/api/shared-workspaces/renew", {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
workspaceId: wsId,
|
|
189
|
+
owner: sharedOwner,
|
|
190
|
+
ttlMinutes: Number(sharedTtl) || undefined,
|
|
191
|
+
}),
|
|
192
|
+
}),
|
|
193
|
+
() => {
|
|
194
|
+
if (sharedWorkspaces) sharedWorkspaces.value = prev;
|
|
195
|
+
},
|
|
196
|
+
).catch(() => {});
|
|
197
|
+
scheduleRefresh(120);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const handleSharedRelease = async (wsId) => {
|
|
201
|
+
haptic("medium");
|
|
202
|
+
const prev = cloneValue(sharedWorkspaces?.value);
|
|
203
|
+
await runOptimistic(
|
|
204
|
+
() => {
|
|
205
|
+
const w = (sharedWorkspaces?.value?.workspaces || []).find(
|
|
206
|
+
(x) => x.id === wsId,
|
|
207
|
+
);
|
|
208
|
+
if (w) {
|
|
209
|
+
w.availability = "available";
|
|
210
|
+
w.lease = null;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
() =>
|
|
214
|
+
apiFetch("/api/shared-workspaces/release", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: JSON.stringify({ workspaceId: wsId, owner: sharedOwner }),
|
|
217
|
+
}),
|
|
218
|
+
() => {
|
|
219
|
+
if (sharedWorkspaces) sharedWorkspaces.value = prev;
|
|
220
|
+
},
|
|
221
|
+
).catch(() => {});
|
|
222
|
+
scheduleRefresh(120);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/* ── Export infrastructure report ── */
|
|
226
|
+
const handleExportReport = () => {
|
|
227
|
+
haptic("medium");
|
|
228
|
+
const now = new Date();
|
|
229
|
+
const activeCount = wStats.active ?? wts.filter((w) => w.status !== "stale" && w.status !== "error").length;
|
|
230
|
+
const staleCount = wStats.stale ?? wts.filter((w) => w.status === "stale").length;
|
|
231
|
+
const availCount = workspaces.filter((w) => w.availability === "available").length;
|
|
232
|
+
const leasedCount = workspaces.filter((w) => w.availability === "leased").length;
|
|
233
|
+
|
|
234
|
+
let report = "";
|
|
235
|
+
report += "VirtEngine Infrastructure Report\n";
|
|
236
|
+
report += `Generated: ${now.toISOString()}\n\n`;
|
|
237
|
+
|
|
238
|
+
report += "== Worktrees ==\n";
|
|
239
|
+
report += `Total: ${wts.length} | Active: ${activeCount} | Stale: ${staleCount}\n\n`;
|
|
240
|
+
for (const wt of wts) {
|
|
241
|
+
report += `- ${wt.branch || "(detached)"} (${wt.status || "active"}) — Age: ${ageString(wt.age)}, Path: ${wt.path || "—"}\n`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
report += "\n== Shared Workspaces ==\n";
|
|
245
|
+
report += `Total: ${workspaces.length} | Available: ${availCount} | Leased: ${leasedCount}\n\n`;
|
|
246
|
+
for (const ws of workspaces) {
|
|
247
|
+
const lease = ws.lease;
|
|
248
|
+
const owner = lease ? lease.owner || "—" : "—";
|
|
249
|
+
const expiry = lease ? new Date(lease.lease_expires_at).toISOString() : "—";
|
|
250
|
+
report += `- ${ws.name || ws.id}: ${ws.availability || "unknown"} — Owner: ${owner}, Expires: ${expiry}\n`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
report += "\n== Active Instances ==\n";
|
|
254
|
+
report += `Coordinator: ${coordinator?.instance_label || coordinator?.instance_id || "none"}\n`;
|
|
255
|
+
report += `Instances: ${instances.length}\n`;
|
|
256
|
+
for (const inst of instances) {
|
|
257
|
+
const since = inst.last_seen_at ? new Date(inst.last_seen_at).toISOString() : "unknown";
|
|
258
|
+
report += `- ${inst.instance_label || inst.instance_id} (${inst.workspace_role || "workspace"}) — Since: ${since}\n`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
262
|
+
downloadFile(report, `infra-report-${dateStr}.txt`, "text/plain");
|
|
263
|
+
showToast("Infrastructure report exported", "success");
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/* ── Render ── */
|
|
267
|
+
return html`
|
|
268
|
+
<!-- ─── Infra header with export ─── -->
|
|
269
|
+
<div class="flex-between mb-md" style="padding:0 4px">
|
|
270
|
+
<span style="font-weight:600;font-size:15px">Infrastructure</span>
|
|
271
|
+
<button class="btn btn-secondary btn-sm" style="display:inline-flex;align-items:center;gap:4px" onClick=${handleExportReport}>
|
|
272
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
|
273
|
+
Export Report
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<!-- ─── Worktrees ─── -->
|
|
278
|
+
<${Collapsible} title="Worktrees" defaultOpen=${true}>
|
|
279
|
+
<${Card}>
|
|
280
|
+
<div class="stats-grid mb-md">
|
|
281
|
+
<${StatCard} value=${wStats.total ?? wts.length} label="Total" />
|
|
282
|
+
<${StatCard}
|
|
283
|
+
value=${wStats.active ?? 0}
|
|
284
|
+
label="Active"
|
|
285
|
+
color="var(--color-done)"
|
|
286
|
+
/>
|
|
287
|
+
<${StatCard}
|
|
288
|
+
value=${wStats.stale ?? 0}
|
|
289
|
+
label="Stale"
|
|
290
|
+
color="var(--color-inreview)"
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="input-row mb-md">
|
|
295
|
+
<input
|
|
296
|
+
class="input"
|
|
297
|
+
placeholder="Task key or branch"
|
|
298
|
+
value=${releaseInput}
|
|
299
|
+
onInput=${(e) => setReleaseInput(e.target.value)}
|
|
300
|
+
/>
|
|
301
|
+
<button
|
|
302
|
+
class="btn btn-secondary btn-sm"
|
|
303
|
+
onClick=${handleReleaseInput}
|
|
304
|
+
>
|
|
305
|
+
Release
|
|
306
|
+
</button>
|
|
307
|
+
<button class="btn btn-danger btn-sm" onClick=${handlePrune}>
|
|
308
|
+
🗑 Prune
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
${wts.map(
|
|
313
|
+
(wt, idx) => html`
|
|
314
|
+
<div key=${wt.branch || wt.path || idx} class="task-card">
|
|
315
|
+
<div
|
|
316
|
+
class="task-card-header"
|
|
317
|
+
style="cursor:pointer"
|
|
318
|
+
onClick=${() => {
|
|
319
|
+
haptic();
|
|
320
|
+
setExpandedWt(expandedWt === idx ? null : idx);
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
324
|
+
<${HealthDot} wt=${wt} />
|
|
325
|
+
<div>
|
|
326
|
+
<div class="task-card-title">
|
|
327
|
+
${wt.branch || "(detached)"}
|
|
328
|
+
</div>
|
|
329
|
+
<div class="task-card-meta">${wt.path}</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
<${Badge}
|
|
333
|
+
status=${wt.status || "active"}
|
|
334
|
+
text=${wt.status || "active"}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="meta-text">
|
|
338
|
+
Age
|
|
339
|
+
${ageString(wt.age)}${wt.taskKey
|
|
340
|
+
? ` · ${wt.taskKey}`
|
|
341
|
+
: ""}${wt.owner ? ` · Owner ${wt.owner}` : ""}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<!-- Collapsible git status section -->
|
|
345
|
+
${expandedWt === idx &&
|
|
346
|
+
html`
|
|
347
|
+
<div class="wt-detail mt-sm">
|
|
348
|
+
${wt.gitStatus &&
|
|
349
|
+
html` <div class="log-box log-box-sm">${wt.gitStatus}</div> `}
|
|
350
|
+
${wt.lastCommit &&
|
|
351
|
+
html`
|
|
352
|
+
<div class="meta-text mt-xs">
|
|
353
|
+
Last commit: ${truncate(wt.lastCommit, 80)}
|
|
354
|
+
</div>
|
|
355
|
+
`}
|
|
356
|
+
${wt.filesChanged != null &&
|
|
357
|
+
html`
|
|
358
|
+
<div class="meta-text">
|
|
359
|
+
Files changed: ${wt.filesChanged}
|
|
360
|
+
</div>
|
|
361
|
+
`}
|
|
362
|
+
</div>
|
|
363
|
+
`}
|
|
364
|
+
|
|
365
|
+
<div class="btn-row mt-sm">
|
|
366
|
+
${wt.taskKey &&
|
|
367
|
+
html`
|
|
368
|
+
<button
|
|
369
|
+
class="btn btn-ghost btn-sm"
|
|
370
|
+
onClick=${() => handleRelease(wt.taskKey, "")}
|
|
371
|
+
>
|
|
372
|
+
Release Key
|
|
373
|
+
</button>
|
|
374
|
+
`}
|
|
375
|
+
${wt.branch &&
|
|
376
|
+
html`
|
|
377
|
+
<button
|
|
378
|
+
class="btn btn-ghost btn-sm"
|
|
379
|
+
onClick=${() => handleRelease("", wt.branch)}
|
|
380
|
+
>
|
|
381
|
+
Release Branch
|
|
382
|
+
</button>
|
|
383
|
+
`}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
`,
|
|
387
|
+
)}
|
|
388
|
+
${!wts.length &&
|
|
389
|
+
html`<${EmptyState} message="No worktrees tracked." />`}
|
|
390
|
+
<//>
|
|
391
|
+
<//>
|
|
392
|
+
|
|
393
|
+
<!-- ─── Shared Workspaces ─── -->
|
|
394
|
+
<${Collapsible} title="Shared Workspaces" defaultOpen=${true}>
|
|
395
|
+
<${Card}>
|
|
396
|
+
<div class="chip-group mb-sm">
|
|
397
|
+
${Object.entries(availability).map(
|
|
398
|
+
([k, v]) => html`<span key=${k} class="pill">${k}: ${v}</span>`,
|
|
399
|
+
)}
|
|
400
|
+
${!Object.keys(availability).length &&
|
|
401
|
+
html`<span class="pill">No registry</span>`}
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<div class="input-row mb-sm">
|
|
405
|
+
<input
|
|
406
|
+
class="input"
|
|
407
|
+
placeholder="Owner"
|
|
408
|
+
value=${sharedOwner}
|
|
409
|
+
onInput=${(e) => setSharedOwner(e.target.value)}
|
|
410
|
+
/>
|
|
411
|
+
<input
|
|
412
|
+
class="input"
|
|
413
|
+
type="number"
|
|
414
|
+
min="30"
|
|
415
|
+
step="15"
|
|
416
|
+
placeholder="TTL (min)"
|
|
417
|
+
value=${sharedTtl}
|
|
418
|
+
onInput=${(e) => setSharedTtl(e.target.value)}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
<input
|
|
422
|
+
class="input mb-md"
|
|
423
|
+
placeholder="Note (optional)"
|
|
424
|
+
value=${sharedNote}
|
|
425
|
+
onInput=${(e) => setSharedNote(e.target.value)}
|
|
426
|
+
/>
|
|
427
|
+
|
|
428
|
+
${workspaces.map((ws) => {
|
|
429
|
+
const lease = ws.lease;
|
|
430
|
+
const leaseInfo = lease
|
|
431
|
+
? `Leased to ${lease.owner} until ${new Date(lease.lease_expires_at).toLocaleString()}`
|
|
432
|
+
: "Available";
|
|
433
|
+
return html`
|
|
434
|
+
<div key=${ws.id} class="task-card">
|
|
435
|
+
<div class="task-card-header">
|
|
436
|
+
<div>
|
|
437
|
+
<div class="task-card-title">${ws.name || ws.id}</div>
|
|
438
|
+
<div class="task-card-meta">
|
|
439
|
+
${ws.provider || "provider"} · ${ws.region || "region?"}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
<${Badge} status=${ws.availability} text=${ws.availability} />
|
|
443
|
+
</div>
|
|
444
|
+
<div class="meta-text">${leaseInfo}</div>
|
|
445
|
+
${lease?.note &&
|
|
446
|
+
html`<div class="meta-text" style="font-style:italic">
|
|
447
|
+
${lease.note}
|
|
448
|
+
</div>`}
|
|
449
|
+
|
|
450
|
+
<div class="btn-row mt-sm">
|
|
451
|
+
<button
|
|
452
|
+
class="btn btn-primary btn-sm"
|
|
453
|
+
onClick=${() => handleClaim(ws.id)}
|
|
454
|
+
>
|
|
455
|
+
🔒 Claim
|
|
456
|
+
</button>
|
|
457
|
+
<button
|
|
458
|
+
class="btn btn-secondary btn-sm"
|
|
459
|
+
onClick=${() => handleRenew(ws.id)}
|
|
460
|
+
>
|
|
461
|
+
↻ Renew
|
|
462
|
+
</button>
|
|
463
|
+
<button
|
|
464
|
+
class="btn btn-ghost btn-sm"
|
|
465
|
+
onClick=${() => handleSharedRelease(ws.id)}
|
|
466
|
+
>
|
|
467
|
+
🔓 Release
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
`;
|
|
472
|
+
})}
|
|
473
|
+
${!workspaces.length &&
|
|
474
|
+
html`<${EmptyState} message="No shared workspaces configured." />`}
|
|
475
|
+
<//>
|
|
476
|
+
<//>
|
|
477
|
+
|
|
478
|
+
<!-- ─── Presence ─── -->
|
|
479
|
+
<${Collapsible} title="Presence" defaultOpen=${true}>
|
|
480
|
+
<${Card}>
|
|
481
|
+
<!-- Coordinator info -->
|
|
482
|
+
<div class="task-card mb-md">
|
|
483
|
+
<div class="task-card-title">🎯 Coordinator</div>
|
|
484
|
+
<div class="meta-text">
|
|
485
|
+
${coordinator?.instance_label || coordinator?.instance_id || "none"}
|
|
486
|
+
· Priority ${coordinator?.coordinator_priority ?? "—"}
|
|
487
|
+
</div>
|
|
488
|
+
${coordinator?.last_seen_at &&
|
|
489
|
+
html`
|
|
490
|
+
<div class="meta-text">
|
|
491
|
+
Last seen: ${formatRelative(coordinator.last_seen_at)}
|
|
492
|
+
</div>
|
|
493
|
+
`}
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<!-- Instance grid -->
|
|
497
|
+
${instances.length
|
|
498
|
+
? html`
|
|
499
|
+
<div class="stats-grid">
|
|
500
|
+
${instances.map(
|
|
501
|
+
(inst, i) => html`
|
|
502
|
+
<div
|
|
503
|
+
key=${i}
|
|
504
|
+
class="stat-card"
|
|
505
|
+
style="text-align:left;padding:10px"
|
|
506
|
+
>
|
|
507
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
508
|
+
<span
|
|
509
|
+
class="health-dot"
|
|
510
|
+
style="background:${inst.status === "offline"
|
|
511
|
+
? "var(--color-error)"
|
|
512
|
+
: "var(--color-done)"}"
|
|
513
|
+
></span>
|
|
514
|
+
<span style="font-weight:600;font-size:13px">
|
|
515
|
+
${inst.instance_label || inst.instance_id}
|
|
516
|
+
</span>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="meta-text">
|
|
519
|
+
${inst.workspace_role || "workspace"} ·
|
|
520
|
+
${inst.host || "host"}
|
|
521
|
+
</div>
|
|
522
|
+
<div class="meta-text">
|
|
523
|
+
Last:
|
|
524
|
+
${inst.last_seen_at
|
|
525
|
+
? formatRelative(inst.last_seen_at)
|
|
526
|
+
: "unknown"}
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
`,
|
|
530
|
+
)}
|
|
531
|
+
</div>
|
|
532
|
+
`
|
|
533
|
+
: html`<${EmptyState} message="No active instances." />`}
|
|
534
|
+
<//>
|
|
535
|
+
<//>
|
|
536
|
+
`;
|
|
537
|
+
}
|