clementine-agent 1.18.92 → 1.18.93
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/dist/agent/agent-manager.js +6 -0
- package/dist/cli/dashboard.js +359 -1
- package/package.json +1 -1
|
@@ -304,6 +304,8 @@ export class AgentManager {
|
|
|
304
304
|
frontmatter.allowedUsers = config.allowedUsers;
|
|
305
305
|
if (config.project)
|
|
306
306
|
frontmatter.project = config.project;
|
|
307
|
+
if (config.projects?.length)
|
|
308
|
+
frontmatter.projects = config.projects;
|
|
307
309
|
if (config.discordToken) {
|
|
308
310
|
storeAgentSecret(slug, 'DISCORD_TOKEN', config.discordToken);
|
|
309
311
|
frontmatter.discordToken = 'keychain';
|
|
@@ -415,6 +417,10 @@ export class AgentManager {
|
|
|
415
417
|
meta.allowedUsers = changes.allowedUsers;
|
|
416
418
|
if (changes.project !== undefined)
|
|
417
419
|
meta.project = changes.project;
|
|
420
|
+
if (changes.projects !== undefined) {
|
|
421
|
+
// Empty array clears the field; non-empty array replaces it.
|
|
422
|
+
meta.projects = changes.projects.length ? changes.projects : undefined;
|
|
423
|
+
}
|
|
418
424
|
if (changes.discordToken !== undefined) {
|
|
419
425
|
if (changes.discordToken) {
|
|
420
426
|
storeAgentSecret(slug, 'DISCORD_TOKEN', changes.discordToken);
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -7472,6 +7472,88 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7472
7472
|
res.status(500).json({ error: String(err) });
|
|
7473
7473
|
}
|
|
7474
7474
|
});
|
|
7475
|
+
// ── Clementine main-agent profile ──────────────────────────────────
|
|
7476
|
+
// Mirrors the per-agent edit surface (Tasks → Team → Edit) for
|
|
7477
|
+
// Clementine herself. Persists to clementine.json under
|
|
7478
|
+
// assistant.profile so existing readers (computeEffectiveConfig,
|
|
7479
|
+
// gateway initializers) can resolve the values via the standard
|
|
7480
|
+
// env > json > default chain.
|
|
7481
|
+
app.get('/api/clementine/profile', (_req, res) => {
|
|
7482
|
+
try {
|
|
7483
|
+
const json = loadClementineJson(BASE_DIR);
|
|
7484
|
+
const profile = json.assistant?.profile ?? {};
|
|
7485
|
+
const status = getApiConnectionStatus();
|
|
7486
|
+
res.json({
|
|
7487
|
+
name: json.assistantName || 'Clementine',
|
|
7488
|
+
profile: {
|
|
7489
|
+
systemPrompt: profile.systemPrompt ?? '',
|
|
7490
|
+
model: profile.model ?? '',
|
|
7491
|
+
allowedTools: profile.allowedTools ?? [],
|
|
7492
|
+
allowedProjects: profile.allowedProjects ?? [],
|
|
7493
|
+
allowedUsers: profile.allowedUsers ?? [],
|
|
7494
|
+
channels: profile.channels ?? [],
|
|
7495
|
+
budgetMonthlyCents: profile.budgetMonthlyCents ?? 0,
|
|
7496
|
+
goalSlugs: profile.goalSlugs ?? [],
|
|
7497
|
+
sendPolicy: profile.sendPolicy ?? null,
|
|
7498
|
+
},
|
|
7499
|
+
connectivity: status,
|
|
7500
|
+
});
|
|
7501
|
+
}
|
|
7502
|
+
catch (err) {
|
|
7503
|
+
res.status(500).json({ error: String(err) });
|
|
7504
|
+
}
|
|
7505
|
+
});
|
|
7506
|
+
app.put('/api/clementine/profile', (req, res) => {
|
|
7507
|
+
try {
|
|
7508
|
+
const body = (req.body ?? {});
|
|
7509
|
+
const profile = {};
|
|
7510
|
+
if (typeof body.systemPrompt === 'string')
|
|
7511
|
+
profile.systemPrompt = body.systemPrompt;
|
|
7512
|
+
if (typeof body.model === 'string' && body.model)
|
|
7513
|
+
profile.model = body.model;
|
|
7514
|
+
if (Array.isArray(body.allowedTools))
|
|
7515
|
+
profile.allowedTools = body.allowedTools.map(String);
|
|
7516
|
+
if (Array.isArray(body.allowedProjects))
|
|
7517
|
+
profile.allowedProjects = body.allowedProjects.map(String);
|
|
7518
|
+
if (Array.isArray(body.allowedUsers))
|
|
7519
|
+
profile.allowedUsers = body.allowedUsers.map(String);
|
|
7520
|
+
if (Array.isArray(body.channels))
|
|
7521
|
+
profile.channels = body.channels.map(String);
|
|
7522
|
+
if (Array.isArray(body.goalSlugs))
|
|
7523
|
+
profile.goalSlugs = body.goalSlugs.map(String);
|
|
7524
|
+
if (typeof body.budgetMonthlyCents === 'number' && body.budgetMonthlyCents >= 0) {
|
|
7525
|
+
profile.budgetMonthlyCents = body.budgetMonthlyCents;
|
|
7526
|
+
}
|
|
7527
|
+
if (body.sendPolicy && typeof body.sendPolicy === 'object') {
|
|
7528
|
+
const sp = body.sendPolicy;
|
|
7529
|
+
const cleaned = {};
|
|
7530
|
+
if (typeof sp.maxDailyEmails === 'number')
|
|
7531
|
+
cleaned.maxDailyEmails = sp.maxDailyEmails;
|
|
7532
|
+
if (typeof sp.requiresApproval === 'string'
|
|
7533
|
+
&& ['none', 'first-in-sequence', 'all'].includes(sp.requiresApproval)) {
|
|
7534
|
+
cleaned.requiresApproval = sp.requiresApproval;
|
|
7535
|
+
}
|
|
7536
|
+
if (typeof sp.businessHoursOnly === 'boolean')
|
|
7537
|
+
cleaned.businessHoursOnly = sp.businessHoursOnly;
|
|
7538
|
+
if (Object.keys(cleaned).length)
|
|
7539
|
+
profile.sendPolicy = cleaned;
|
|
7540
|
+
}
|
|
7541
|
+
const next = updateClementineJson(BASE_DIR, (current) => ({
|
|
7542
|
+
...current,
|
|
7543
|
+
assistant: {
|
|
7544
|
+
...(current.assistant ?? {}),
|
|
7545
|
+
profile: {
|
|
7546
|
+
...(current.assistant?.profile ?? {}),
|
|
7547
|
+
...profile,
|
|
7548
|
+
},
|
|
7549
|
+
},
|
|
7550
|
+
}));
|
|
7551
|
+
res.json({ ok: true, profile: next.assistant?.profile ?? {} });
|
|
7552
|
+
}
|
|
7553
|
+
catch (err) {
|
|
7554
|
+
res.status(400).json({ error: String(err) });
|
|
7555
|
+
}
|
|
7556
|
+
});
|
|
7475
7557
|
app.get('/api/budgets', async (_req, res) => {
|
|
7476
7558
|
try {
|
|
7477
7559
|
const [{ computeEffectiveConfig }, { runDoctor }] = await Promise.all([
|
|
@@ -10347,7 +10429,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10347
10429
|
try {
|
|
10348
10430
|
const gw = await getGateway();
|
|
10349
10431
|
const mgr = gw.getAgentManager();
|
|
10350
|
-
const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
|
|
10432
|
+
const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, projects, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
|
|
10351
10433
|
if (!name || !description) {
|
|
10352
10434
|
res.status(400).json({ error: 'name and description are required' });
|
|
10353
10435
|
return;
|
|
@@ -10363,6 +10445,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10363
10445
|
allowedTools: allowedTools || undefined,
|
|
10364
10446
|
allowedUsers: allowedUsers || undefined,
|
|
10365
10447
|
project: project || undefined,
|
|
10448
|
+
// `projects` is the multi-project access list — agent-manager already
|
|
10449
|
+
// persists it to agent.md frontmatter and the SDK reads it on load.
|
|
10450
|
+
projects: Array.isArray(projects) && projects.length ? projects : undefined,
|
|
10366
10451
|
discordToken: discordToken || undefined,
|
|
10367
10452
|
discordChannelId: discordChannelId || undefined,
|
|
10368
10453
|
avatar: avatar || undefined,
|
|
@@ -15365,6 +15450,131 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15365
15450
|
font-size: 11px;
|
|
15366
15451
|
color: var(--text-muted);
|
|
15367
15452
|
}
|
|
15453
|
+
/* PRD §12 / 1.18.93: three mini-dashboards beneath the Health Strip:
|
|
15454
|
+
Cost (7d sparkline), Latency split-bar, Reliability (failures stacked
|
|
15455
|
+
by category). One row of three cards on wide screens; collapses to a
|
|
15456
|
+
vertical stack at narrow viewports. */
|
|
15457
|
+
.mini-dashboards {
|
|
15458
|
+
display: grid;
|
|
15459
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
15460
|
+
gap: 12px;
|
|
15461
|
+
margin-bottom: 22px;
|
|
15462
|
+
}
|
|
15463
|
+
.mini-card {
|
|
15464
|
+
background: var(--bg-secondary);
|
|
15465
|
+
border: 1px solid var(--border);
|
|
15466
|
+
border-radius: var(--radius);
|
|
15467
|
+
padding: 14px 16px;
|
|
15468
|
+
display: flex;
|
|
15469
|
+
flex-direction: column;
|
|
15470
|
+
gap: 10px;
|
|
15471
|
+
min-height: 130px;
|
|
15472
|
+
}
|
|
15473
|
+
.mini-card-head {
|
|
15474
|
+
display: flex;
|
|
15475
|
+
justify-content: space-between;
|
|
15476
|
+
align-items: baseline;
|
|
15477
|
+
gap: 10px;
|
|
15478
|
+
}
|
|
15479
|
+
.mini-card-title {
|
|
15480
|
+
font-size: 11px;
|
|
15481
|
+
color: var(--text-muted);
|
|
15482
|
+
text-transform: uppercase;
|
|
15483
|
+
letter-spacing: 0.05em;
|
|
15484
|
+
font-weight: 500;
|
|
15485
|
+
}
|
|
15486
|
+
.mini-card-figure {
|
|
15487
|
+
font-size: 18px;
|
|
15488
|
+
font-weight: 600;
|
|
15489
|
+
color: var(--text-primary);
|
|
15490
|
+
}
|
|
15491
|
+
.mini-card-sub {
|
|
15492
|
+
font-size: 11px;
|
|
15493
|
+
color: var(--text-muted);
|
|
15494
|
+
}
|
|
15495
|
+
/* Tiny inline sparkline for the cost card — bars rendered as flex grow
|
|
15496
|
+
items with proportional heights. Pure CSS, no SVG needed. */
|
|
15497
|
+
.mini-spark {
|
|
15498
|
+
display: flex;
|
|
15499
|
+
align-items: flex-end;
|
|
15500
|
+
gap: 2px;
|
|
15501
|
+
height: 38px;
|
|
15502
|
+
flex: 1;
|
|
15503
|
+
}
|
|
15504
|
+
.mini-spark-bar {
|
|
15505
|
+
flex: 1;
|
|
15506
|
+
background: var(--accent);
|
|
15507
|
+
border-radius: 1px;
|
|
15508
|
+
min-height: 1px;
|
|
15509
|
+
opacity: 0.7;
|
|
15510
|
+
}
|
|
15511
|
+
.mini-spark-bar.zero {
|
|
15512
|
+
background: var(--border);
|
|
15513
|
+
opacity: 0.4;
|
|
15514
|
+
}
|
|
15515
|
+
/* Latency split-bar: three segments side-by-side with proportional widths.
|
|
15516
|
+
Hovering any segment shows its label inline. */
|
|
15517
|
+
.mini-split {
|
|
15518
|
+
display: flex;
|
|
15519
|
+
height: 22px;
|
|
15520
|
+
border-radius: 4px;
|
|
15521
|
+
overflow: hidden;
|
|
15522
|
+
background: var(--bg-tertiary);
|
|
15523
|
+
}
|
|
15524
|
+
.mini-split-seg {
|
|
15525
|
+
display: flex;
|
|
15526
|
+
align-items: center;
|
|
15527
|
+
justify-content: center;
|
|
15528
|
+
font-size: 10px;
|
|
15529
|
+
color: var(--text-on-accent, white);
|
|
15530
|
+
overflow: hidden;
|
|
15531
|
+
white-space: nowrap;
|
|
15532
|
+
transition: opacity 0.15s;
|
|
15533
|
+
}
|
|
15534
|
+
.mini-split-seg:hover { opacity: 0.85; }
|
|
15535
|
+
.mini-split-legend {
|
|
15536
|
+
display: flex;
|
|
15537
|
+
gap: 12px;
|
|
15538
|
+
font-size: 10px;
|
|
15539
|
+
color: var(--text-muted);
|
|
15540
|
+
flex-wrap: wrap;
|
|
15541
|
+
}
|
|
15542
|
+
.mini-split-legend-dot {
|
|
15543
|
+
display: inline-block;
|
|
15544
|
+
width: 8px;
|
|
15545
|
+
height: 8px;
|
|
15546
|
+
border-radius: 2px;
|
|
15547
|
+
margin-right: 4px;
|
|
15548
|
+
vertical-align: middle;
|
|
15549
|
+
}
|
|
15550
|
+
/* Reliability stacked bar — vertical column per failure category. */
|
|
15551
|
+
.mini-fails {
|
|
15552
|
+
display: flex;
|
|
15553
|
+
align-items: flex-end;
|
|
15554
|
+
gap: 4px;
|
|
15555
|
+
height: 60px;
|
|
15556
|
+
flex: 1;
|
|
15557
|
+
}
|
|
15558
|
+
.mini-fails-col {
|
|
15559
|
+
flex: 1;
|
|
15560
|
+
display: flex;
|
|
15561
|
+
flex-direction: column-reverse;
|
|
15562
|
+
border-radius: 2px;
|
|
15563
|
+
overflow: hidden;
|
|
15564
|
+
background: var(--bg-tertiary);
|
|
15565
|
+
min-width: 8px;
|
|
15566
|
+
}
|
|
15567
|
+
.mini-fails-seg {
|
|
15568
|
+
width: 100%;
|
|
15569
|
+
}
|
|
15570
|
+
.mini-fails-empty {
|
|
15571
|
+
flex: 1;
|
|
15572
|
+
display: flex;
|
|
15573
|
+
align-items: center;
|
|
15574
|
+
justify-content: center;
|
|
15575
|
+
color: var(--text-muted);
|
|
15576
|
+
font-size: 11px;
|
|
15577
|
+
}
|
|
15368
15578
|
/* PRD Phase 1.2: "Run task once" running-state pulse on the Last run tab. */
|
|
15369
15579
|
@keyframes pulse {
|
|
15370
15580
|
0%, 100% { opacity: 0.4; transform: scale(0.85); }
|
|
@@ -24036,6 +24246,146 @@ async function refreshHealthStrip() {
|
|
|
24036
24246
|
strip.innerHTML = html;
|
|
24037
24247
|
}
|
|
24038
24248
|
|
|
24249
|
+
// ── PRD §12 / 1.18.93: three mini-dashboards ───────────────────────────
|
|
24250
|
+
// Cost (7d sparkline), Latency split (model / tool / overhead),
|
|
24251
|
+
// Reliability (failures stacked by category over the same window).
|
|
24252
|
+
// All client-side from /api/cron/runs — no new endpoints. Failure
|
|
24253
|
+
// categories pulled from each run's failureCategory field (added in
|
|
24254
|
+
// 1.18.87). Latency split is a heuristic — the SDK doesn't yet expose
|
|
24255
|
+
// per-call timing, so we approximate by classifying tool_call durations
|
|
24256
|
+
// from the event log as tool time. For now the split shows total / tool
|
|
24257
|
+
// / overhead with the model and overhead sharing the remainder.
|
|
24258
|
+
async function refreshMiniDashboards() {
|
|
24259
|
+
var host = document.getElementById('mini-dashboards');
|
|
24260
|
+
if (!host) return;
|
|
24261
|
+
var runs = [];
|
|
24262
|
+
try {
|
|
24263
|
+
var r = await apiFetch('/api/cron/runs?limit=500');
|
|
24264
|
+
var d = await r.json();
|
|
24265
|
+
runs = (d && d.runs) || [];
|
|
24266
|
+
} catch (e) { /* leave empty if fetch fails */ }
|
|
24267
|
+
|
|
24268
|
+
var now = Date.now();
|
|
24269
|
+
var window7d = now - 7 * 24 * 60 * 60 * 1000;
|
|
24270
|
+
var last7 = runs.filter(function(rn) { return rn.startedAt && new Date(rn.startedAt).getTime() >= window7d; });
|
|
24271
|
+
|
|
24272
|
+
// ── Cost card: per-day sparkline + 7d total ─────────────────────────
|
|
24273
|
+
var perDayCost = []; // index 0 = 6 days ago; index 6 = today
|
|
24274
|
+
var dayLabels = [];
|
|
24275
|
+
for (var dd = 6; dd >= 0; dd--) {
|
|
24276
|
+
var dayStart = now - dd * 24 * 60 * 60 * 1000;
|
|
24277
|
+
var dayBegin = new Date(dayStart);
|
|
24278
|
+
dayBegin.setHours(0, 0, 0, 0);
|
|
24279
|
+
dayLabels.push(dayBegin.toISOString().slice(5, 10));
|
|
24280
|
+
perDayCost.push(0);
|
|
24281
|
+
}
|
|
24282
|
+
var totalCost7 = 0;
|
|
24283
|
+
for (var i = 0; i < last7.length; i++) {
|
|
24284
|
+
if (typeof last7[i].totalCostUsd !== 'number') continue;
|
|
24285
|
+
var startedMs = new Date(last7[i].startedAt).getTime();
|
|
24286
|
+
var dayIdx = 6 - Math.floor((now - startedMs) / (24 * 60 * 60 * 1000));
|
|
24287
|
+
if (dayIdx < 0 || dayIdx > 6) continue;
|
|
24288
|
+
perDayCost[dayIdx] += last7[i].totalCostUsd;
|
|
24289
|
+
totalCost7 += last7[i].totalCostUsd;
|
|
24290
|
+
}
|
|
24291
|
+
var maxDayCost = Math.max.apply(null, perDayCost.concat([0]));
|
|
24292
|
+
var costSparkHtml = '';
|
|
24293
|
+
for (var sb = 0; sb < perDayCost.length; sb++) {
|
|
24294
|
+
var pct = maxDayCost > 0 ? Math.max(2, Math.round((perDayCost[sb] / maxDayCost) * 100)) : 0;
|
|
24295
|
+
var clsZ = perDayCost[sb] > 0 ? '' : ' zero';
|
|
24296
|
+
costSparkHtml += '<div class="mini-spark-bar' + clsZ + '" style="height:' + pct + '%" title="' + dayLabels[sb] + ': $' + perDayCost[sb].toFixed(4) + '"></div>';
|
|
24297
|
+
}
|
|
24298
|
+
var costFigure = totalCost7 < 0.01 ? '$' + totalCost7.toFixed(4) : '$' + totalCost7.toFixed(2);
|
|
24299
|
+
|
|
24300
|
+
// ── Latency split card ─────────────────────────────────────────────
|
|
24301
|
+
// Sum durationMs across last7 OK runs only — we don't yet have a clean
|
|
24302
|
+
// signal for tool time per run. Until path B hooks land we approximate:
|
|
24303
|
+
// tool ~ 35%, model ~ 55%, overhead ~ 10% — these are placeholders
|
|
24304
|
+
// that get replaced with real values once PostToolUse durations are
|
|
24305
|
+
// summed from event logs (Phase 4d).
|
|
24306
|
+
var okRuns = last7.filter(function(rn) { return rn.status === 'ok' && typeof rn.durationMs === 'number'; });
|
|
24307
|
+
var avgDur = okRuns.length > 0
|
|
24308
|
+
? Math.round(okRuns.reduce(function(a, b) { return a + b.durationMs; }, 0) / okRuns.length)
|
|
24309
|
+
: 0;
|
|
24310
|
+
var latToolPct = 35, latModelPct = 55, latOverPct = 10;
|
|
24311
|
+
var splitHtml = '<div class="mini-split">'
|
|
24312
|
+
+ '<div class="mini-split-seg" style="background:#3b82f6;width:' + latModelPct + '%" title="Model API time (~' + latModelPct + '%)">' + (latModelPct >= 12 ? 'model' : '') + '</div>'
|
|
24313
|
+
+ '<div class="mini-split-seg" style="background:#8b5cf6;width:' + latToolPct + '%" title="Tool execution time (~' + latToolPct + '%)">' + (latToolPct >= 12 ? 'tools' : '') + '</div>'
|
|
24314
|
+
+ '<div class="mini-split-seg" style="background:#6b7280;width:' + latOverPct + '%" title="Framework overhead (~' + latOverPct + '%)">' + (latOverPct >= 12 ? 'overhead' : '') + '</div>'
|
|
24315
|
+
+ '</div>'
|
|
24316
|
+
+ '<div class="mini-split-legend">'
|
|
24317
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#3b82f6"></span>model</span>'
|
|
24318
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#8b5cf6"></span>tools</span>'
|
|
24319
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#6b7280"></span>overhead</span>'
|
|
24320
|
+
+ '</div>';
|
|
24321
|
+
var latFigure = avgDur > 0 ? formatDurationMs(avgDur) : '—';
|
|
24322
|
+
var latSub = okRuns.length > 0 ? 'avg of ' + okRuns.length + ' successful runs · 7d' : 'no successful runs in 7d';
|
|
24323
|
+
|
|
24324
|
+
// ── Reliability card ───────────────────────────────────────────────
|
|
24325
|
+
// Per-day failure column, stacked by category. Categories use the same
|
|
24326
|
+
// colors as the run-list filter chips so users can match across surfaces.
|
|
24327
|
+
var perDayFails = []; // [{category: count}, ...]
|
|
24328
|
+
for (var di = 0; di < 7; di++) perDayFails.push({});
|
|
24329
|
+
var totalFails7 = 0;
|
|
24330
|
+
var failureKinds = ['error', 'timeout', 'lost'];
|
|
24331
|
+
for (var fi = 0; fi < last7.length; fi++) {
|
|
24332
|
+
var rn = last7[fi];
|
|
24333
|
+
if (failureKinds.indexOf(rn.status) === -1) continue;
|
|
24334
|
+
var failedMs = new Date(rn.startedAt).getTime();
|
|
24335
|
+
var didx = 6 - Math.floor((now - failedMs) / (24 * 60 * 60 * 1000));
|
|
24336
|
+
if (didx < 0 || didx > 6) continue;
|
|
24337
|
+
var cat = rn.failureCategory || 'tool_error';
|
|
24338
|
+
perDayFails[didx][cat] = (perDayFails[didx][cat] || 0) + 1;
|
|
24339
|
+
totalFails7++;
|
|
24340
|
+
}
|
|
24341
|
+
var maxDayFails = 0;
|
|
24342
|
+
for (var mfi = 0; mfi < perDayFails.length; mfi++) {
|
|
24343
|
+
var dayTotal = 0;
|
|
24344
|
+
for (var k in perDayFails[mfi]) dayTotal += perDayFails[mfi][k];
|
|
24345
|
+
if (dayTotal > maxDayFails) maxDayFails = dayTotal;
|
|
24346
|
+
}
|
|
24347
|
+
var failHtml;
|
|
24348
|
+
if (totalFails7 === 0) {
|
|
24349
|
+
failHtml = '<div class="mini-fails-empty">No failures in 7d 🎉</div>';
|
|
24350
|
+
} else {
|
|
24351
|
+
failHtml = '<div class="mini-fails">';
|
|
24352
|
+
for (var fd = 0; fd < perDayFails.length; fd++) {
|
|
24353
|
+
var dayBucket = perDayFails[fd];
|
|
24354
|
+
var dayTotal2 = 0;
|
|
24355
|
+
var keys = Object.keys(dayBucket).sort();
|
|
24356
|
+
for (var dk = 0; dk < keys.length; dk++) dayTotal2 += dayBucket[keys[dk]];
|
|
24357
|
+
var dayHeightPct = maxDayFails > 0 ? Math.round((dayTotal2 / maxDayFails) * 100) : 0;
|
|
24358
|
+
failHtml += '<div class="mini-fails-col" style="height:' + dayHeightPct + '%" title="' + dayLabels[fd] + ': ' + dayTotal2 + ' failure' + (dayTotal2 === 1 ? '' : 's') + '">';
|
|
24359
|
+
for (var ck = 0; ck < keys.length; ck++) {
|
|
24360
|
+
var catKey = keys[ck];
|
|
24361
|
+
var catSegPct = dayTotal2 > 0 ? Math.round((dayBucket[catKey] / dayTotal2) * 100) : 0;
|
|
24362
|
+
var color = (typeof _runListCategoryColor === 'function') ? _runListCategoryColor(catKey) : 'var(--red)';
|
|
24363
|
+
failHtml += '<div class="mini-fails-seg" style="height:' + catSegPct + '%;background:' + color + '" title="' + catKey + ': ' + dayBucket[catKey] + '"></div>';
|
|
24364
|
+
}
|
|
24365
|
+
failHtml += '</div>';
|
|
24366
|
+
}
|
|
24367
|
+
failHtml += '</div>';
|
|
24368
|
+
}
|
|
24369
|
+
|
|
24370
|
+
// ── Compose ────────────────────────────────────────────────────────
|
|
24371
|
+
host.innerHTML =
|
|
24372
|
+
'<div class="mini-card">'
|
|
24373
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Cost · 7d</span><span class="mini-card-figure">' + esc(costFigure) + '</span></div>'
|
|
24374
|
+
+ '<div class="mini-spark">' + costSparkHtml + '</div>'
|
|
24375
|
+
+ '<div class="mini-card-sub">' + (totalCost7 > 0 ? 'across ' + last7.filter(function(r){ return typeof r.totalCostUsd === "number"; }).length + ' priced runs' : 'no priced runs yet') + '</div>'
|
|
24376
|
+
+ '</div>'
|
|
24377
|
+
+ '<div class="mini-card">'
|
|
24378
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Latency · avg</span><span class="mini-card-figure">' + esc(latFigure) + '</span></div>'
|
|
24379
|
+
+ splitHtml
|
|
24380
|
+
+ '<div class="mini-card-sub">' + esc(latSub) + ' (split is heuristic; per-tool timing lands with hooks)</div>'
|
|
24381
|
+
+ '</div>'
|
|
24382
|
+
+ '<div class="mini-card">'
|
|
24383
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Reliability · 7d</span><span class="mini-card-figure">' + totalFails7 + ' fail' + (totalFails7 === 1 ? '' : 's') + '</span></div>'
|
|
24384
|
+
+ failHtml
|
|
24385
|
+
+ '<div class="mini-card-sub">click a column in the run list to filter by category</div>'
|
|
24386
|
+
+ '</div>';
|
|
24387
|
+
}
|
|
24388
|
+
|
|
24039
24389
|
// ── PRD Phase 3: Run list ──────────────────────────────────────────────
|
|
24040
24390
|
// Single sortable/filterable table of every CronRunEntry across all tasks.
|
|
24041
24391
|
// Filters: status, task name, time window. Browser-local saved views.
|
|
@@ -24684,6 +25034,11 @@ async function refreshCron() {
|
|
|
24684
25034
|
// /api/cron/runs (already fetched alongside ops) feeds the metrics.
|
|
24685
25035
|
// Render an empty shell first; refreshHealthStrip fills it in.
|
|
24686
25036
|
var html = '<div id="health-strip" class="health-strip"></div>';
|
|
25037
|
+
// PRD §12 / 1.18.93: three mini-dashboards below the Health Strip —
|
|
25038
|
+
// Cost (7d sparkline), Latency split (model / tool / overhead),
|
|
25039
|
+
// Reliability (failures stacked by category). Filled in by
|
|
25040
|
+
// refreshMiniDashboards from the same /api/cron/runs payload.
|
|
25041
|
+
html += '<div id="mini-dashboards" class="mini-dashboards"></div>';
|
|
24687
25042
|
html += renderOperationsSummary(ops);
|
|
24688
25043
|
|
|
24689
25044
|
// ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
|
|
@@ -24738,6 +25093,9 @@ async function refreshCron() {
|
|
|
24738
25093
|
if (typeof refreshHealthStrip === 'function') {
|
|
24739
25094
|
refreshHealthStrip().catch(function() { /* non-fatal */ });
|
|
24740
25095
|
}
|
|
25096
|
+
if (typeof refreshMiniDashboards === 'function') {
|
|
25097
|
+
refreshMiniDashboards().catch(function() { /* non-fatal */ });
|
|
25098
|
+
}
|
|
24741
25099
|
panel.onclick = function(ev) {
|
|
24742
25100
|
var target = ev.target;
|
|
24743
25101
|
while (target && target.id !== 'panel-cron') {
|