2026-10-07 19:26:06 -04:00
|
|
|
// Ops Alerts panel: firing/pending Grafana alert rules (`devdesk attach --alerts`),
|
|
|
|
|
// each with an "Ask Claude" button that types an investigation prompt into the
|
|
|
|
|
// current session (without pressing Enter).
|
|
|
|
|
|
2026-10-08 00:32:23 -04:00
|
|
|
import { el } from './dom.js';
|
2026-10-07 19:26:06 -04:00
|
|
|
|
|
|
|
|
const SEVERITY = {
|
|
|
|
|
critical: 'lt-chip-critical',
|
|
|
|
|
warning: 'lt-chip-warn',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function age(iso) {
|
|
|
|
|
const t = Date.parse(iso || '');
|
|
|
|
|
if (Number.isNaN(t)) return '';
|
|
|
|
|
const s = Math.max(0, (Date.now() - t) / 1000);
|
|
|
|
|
if (s < 3600) return `${Math.max(1, Math.floor(s / 60))}m`;
|
|
|
|
|
if (s < 86400) return `${Math.floor(s / 3600)}h`;
|
|
|
|
|
return `${Math.floor(s / 86400)}d`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function investigatePrompt(alert) {
|
|
|
|
|
const where = alert.instances?.length ? ` on ${alert.instances.join('; ')}` : '';
|
|
|
|
|
return `Investigate the ${alert.state} Grafana alert "${alert.name}"`
|
|
|
|
|
+ ` (${alert.severity || 'no severity'}, ${alert.folder} / ${alert.group}${where}, since ${alert.since}).`
|
|
|
|
|
+ ` Summary: ${alert.summary || 'n/a'}. Use the grafana MCP server to look at the rule and the related`
|
|
|
|
|
+ ' metrics, find the cause, and tell me what you would change before changing anything.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renderAlerts(root, data) {
|
|
|
|
|
const list = root.querySelector('.dd-alert-list');
|
|
|
|
|
const note = root.querySelector('.dd-alerts-note');
|
|
|
|
|
const count = root.querySelector('.dd-alert-count');
|
|
|
|
|
const alerts = data?.alerts ?? [];
|
|
|
|
|
count.textContent = alerts.length ? String(alerts.length) : '';
|
|
|
|
|
count.className = `lt-badge lt-badge-sm dd-alert-count${alerts.some((a) => a.severity === 'critical') ? ' lt-badge-p1' : alerts.length ? ' lt-badge-p2' : ''}`;
|
|
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
|
|
root.dataset.state = 'empty';
|
|
|
|
|
list.replaceChildren(el('li', { class: 'dd-empty' }, 'Loading…'));
|
|
|
|
|
note.textContent = '';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!data.configured) {
|
|
|
|
|
root.dataset.state = 'empty';
|
|
|
|
|
list.replaceChildren(el('li', { class: 'dd-empty' }, 'Grafana is not configured.'));
|
|
|
|
|
note.textContent = 'Set DEVDESK_GRAFANA_URL and a token file in /etc/devdesk/env.local.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
root.dataset.state = 'live';
|
|
|
|
|
note.textContent = data.error ? `Grafana unreachable: ${data.error}` : '';
|
|
|
|
|
if (!alerts.length) {
|
|
|
|
|
list.replaceChildren(el('li', { class: 'dd-all-clear' },
|
|
|
|
|
el('span', { class: 'lt-dot lt-dot-up' }), data.error ? 'No data' : 'All clear: nothing firing'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
list.replaceChildren(...alerts.map((a) => el('li', { class: `dd-alert is-${a.state}` },
|
|
|
|
|
el('div', { class: 'dd-alert-head' },
|
|
|
|
|
el('span', { class: `lt-chip ${SEVERITY[a.severity] || 'lt-chip-info'}` }, a.state === 'pending' ? 'pending' : (a.severity || 'alert')),
|
|
|
|
|
el('a', { class: 'dd-alert-name', href: a.url, target: '_blank', rel: 'noopener', title: `${a.folder} / ${a.group}` }, a.name),
|
|
|
|
|
el('span', { class: 'dd-muted dd-alert-age' }, age(a.since))),
|
|
|
|
|
a.instances?.length ? el('div', { class: 'dd-muted dd-alert-instances' }, a.instances.join(' · ')) : null,
|
|
|
|
|
a.summary ? el('div', { class: 'dd-alert-summary' }, a.summary) : null,
|
|
|
|
|
el('button', { type: 'button', class: 'lt-btn lt-btn-sm dd-ask', 'data-ask-claude': investigatePrompt(a) }, 'Ask Claude'))));
|
|
|
|
|
}
|