waymark-hub 1.0.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/LICENSE +21 -0
- package/README.md +176 -0
- package/dashboard/README.md +71 -0
- package/dashboard/index.html +520 -0
- package/dashboard/server.mjs +343 -0
- package/dashboard/vendor/tailwind.js +83 -0
- package/dist/cli/benchmark.d.ts +3 -0
- package/dist/cli/benchmark.d.ts.map +1 -0
- package/dist/cli/benchmark.js +222 -0
- package/dist/cli/benchmark.js.map +1 -0
- package/dist/cli/waymark.d.ts +3 -0
- package/dist/cli/waymark.d.ts.map +1 -0
- package/dist/cli/waymark.js +216 -0
- package/dist/cli/waymark.js.map +1 -0
- package/dist/context/builder.d.ts +67 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/builder.js +270 -0
- package/dist/context/builder.js.map +1 -0
- package/dist/db/client.d.ts +5 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +67 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/migrations/001_initial.sql +85 -0
- package/dist/db/migrations/002_usage_experiments.sql +52 -0
- package/dist/db/migrations/003_agent_identity.sql +37 -0
- package/dist/db/migrations/004_memory_lifecycle.sql +25 -0
- package/dist/db/migrations/005_task_coordination.sql +17 -0
- package/dist/db/schema.d.ts +132 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +3 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +164 -0
- package/dist/server.js.map +1 -0
- package/dist/telemetry/tokens.d.ts +9 -0
- package/dist/telemetry/tokens.d.ts.map +1 -0
- package/dist/telemetry/tokens.js +23 -0
- package/dist/telemetry/tokens.js.map +1 -0
- package/dist/tools/agents.d.ts +3 -0
- package/dist/tools/agents.d.ts.map +1 -0
- package/dist/tools/agents.js +126 -0
- package/dist/tools/agents.js.map +1 -0
- package/dist/tools/context.d.ts +3 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +53 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +273 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/projects.d.ts +3 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +92 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/sessions.d.ts +3 -0
- package/dist/tools/sessions.d.ts.map +1 -0
- package/dist/tools/sessions.js +113 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +304 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/telemetry.d.ts +3 -0
- package/dist/tools/telemetry.d.ts.map +1 -0
- package/dist/tools/telemetry.js +291 -0
- package/dist/tools/telemetry.js.map +1 -0
- package/docs/AGENT_IDENTITY.md +67 -0
- package/docs/BENCHMARKING.md +161 -0
- package/docs/CONTEXT.md +95 -0
- package/docs/MEMORY_LIFECYCLE.md +65 -0
- package/docs/TASK_COORDINATION.md +27 -0
- package/package.json +70 -0
- package/scripts/hooks/session-start-resume.cjs +70 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ru" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Waymark — тропа агентов</title>
|
|
7
|
+
<!-- Tailwind vendored locally (no external CDN): offline-capable, no SRI/CDN-compromise risk. -->
|
|
8
|
+
<script src="./vendor/tailwind.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0E1013; --panel: #14171C; --raised: #191E25; --line: #262B33;
|
|
12
|
+
--ink: #D8D4CC; --muted: #8A8F96; --blaze: #E8833A;
|
|
13
|
+
--moss: #8FB573; --amber: #D9A441; --rust: #C4564A; --blue: #7A93B8;
|
|
14
|
+
}
|
|
15
|
+
body { background: var(--bg); color: var(--ink); }
|
|
16
|
+
.mono { font-family: ui-monospace, 'Cascadia Code', Consolas, 'Courier New', monospace; }
|
|
17
|
+
.eyebrow { font-family: ui-monospace, Consolas, monospace; font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: var(--muted); }
|
|
18
|
+
.card { background: var(--panel); border: 1px solid var(--line); }
|
|
19
|
+
.raised { background: var(--raised); }
|
|
20
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
21
|
+
::-webkit-scrollbar-thumb { background: #232830; border-radius: 6px; }
|
|
22
|
+
|
|
23
|
+
/* Trail: the signature element. A route line with waymark nodes. */
|
|
24
|
+
.trail { position: relative; padding-left: 28px; }
|
|
25
|
+
.trail::before { content: ''; position: absolute; left: 9px; top: 6px; bottom: 6px; width: 2px;
|
|
26
|
+
background: repeating-linear-gradient(to bottom, var(--line) 0 6px, transparent 6px 11px); }
|
|
27
|
+
.trail-node { position: relative; }
|
|
28
|
+
.trail-node::before { content: ''; position: absolute; left: -25px; top: 14px; width: 10px; height: 10px;
|
|
29
|
+
border-radius: 9999px; background: var(--bg); border: 2px solid var(--dot, var(--muted)); }
|
|
30
|
+
/* A handoff is a blaze: the painted diamond that marks the trail. */
|
|
31
|
+
.blaze-diamond { display: inline-block; width: 11px; height: 11px; background: var(--blaze);
|
|
32
|
+
transform: rotate(45deg); flex: none; }
|
|
33
|
+
.trail-node.handoff::before { border-radius: 0; transform: rotate(45deg); background: var(--blaze); border-color: var(--blaze); }
|
|
34
|
+
|
|
35
|
+
.pulse { animation: pulse 2.2s ease-in-out infinite; }
|
|
36
|
+
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
|
|
37
|
+
@media (prefers-reduced-motion: reduce) { .pulse { animation: none } }
|
|
38
|
+
|
|
39
|
+
button:focus-visible, select:focus-visible, input:focus-visible, [tabindex]:focus-visible {
|
|
40
|
+
outline: 2px solid var(--blaze); outline-offset: 1px; }
|
|
41
|
+
.num { font-variant-numeric: tabular-nums; }
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body class="font-sans text-[15px]">
|
|
45
|
+
<div class="min-h-screen flex flex-col">
|
|
46
|
+
<header class="border-b px-5 py-3 flex items-center justify-between sticky top-0 z-10 gap-3 flex-wrap"
|
|
47
|
+
style="border-color: var(--line); background: var(--bg);">
|
|
48
|
+
<div class="flex items-center gap-3 min-w-0">
|
|
49
|
+
<span class="blaze-diamond" aria-hidden="true"></span>
|
|
50
|
+
<h1 class="mono font-bold tracking-[.22em] text-base">WAYMARK</h1>
|
|
51
|
+
<span class="eyebrow hidden sm:inline" id="dbPath">тропа агентов</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
54
|
+
<span id="adminBadge" class="hidden eyebrow px-2 py-1 rounded border" style="color: var(--amber); border-color: var(--amber);">admin</span>
|
|
55
|
+
<span class="flex items-center gap-1.5 eyebrow" title="Автообновление: панель следит за изменениями БД">
|
|
56
|
+
<span id="liveDot" class="w-2 h-2 rounded-full pulse" style="background: var(--moss)"></span>
|
|
57
|
+
<span id="liveLabel">live</span>
|
|
58
|
+
</span>
|
|
59
|
+
<select id="projectFilter" aria-label="Фильтр по проекту"
|
|
60
|
+
class="raised border rounded-lg px-3 py-1.5 text-sm max-w-[220px]" style="border-color: var(--line);">
|
|
61
|
+
<option value="">Все проекты</option>
|
|
62
|
+
</select>
|
|
63
|
+
<span id="trStatus" class="text-xs" style="color: var(--muted)"></span>
|
|
64
|
+
<button id="trToggle" title="Перевод содержимого на русский (машинный, кэшируется)"
|
|
65
|
+
class="text-xs px-3 py-1.5 rounded-lg border transition" style="border-color: var(--line); color: var(--muted);">RU</button>
|
|
66
|
+
</div>
|
|
67
|
+
</header>
|
|
68
|
+
|
|
69
|
+
<div class="flex flex-1 min-h-0">
|
|
70
|
+
<nav class="w-44 border-r p-3 space-y-1 shrink-0 hidden md:block" id="nav" style="border-color: var(--line);"></nav>
|
|
71
|
+
<nav class="md:hidden w-full border-b p-2 flex gap-1 overflow-x-auto fixed bottom-0 z-10" id="navMobile"
|
|
72
|
+
style="border-color: var(--line); background: var(--bg);"></nav>
|
|
73
|
+
<main class="flex-1 overflow-auto p-6 pb-20 md:pb-6" id="content">
|
|
74
|
+
<div style="color: var(--muted)">Загрузка…</div>
|
|
75
|
+
</main>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<script>
|
|
80
|
+
const TABS = [
|
|
81
|
+
{ id: 'overview', label: 'Обзор' },
|
|
82
|
+
{ id: 'trail', label: 'Тропа' },
|
|
83
|
+
{ id: 'tasks', label: 'Задачи' },
|
|
84
|
+
{ id: 'memory', label: 'Память' },
|
|
85
|
+
{ id: 'sessions', label: 'Сессии' },
|
|
86
|
+
{ id: 'agents', label: 'Агенты' },
|
|
87
|
+
{ id: 'experiments', label: 'Бенчмарки' },
|
|
88
|
+
];
|
|
89
|
+
let current = 'trail';
|
|
90
|
+
let projectId = '';
|
|
91
|
+
let memQuery = '';
|
|
92
|
+
let memType = '';
|
|
93
|
+
let memStatus = 'active';
|
|
94
|
+
let isAdmin = false;
|
|
95
|
+
|
|
96
|
+
const el = (html) => { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstChild; };
|
|
97
|
+
const esc = (s) => (s == null ? '' : String(s)).replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c]));
|
|
98
|
+
async function api(p, opts) { const r = await fetch(p, opts); if (!r.ok) throw new Error((await r.json().catch(() => null))?.error || await r.text()); return r.json(); }
|
|
99
|
+
const pq = () => projectId ? `?project_id=${encodeURIComponent(projectId)}` : '';
|
|
100
|
+
|
|
101
|
+
const OUTCOME = { completed: 'var(--moss)', partial: 'var(--amber)', blocked: 'var(--rust)' };
|
|
102
|
+
const STATUS_RU = {
|
|
103
|
+
pending: 'в очереди', in_progress: 'в работе', done: 'готово', cancelled: 'отменено',
|
|
104
|
+
active: 'активна', paused: 'пауза', archived: 'архив', superseded: 'заменена',
|
|
105
|
+
stale: 'устарела', completed: 'завершена', blocked: 'блок', retired: 'выведен',
|
|
106
|
+
};
|
|
107
|
+
const statusColor = (s) => ({
|
|
108
|
+
active: 'var(--moss)', done: 'var(--moss)', completed: 'var(--moss)',
|
|
109
|
+
pending: 'var(--amber)', in_progress: 'var(--blaze)', paused: 'var(--muted)',
|
|
110
|
+
archived: 'var(--muted)', superseded: 'var(--muted)', stale: 'var(--amber)',
|
|
111
|
+
blocked: 'var(--rust)', cancelled: 'var(--muted)',
|
|
112
|
+
}[s] || 'var(--muted)');
|
|
113
|
+
|
|
114
|
+
function relTime(s) {
|
|
115
|
+
if (!s) return '—';
|
|
116
|
+
const d = new Date(s.replace(' ', 'T') + (String(s).includes('Z') || String(s).includes('+') ? '' : 'Z'));
|
|
117
|
+
const diff = (Date.now() - d.getTime()) / 1000;
|
|
118
|
+
if (!Number.isFinite(diff)) return '—';
|
|
119
|
+
if (diff < 90) return 'только что';
|
|
120
|
+
if (diff < 5400) return `${Math.round(diff / 60)} мин назад`;
|
|
121
|
+
if (diff < 129600) return `${Math.round(diff / 3600)} ч назад`;
|
|
122
|
+
if (diff < 12 * 86400) return `${Math.round(diff / 86400)} дн назад`;
|
|
123
|
+
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
|
|
124
|
+
}
|
|
125
|
+
const fmtDate = (s) => s ? new Date(s.replace(' ', 'T') + (String(s).includes('Z') || String(s).includes('+') ? '' : 'Z')).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' }) : '—';
|
|
126
|
+
|
|
127
|
+
const initials = (name) => (name || '??').split(/[\s(-]+/).filter(Boolean).slice(0, 2).map(w => w[0].toUpperCase()).join('');
|
|
128
|
+
const avatar = (name, color) => `
|
|
129
|
+
<span class="mono inline-flex items-center justify-center w-7 h-7 rounded-full border text-[11px] shrink-0"
|
|
130
|
+
style="border-color:${color || 'var(--line)'}; color:${color || 'var(--muted)'}" title="${esc(name || 'без имени')}">${esc(initials(name))}</span>`;
|
|
131
|
+
const chip = (text, color) => `<span class="mono text-[11px] px-2 py-0.5 rounded-full border" style="border-color:${color};color:${color}">${esc(text)}</span>`;
|
|
132
|
+
|
|
133
|
+
function title(t, sub) { return `<div class="mb-5"><div class="eyebrow mb-1">waymark / ${esc(current)}</div><h2 class="text-xl font-semibold">${esc(t)}</h2>${sub ? `<p class="text-sm mt-0.5" style="color:var(--muted)">${esc(sub)}</p>` : ''}</div>`; }
|
|
134
|
+
function empty(t, hint) { return `<div class="card rounded-xl p-10 text-center"><div style="color:var(--muted)">${esc(t)}</div>${hint ? `<div class="text-xs mt-2" style="color:var(--muted)">${esc(hint)}</div>` : ''}</div>`; }
|
|
135
|
+
|
|
136
|
+
function renderNav() {
|
|
137
|
+
for (const navEl of [document.getElementById('nav'), document.getElementById('navMobile')]) {
|
|
138
|
+
navEl.innerHTML = '';
|
|
139
|
+
for (const t of TABS) {
|
|
140
|
+
const active = t.id === current;
|
|
141
|
+
navEl.appendChild(el(`
|
|
142
|
+
<button data-tab="${t.id}" class="text-left px-3 py-2 rounded-lg text-sm flex items-center gap-2 transition w-full md:w-auto whitespace-nowrap"
|
|
143
|
+
style="${active ? 'background:var(--raised);color:var(--ink);border:1px solid var(--line)' : 'color:var(--muted);border:1px solid transparent'}">
|
|
144
|
+
<span class="inline-block w-2 h-2 ${t.id === 'trail' ? '' : 'rounded-full'}"
|
|
145
|
+
style="background:${active ? 'var(--blaze)' : 'var(--line)'};${t.id === 'trail' ? 'transform:rotate(45deg)' : ''}"></span>
|
|
146
|
+
${t.label}
|
|
147
|
+
</button>`));
|
|
148
|
+
}
|
|
149
|
+
navEl.querySelectorAll('button').forEach(b => b.onclick = () => { current = b.dataset.tab; renderNav(); route(); });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function route() {
|
|
154
|
+
const c = document.getElementById('content');
|
|
155
|
+
c.innerHTML = '<div style="color:var(--muted)">Загрузка…</div>';
|
|
156
|
+
try {
|
|
157
|
+
const r = {
|
|
158
|
+
overview: renderOverview, trail: renderTrail, tasks: renderTasks,
|
|
159
|
+
memory: renderMemory, sessions: renderSessions, agents: renderAgents,
|
|
160
|
+
experiments: renderExperiments,
|
|
161
|
+
}[current];
|
|
162
|
+
if (r) await r(c);
|
|
163
|
+
await translateSection();
|
|
164
|
+
} catch (e) {
|
|
165
|
+
c.innerHTML = `<div class="card rounded-xl p-6" style="color:var(--rust)">Не удалось загрузить данные: ${esc(e.message)}. Проверьте, что хаб-БД доступна, и обновите страницу.</div>`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- Обзор ---------------------------------------------------------------
|
|
170
|
+
async function renderOverview(c) {
|
|
171
|
+
const [ov, trail] = await Promise.all([api('/api/overview'), api('/api/trail' + pq())]);
|
|
172
|
+
const tiles = Object.entries({ 'проекты': ov.counts.projects, 'задачи': ov.counts.tasks, 'память': ov.counts.memory, 'сессии': ov.counts.sessions, 'агенты': ov.counts.agents, 'бенчмарки': ov.counts.experiments })
|
|
173
|
+
.map(([k, v]) => `<div class="card rounded-xl p-4"><div class="mono num text-2xl">${v}</div><div class="eyebrow mt-1">${k}</div></div>`).join('');
|
|
174
|
+
const byStatus = ov.tasks_by_status.map(r => `
|
|
175
|
+
<div class="flex items-center gap-3 py-1.5">
|
|
176
|
+
<span class="w-24 text-sm" style="color:var(--muted)">${esc(STATUS_RU[r.status] || r.status)}</span>
|
|
177
|
+
<div class="flex-1 h-1.5 rounded-full overflow-hidden" style="background:var(--raised)">
|
|
178
|
+
<div class="h-full rounded-full" style="width:${Math.min(100, r.c / Math.max(1, ov.counts.tasks) * 100)}%;background:${statusColor(r.status)}"></div>
|
|
179
|
+
</div>
|
|
180
|
+
<span class="mono num text-sm w-8 text-right">${r.c}</span>
|
|
181
|
+
</div>`).join('');
|
|
182
|
+
const activeHandoffs = trail.handoffs.filter(h => h.status === 'active');
|
|
183
|
+
c.innerHTML = `
|
|
184
|
+
${title('Обзор', 'Состояние общего хаба памяти')}
|
|
185
|
+
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 mb-6">${tiles}</div>
|
|
186
|
+
<div class="grid lg:grid-cols-2 gap-4">
|
|
187
|
+
<div class="card rounded-xl p-5">
|
|
188
|
+
<div class="eyebrow mb-3">задачи по статусам</div>
|
|
189
|
+
${byStatus || '<div style="color:var(--muted)" class="text-sm">Задач пока нет</div>'}
|
|
190
|
+
</div>
|
|
191
|
+
<div class="card rounded-xl p-5">
|
|
192
|
+
<div class="eyebrow mb-3">активные передачи</div>
|
|
193
|
+
${activeHandoffs.length ? activeHandoffs.map(h => `
|
|
194
|
+
<div class="flex items-start gap-3 py-2">
|
|
195
|
+
<span class="blaze-diamond mt-1.5"></span>
|
|
196
|
+
<div class="min-w-0">
|
|
197
|
+
<div class="text-sm truncate" data-tr>${esc(h.description || h.name)}</div>
|
|
198
|
+
<div class="eyebrow mt-0.5">${esc(h.project_name || 'глобально')} · ${relTime(h.updated_at)}</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>`).join('') : '<div class="text-sm" style="color:var(--muted)">Открытых передач нет — все тропы пройдены до конца.</div>'}
|
|
201
|
+
</div>
|
|
202
|
+
</div>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---- Тропа (signature) ----------------------------------------------------
|
|
206
|
+
async function renderTrail(c) {
|
|
207
|
+
const t = await api('/api/trail' + pq());
|
|
208
|
+
const active = t.handoffs.find(h => h.status === 'active');
|
|
209
|
+
const handoffBySession = new Map(t.handoffs.map(h => [h.origin_session, h]));
|
|
210
|
+
const pinned = active ? `
|
|
211
|
+
<div class="card rounded-xl p-5 mb-6" style="border-left: 3px solid var(--blaze)">
|
|
212
|
+
<div class="flex items-center gap-2 mb-2">
|
|
213
|
+
<span class="blaze-diamond"></span>
|
|
214
|
+
<span class="eyebrow" style="color:var(--blaze)">эстафета ждёт — продолжить отсюда</span>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="text-sm mb-2" data-tr>${esc(active.description || '')}</div>
|
|
217
|
+
<pre class="mono text-[12.5px] whitespace-pre-wrap leading-relaxed p-3 rounded-lg raised" data-tr>${esc(active.body || '')}</pre>
|
|
218
|
+
<div class="eyebrow mt-2">${esc(active.agent_name || active.created_by_agent || 'агент не назван')} · ${relTime(active.updated_at)}${isAdmin ? ` · <button class="underline" style="color:var(--muted)" onclick="adminMemory('${esc(active.id)}','archived')">погасить</button>` : ''}</div>
|
|
219
|
+
</div>` : '';
|
|
220
|
+
const nodes = t.sessions.map(s => {
|
|
221
|
+
const color = OUTCOME[s.outcome] || 'var(--muted)';
|
|
222
|
+
const h = handoffBySession.get(s.id);
|
|
223
|
+
return `
|
|
224
|
+
<div class="trail-node pb-5 ${h ? 'handoff' : ''}" style="--dot:${color}">
|
|
225
|
+
<div class="card rounded-xl p-4">
|
|
226
|
+
<div class="flex items-center gap-2.5 flex-wrap">
|
|
227
|
+
${avatar(s.agent_name || s.agent_id || s.client, color)}
|
|
228
|
+
<span class="text-sm font-medium">${esc(s.agent_name || s.agent_id || s.client || s.surface)}</span>
|
|
229
|
+
${chip(STATUS_RU[s.outcome] || s.outcome || '—', color)}
|
|
230
|
+
${!projectId && s.project_name ? chip(s.project_name, 'var(--blue)') : ''}
|
|
231
|
+
<span class="eyebrow ml-auto" title="${esc(fmtDate(s.ended_at || s.started_at))}">${relTime(s.ended_at || s.started_at)}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="text-sm mt-2.5 leading-relaxed" data-tr>${esc(s.summary || '')}</div>
|
|
234
|
+
<div class="flex gap-2 mt-2.5 flex-wrap">
|
|
235
|
+
${s.commits_made.length ? chip(`коммиты: ${s.commits_made.length}`, 'var(--muted)') : ''}
|
|
236
|
+
${s.files_touched.length ? chip(`файлы: ${s.files_touched.length}`, 'var(--muted)') : ''}
|
|
237
|
+
${h ? chip(h.status === 'active' ? 'передал эстафету' : 'эстафета принята', h.status === 'active' ? 'var(--blaze)' : 'var(--muted)') : ''}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>`;
|
|
241
|
+
}).join('');
|
|
242
|
+
c.innerHTML = `
|
|
243
|
+
${title('Тропа', projectId ? 'Кто прошёл этот маршрут и что оставил следующему' : 'Последние сессии по всем проектам')}
|
|
244
|
+
${pinned}
|
|
245
|
+
${t.sessions.length ? `<div class="trail">${nodes}</div>` : empty('На этой тропе ещё нет меток', 'Сессии появятся после первого session_log от любого агента.')}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- Задачи (доска) --------------------------------------------------------
|
|
249
|
+
async function renderTasks(c) {
|
|
250
|
+
const rows = await api('/api/tasks' + pq());
|
|
251
|
+
const COLS = [['pending', 'В очереди'], ['in_progress', 'В работе'], ['done', 'Готово'], ['cancelled', 'Отменено']];
|
|
252
|
+
const cols = COLS.map(([st, label]) => {
|
|
253
|
+
const items = rows.filter(r => r.status === st);
|
|
254
|
+
return `
|
|
255
|
+
<div class="min-w-[260px] flex-1">
|
|
256
|
+
<div class="flex items-center gap-2 mb-3">
|
|
257
|
+
<span class="w-2 h-2 rounded-full" style="background:${statusColor(st)}"></span>
|
|
258
|
+
<span class="eyebrow">${label}</span>
|
|
259
|
+
<span class="mono num text-xs" style="color:var(--muted)">${items.length}</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="space-y-3">
|
|
262
|
+
${items.map(r => `
|
|
263
|
+
<div class="card rounded-xl p-4">
|
|
264
|
+
<div class="text-sm font-medium leading-snug" data-tr>${esc(r.title)}</div>
|
|
265
|
+
${r.description ? `<div class="text-[13px] mt-1.5 leading-relaxed" style="color:var(--muted)" data-tr>${esc(r.description.slice(0, 220))}${r.description.length > 220 ? '…' : ''}</div>` : ''}
|
|
266
|
+
<div class="flex gap-2 mt-3 flex-wrap items-center">
|
|
267
|
+
${!projectId && r.project_name ? chip(r.project_name, 'var(--blue)') : ''}
|
|
268
|
+
${r.assigned_to || r.claimed_agent_name ? chip(r.claimed_agent_name || r.assigned_to, 'var(--muted)') : ''}
|
|
269
|
+
${r.priority !== 50 ? chip(`p${r.priority}`, r.priority > 50 ? 'var(--amber)' : 'var(--muted)') : ''}
|
|
270
|
+
${r.blocker ? chip('блокер', 'var(--rust)') : ''}
|
|
271
|
+
</div>
|
|
272
|
+
${isAdmin && (st === 'pending' || st === 'in_progress') ? `
|
|
273
|
+
<div class="flex gap-2 mt-3">
|
|
274
|
+
${st === 'pending' ? `<button class="text-xs px-2 py-1 rounded border" style="border-color:var(--line);color:var(--blaze)" onclick="adminTask('${esc(r.id)}','in_progress')">в работу</button>` : ''}
|
|
275
|
+
<button class="text-xs px-2 py-1 rounded border" style="border-color:var(--line);color:var(--moss)" onclick="adminTask('${esc(r.id)}','done')">готово</button>
|
|
276
|
+
<button class="text-xs px-2 py-1 rounded border" style="border-color:var(--line);color:var(--muted)" onclick="adminTask('${esc(r.id)}','cancelled')">отменить</button>
|
|
277
|
+
</div>` : ''}
|
|
278
|
+
</div>`).join('') || `<div class="text-xs px-1" style="color:var(--muted)">пусто</div>`}
|
|
279
|
+
</div>
|
|
280
|
+
</div>`;
|
|
281
|
+
}).join('');
|
|
282
|
+
c.innerHTML = `${title('Задачи', 'Очередь координации: claim забирает задачу атомарно')}<div class="flex gap-4 overflow-x-auto pb-2">${cols}</div>`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---- Память -----------------------------------------------------------------
|
|
286
|
+
async function renderMemory(c) {
|
|
287
|
+
const params = new URLSearchParams();
|
|
288
|
+
if (projectId) params.set('project_id', projectId);
|
|
289
|
+
if (memQuery) params.set('q', memQuery);
|
|
290
|
+
if (memType) params.set('type', memType);
|
|
291
|
+
if (memStatus) params.set('status', memStatus);
|
|
292
|
+
const rows = await api('/api/memory?' + params);
|
|
293
|
+
const TYPES = ['', 'handoff', 'decision', 'project', 'user', 'feedback', 'reference'];
|
|
294
|
+
c.innerHTML = `
|
|
295
|
+
${title('Память', 'Общие знания: решения, факты, передачи')}
|
|
296
|
+
<div class="flex gap-2 mb-4 flex-wrap items-center">
|
|
297
|
+
<input id="memSearch" value="${esc(memQuery)}" placeholder="Поиск по памяти…" aria-label="Поиск по памяти"
|
|
298
|
+
class="raised border rounded-lg px-3 py-1.5 text-sm w-60" style="border-color:var(--line)" />
|
|
299
|
+
${TYPES.map(t => `<button data-type="${t}" class="text-xs px-2.5 py-1 rounded-full border mono"
|
|
300
|
+
style="border-color:${memType === t ? 'var(--blaze)' : 'var(--line)'};color:${memType === t ? 'var(--blaze)' : 'var(--muted)'}">${t || 'все типы'}</button>`).join('')}
|
|
301
|
+
<select id="memStatus" class="raised border rounded-lg px-2 py-1 text-xs ml-auto" style="border-color:var(--line)" aria-label="Статус записей">
|
|
302
|
+
${['active', 'superseded', 'stale', 'archived', ''].map(s => `<option value="${s}" ${memStatus === s ? 'selected' : ''}>${s ? (STATUS_RU[s] || s) : 'любой статус'}</option>`).join('')}
|
|
303
|
+
</select>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="space-y-3">
|
|
306
|
+
${rows.map(r => `
|
|
307
|
+
<details class="card rounded-xl">
|
|
308
|
+
<summary class="p-4 cursor-pointer list-none flex items-center gap-3 flex-wrap">
|
|
309
|
+
${r.type === 'handoff' ? '<span class="blaze-diamond"></span>' : `<span class="w-2 h-2 rounded-full shrink-0" style="background:${statusColor(r.status)}"></span>`}
|
|
310
|
+
<span class="mono text-[13px]">${esc(r.name)}</span>
|
|
311
|
+
${chip(r.type, r.type === 'handoff' ? 'var(--blaze)' : 'var(--blue)')}
|
|
312
|
+
${r.status !== 'active' ? chip(STATUS_RU[r.status] || r.status, 'var(--muted)') : ''}
|
|
313
|
+
<span class="flex-1 text-sm truncate" style="color:var(--muted)" data-tr>${esc(r.description || '')}</span>
|
|
314
|
+
<span class="mono num text-[11px]" style="color:var(--muted)" title="важность / уверенность">${r.importance}/${r.confidence}</span>
|
|
315
|
+
<span class="eyebrow">${relTime(r.updated_at)}</span>
|
|
316
|
+
</summary>
|
|
317
|
+
<div class="px-4 pb-4">
|
|
318
|
+
<pre class="mono text-[12.5px] whitespace-pre-wrap leading-relaxed p-3 rounded-lg raised" data-tr-body>${esc(r.body || '')}</pre>
|
|
319
|
+
<div class="flex gap-2 mt-2 items-center flex-wrap">
|
|
320
|
+
${(r.tags || []).map(t => chip('#' + t, 'var(--muted)')).join('')}
|
|
321
|
+
<span class="eyebrow ml-auto">${esc(r.created_by_agent || r.surface || '')}</span>
|
|
322
|
+
${isAdmin && r.status === 'active' ? `<button class="text-xs px-2 py-1 rounded border" style="border-color:var(--line);color:var(--muted)" onclick="adminMemory('${esc(r.id)}','archived')">архивировать</button>` : ''}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</details>`).join('') || empty('Ничего не найдено', 'Измените фильтры или поисковый запрос.')}
|
|
326
|
+
</div>`;
|
|
327
|
+
const search = document.getElementById('memSearch');
|
|
328
|
+
let deb;
|
|
329
|
+
search.oninput = () => { clearTimeout(deb); deb = setTimeout(() => { memQuery = search.value.trim(); route(); }, 350); };
|
|
330
|
+
document.getElementById('memStatus').onchange = (e) => { memStatus = e.target.value; route(); };
|
|
331
|
+
c.querySelectorAll('[data-type]').forEach(b => b.onclick = () => { memType = b.dataset.type; route(); });
|
|
332
|
+
c.querySelectorAll('details').forEach(d => d.addEventListener('toggle', () => translateBody(d)));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---- Сессии ------------------------------------------------------------------
|
|
336
|
+
async function renderSessions(c) {
|
|
337
|
+
const rows = await api('/api/sessions' + pq());
|
|
338
|
+
c.innerHTML = `
|
|
339
|
+
${title('Сессии', 'Журнал: кто работал, что сделал, чем закончил')}
|
|
340
|
+
<div class="space-y-3">
|
|
341
|
+
${rows.map(s => `
|
|
342
|
+
<div class="card rounded-xl p-4">
|
|
343
|
+
<div class="flex items-center gap-2.5 flex-wrap">
|
|
344
|
+
${avatar(s.agent_name || s.agent_id || s.client, OUTCOME[s.outcome] || 'var(--muted)')}
|
|
345
|
+
<span class="text-sm font-medium">${esc(s.agent_name || s.agent_id || s.client || s.surface)}</span>
|
|
346
|
+
${s.model ? chip(s.model, 'var(--muted)') : ''}
|
|
347
|
+
${chip(STATUS_RU[s.outcome] || s.outcome || '—', OUTCOME[s.outcome] || 'var(--muted)')}
|
|
348
|
+
${!projectId && s.project_name ? chip(s.project_name, 'var(--blue)') : ''}
|
|
349
|
+
<span class="eyebrow ml-auto" title="${esc(fmtDate(s.started_at))}">${relTime(s.ended_at || s.started_at)}</span>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="text-sm mt-2.5 leading-relaxed" data-tr>${esc(s.summary || '')}</div>
|
|
352
|
+
${s.commits_made.length ? `<div class="mono text-[11px] mt-2" style="color:var(--muted)">коммиты: ${s.commits_made.map(esc).join(', ')}</div>` : ''}
|
|
353
|
+
</div>`).join('') || empty('Сессий пока нет')}
|
|
354
|
+
</div>`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---- Агенты --------------------------------------------------------------------
|
|
358
|
+
async function renderAgents(c) {
|
|
359
|
+
const rows = await api('/api/agents');
|
|
360
|
+
c.innerHTML = `
|
|
361
|
+
${title('Агенты', 'Зарегистрированные участники команды')}
|
|
362
|
+
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
363
|
+
${rows.map(a => `
|
|
364
|
+
<div class="card rounded-xl p-4">
|
|
365
|
+
<div class="flex items-center gap-3">
|
|
366
|
+
${avatar(a.display_name, statusColor(a.status))}
|
|
367
|
+
<div class="min-w-0">
|
|
368
|
+
<div class="text-sm font-medium truncate">${esc(a.display_name)}</div>
|
|
369
|
+
<div class="eyebrow mt-0.5">${esc([a.provider, a.model, a.client].filter(Boolean).join(' · '))}</div>
|
|
370
|
+
</div>
|
|
371
|
+
<span class="ml-auto">${chip(STATUS_RU[a.status] || a.status, statusColor(a.status))}</span>
|
|
372
|
+
</div>
|
|
373
|
+
${(a.capabilities || []).length ? `<div class="flex gap-1.5 mt-3 flex-wrap">${a.capabilities.map(cp => chip(cp, 'var(--muted)')).join('')}</div>` : ''}
|
|
374
|
+
</div>`).join('') || empty('Агенты ещё не регистрировались', 'agent_register доступен в профиле HUB_TOOLS=full.')}
|
|
375
|
+
</div>`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---- Бенчмарки --------------------------------------------------------------------
|
|
379
|
+
async function renderExperiments(c) {
|
|
380
|
+
const rows = await api('/api/experiments');
|
|
381
|
+
c.innerHTML = `
|
|
382
|
+
${title('Бенчмарки', 'A/B: сколько токенов экономит хаб')}
|
|
383
|
+
<div class="space-y-3">
|
|
384
|
+
${rows.map(e => `
|
|
385
|
+
<div class="card rounded-xl p-4 flex items-center gap-3 flex-wrap">
|
|
386
|
+
<span class="mono text-[13px]">${esc(e.name)}</span>
|
|
387
|
+
${chip(e.scenario, 'var(--blue)')}
|
|
388
|
+
${chip(STATUS_RU[e.status] || e.status, statusColor(e.status))}
|
|
389
|
+
${e.project_name ? chip(e.project_name, 'var(--muted)') : ''}
|
|
390
|
+
<span class="text-sm flex-1" style="color:var(--muted)" data-tr>${esc(e.description || '')}</span>
|
|
391
|
+
<span class="eyebrow">${relTime(e.created_at)}</span>
|
|
392
|
+
</div>`).join('') || empty('Экспериментов нет', 'Создайте через npm run benchmark -- create.')}
|
|
393
|
+
</div>`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---- Admin actions -----------------------------------------------------------
|
|
397
|
+
async function adminTask(id, status) {
|
|
398
|
+
try { await api('/api/admin/task-status', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, status }) }); route(); }
|
|
399
|
+
catch (e) { alert('Не удалось изменить статус: ' + e.message); }
|
|
400
|
+
}
|
|
401
|
+
async function adminMemory(id, status) {
|
|
402
|
+
try { await api('/api/admin/memory-status', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, status }) }); route(); }
|
|
403
|
+
catch (e) { alert('Не удалось изменить статус: ' + e.message); }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---- Live refresh --------------------------------------------------------------
|
|
407
|
+
let dataVersion = null;
|
|
408
|
+
async function pollLive() {
|
|
409
|
+
try {
|
|
410
|
+
const s = await api('/api/stats');
|
|
411
|
+
document.getElementById('liveDot').style.background = 'var(--moss)';
|
|
412
|
+
document.getElementById('liveLabel').textContent = 'live';
|
|
413
|
+
if (dataVersion !== null && s.data_version !== dataVersion) route();
|
|
414
|
+
dataVersion = s.data_version;
|
|
415
|
+
} catch {
|
|
416
|
+
document.getElementById('liveDot').style.background = 'var(--rust)';
|
|
417
|
+
document.getElementById('liveLabel').textContent = 'нет связи';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
setInterval(pollLive, 4000);
|
|
421
|
+
|
|
422
|
+
// ---- Translation (opt-in, server-cached) ----------------------------------------
|
|
423
|
+
let translateOn = localStorage.getItem('dash_translate') === '1';
|
|
424
|
+
const trCache = new Map();
|
|
425
|
+
const setStatus = (t, color) => {
|
|
426
|
+
const s = document.getElementById('trStatus');
|
|
427
|
+
if (s) { s.textContent = t; s.style.color = color || 'var(--muted)'; }
|
|
428
|
+
};
|
|
429
|
+
async function translateTexts(texts) {
|
|
430
|
+
const need = [...new Set(texts)].filter(t => t && !trCache.has(t));
|
|
431
|
+
if (need.length) {
|
|
432
|
+
setStatus('перевожу…');
|
|
433
|
+
try {
|
|
434
|
+
const r = await fetch('/api/translate', {
|
|
435
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
436
|
+
body: JSON.stringify({ texts: need }),
|
|
437
|
+
});
|
|
438
|
+
if (!r.ok) { setStatus(`сервер ${r.status} — перезапустите npm run dashboard`, 'var(--rust)'); return mapFrom(texts); }
|
|
439
|
+
const data = await r.json();
|
|
440
|
+
for (const [src, tr] of Object.entries(data.translations || {})) trCache.set(src, tr);
|
|
441
|
+
if (data.error === 'no_key') {
|
|
442
|
+
setStatus('нет ключа DeepSeek', 'var(--amber)');
|
|
443
|
+
document.getElementById('trToggle').title = 'Не найден ключ DeepSeek (env DEEPSEEK_API_KEY или ~/.deepseek-code/settings.json)';
|
|
444
|
+
} else if (data.error) {
|
|
445
|
+
setStatus('ошибка перевода: ' + data.error, 'var(--rust)');
|
|
446
|
+
} else {
|
|
447
|
+
setStatus('переведено', 'var(--moss)');
|
|
448
|
+
setTimeout(() => { if (translateOn) setStatus(''); }, 1500);
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
setStatus('нет связи с сервером перевода', 'var(--rust)');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return mapFrom(texts);
|
|
455
|
+
}
|
|
456
|
+
function mapFrom(texts) {
|
|
457
|
+
const map = {};
|
|
458
|
+
for (const t of texts) if (trCache.has(t)) map[t] = trCache.get(t);
|
|
459
|
+
return map;
|
|
460
|
+
}
|
|
461
|
+
async function translateSection() {
|
|
462
|
+
if (!translateOn) return;
|
|
463
|
+
const els = [...document.querySelectorAll('[data-tr]')];
|
|
464
|
+
if (!els.length) return;
|
|
465
|
+
const map = await translateTexts(els.map(e => e.textContent.trim()));
|
|
466
|
+
for (const e of els) { const tr = map[e.textContent.trim()]; if (tr) e.textContent = tr; }
|
|
467
|
+
}
|
|
468
|
+
// Memory bodies are large — translate only when the user expands them.
|
|
469
|
+
async function translateBody(d) {
|
|
470
|
+
if (!translateOn || !d.open) return;
|
|
471
|
+
const pre = d.querySelector('[data-tr-body]');
|
|
472
|
+
if (!pre) return;
|
|
473
|
+
const o = pre.textContent.trim();
|
|
474
|
+
const map = await translateTexts([o]);
|
|
475
|
+
if (map[o]) pre.textContent = map[o];
|
|
476
|
+
}
|
|
477
|
+
function syncTrButton() {
|
|
478
|
+
const b = document.getElementById('trToggle');
|
|
479
|
+
b.style.color = translateOn ? 'var(--blaze)' : 'var(--muted)';
|
|
480
|
+
b.style.borderColor = translateOn ? 'var(--blaze)' : 'var(--line)';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---- Init ------------------------------------------------------------------------
|
|
484
|
+
async function init() {
|
|
485
|
+
renderNav();
|
|
486
|
+
syncTrButton();
|
|
487
|
+
document.getElementById('trToggle').onclick = () => {
|
|
488
|
+
if (!translateOn && !localStorage.getItem('dash_translate_ack')) {
|
|
489
|
+
const ok = confirm(
|
|
490
|
+
'Перевод отправляет тексты памяти, задач и сессий на внешний сервис (api.deepseek.com).\n' +
|
|
491
|
+
'Не включайте, если в хабе есть чувствительные данные. Продолжить?'
|
|
492
|
+
);
|
|
493
|
+
if (!ok) return;
|
|
494
|
+
localStorage.setItem('dash_translate_ack', '1');
|
|
495
|
+
}
|
|
496
|
+
translateOn = !translateOn;
|
|
497
|
+
localStorage.setItem('dash_translate', translateOn ? '1' : '0');
|
|
498
|
+
syncTrButton();
|
|
499
|
+
if (!translateOn) setStatus('');
|
|
500
|
+
route();
|
|
501
|
+
};
|
|
502
|
+
try {
|
|
503
|
+
const caps = await api('/api/capabilities');
|
|
504
|
+
isAdmin = caps.admin;
|
|
505
|
+
if (isAdmin) document.getElementById('adminBadge').classList.remove('hidden');
|
|
506
|
+
document.getElementById('dbPath').textContent = caps.db_path;
|
|
507
|
+
} catch {}
|
|
508
|
+
try {
|
|
509
|
+
const projects = await api('/api/projects');
|
|
510
|
+
const sel = document.getElementById('projectFilter');
|
|
511
|
+
for (const p of projects) sel.appendChild(el(`<option value="${esc(p.id)}">${esc(p.name)}</option>`));
|
|
512
|
+
sel.onchange = () => { projectId = sel.value; route(); };
|
|
513
|
+
} catch {}
|
|
514
|
+
pollLive();
|
|
515
|
+
route();
|
|
516
|
+
}
|
|
517
|
+
init();
|
|
518
|
+
</script>
|
|
519
|
+
</body>
|
|
520
|
+
</html>
|