feat: v0.9.2 Ask Claude on an alert starts an investigation session
- Ops Alerts → Ask Claude now calls `devdesk attach --investigate SESSION UID`. The server looks the alert up, writes the prompt and starts Claude in a new alert-<name> session with it as the first prompt, already sent. Prompts are stored in prompts/<session>.txt and consumed once by devdesk-claude-run. A session that is already running is just opened. - The prompt has the rule, query, summary, details and labels, and where it runs: the CT/VM id, the node it is on now, status and HA state, and `ssh <node> pct exec <id> --`. It reminds Claude that HA guests move (`devdesk guests <id>`), and gives aggregate rules a hint. - `devdesk guests`: Proxmox inventory over SSH (DEVDESK_PVE_NODES). pvesh gives the cluster view; each node gives lxc-info and guest-agent IPs. Cached in guests.json, keeping last-seen IPs for stopped guests; --alerts refreshes it in the background every 5 minutes. - Alerts carry uid, query, description and targets, matched by instance IP/host or by name; each card shows the target. - cmd_alerts moves to libexec/devdesk-ops.sh. Closes #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+27
-12
@@ -1,8 +1,10 @@
|
||||
// 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).
|
||||
// the Proxmox guest or node each is about (CT id, the node it runs on now), and
|
||||
// an "Ask Claude" button that starts an investigation in a session of its own
|
||||
// (devdesk#39).
|
||||
|
||||
import { el } from './dom.js';
|
||||
import { slugify } from './protocol.js';
|
||||
|
||||
const SEVERITY = {
|
||||
critical: 'lt-chip-critical',
|
||||
@@ -18,14 +20,24 @@ function age(iso) {
|
||||
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.';
|
||||
// "CT 110 draupnir · monitor-02 · stopped", "node compute-storage-01".
|
||||
function targetLabel(t) {
|
||||
if (t.kind === 'unknown') return null;
|
||||
const what = t.kind === 'node' ? `node ${t.name}` : `${t.kind.toUpperCase()} ${t.id} ${t.name}`;
|
||||
const parts = [what, t.kind === 'node' ? null : t.node, t.status === 'running' || t.status === 'online' ? null : t.status];
|
||||
return parts.filter(Boolean).join(' · ') + (t.by === 'name' ? ' (by name)' : '');
|
||||
}
|
||||
|
||||
function targetTitle(t) {
|
||||
const how = t.kind === 'ct' ? `ssh ${t.node} pct exec ${t.id} -- …`
|
||||
: t.kind === 'vm' ? `ssh ${t.node} qm guest exec ${t.id} -- …` : `ssh ${t.name}`;
|
||||
const note = t.by === 'name' ? '\nMatched by name, not by IP (the guest has not been seen running with this IP).'
|
||||
: t.ip_seen ? '\nIts IP when it was last seen running.' : '';
|
||||
return `${how}${t.ha ? `\nHA state: ${t.ha}` : ''}${note}`;
|
||||
}
|
||||
|
||||
export const alertSession = (alert) => slugify(`alert-${alert.name}`).toLowerCase().slice(0, 48) || 'alert';
|
||||
|
||||
export function renderAlerts(root, data) {
|
||||
const list = root.querySelector('.dd-alert-list');
|
||||
const note = root.querySelector('.dd-alerts-note');
|
||||
@@ -59,11 +71,14 @@ export function renderAlerts(root, data) {
|
||||
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.targets ?? []).map((t) => {
|
||||
const label = targetLabel(t);
|
||||
return label ? el('div', { class: `dd-alert-where${t.by === 'name' ? ' is-guess' : ''}`, title: targetTitle(t) }, label) : null;
|
||||
}),
|
||||
a.summary ? el('div', { class: 'dd-alert-summary' }, a.summary) : null,
|
||||
el('div', { class: 'dd-ask-row' },
|
||||
el('button', { type: 'button', class: 'lt-btn lt-btn-sm dd-ask', 'data-ask-claude': investigatePrompt(a), title: 'Type the prompt into the current session (not sent)' }, 'Ask Claude'),
|
||||
el('button', {
|
||||
type: 'button', class: 'lt-btn lt-btn-ghost lt-btn-sm dd-ask', 'data-ask-claude': investigatePrompt(a),
|
||||
'data-ask-session': `alert-${a.name}`, title: 'Open a separate ops session for this alert and type the prompt there (not sent)',
|
||||
}, 'In a new session')))));
|
||||
type: 'button', class: 'lt-btn lt-btn-sm dd-ask', 'data-investigate': a.uid, 'data-session': alertSession(a),
|
||||
title: `Start Claude in a new session (${alertSession(a)}) on this alert, with the rule, the query and where it runs (CT id, node)`,
|
||||
}, 'Ask Claude')))));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user