feat: v0.9.2 Ask Claude on an alert starts an investigation session
build-deb / Lint, test, package (push) Successful in 2m23s
build-deb / Deploy to CT 126 (push) Successful in 30s
build-deb / Notify on failure (push) Skipped

- 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:
2026-10-11 11:24:49 -04:00
co-authored by Claude Opus 5.5
parent afdbf6eaec
commit c0f475bffe
14 changed files with 556 additions and 98 deletions
+13 -38
View File
@@ -7,11 +7,12 @@
# devdesk end NAME stop session NAME and forget it
# devdesk repo NAME git + Gitea CI status of the session's folder (JSON)
# devdesk alerts firing/pending Grafana alerts (JSON)
# devdesk guests [ID|IP|NAME] Proxmox CTs/VMs: the node each runs on now, state, IPs
# devdesk ls list sessions
# devdesk pick interactive picker
#
# The web UI calls `devdesk attach --list | --repo NAME | --alerts | --upload SESSION FILE BYTES |
# --new NAME [PROJECT] | --rename OLD NEW | --end NAME | --existing NAME`
# The web UI calls `devdesk attach --list | --repo NAME | --alerts | --investigate SESSION UID |
# --upload SESSION FILE BYTES | --new NAME [PROJECT] | --rename OLD NEW | --end NAME | --existing NAME`
# through ttyd; those print JSON.
set -euo pipefail
@@ -23,9 +24,11 @@ set -euo pipefail
. "$DEVDESK_LIBEXEC/devdesk-inbox.sh"
# shellcheck source=libexec/devdesk-history.sh
. "$DEVDESK_LIBEXEC/devdesk-history.sh"
# shellcheck source=libexec/devdesk-ops.sh
. "$DEVDESK_LIBEXEC/devdesk-ops.sh"
usage() {
sed -n '2,12s/^# \{0,1\}//p' "$0"
sed -n '2,13s/^# \{0,1\}//p' "$0"
}
# Tab-separated running sessions: name, dir, attached clients, last activity, windows.
@@ -230,41 +233,6 @@ cmd_repo() {
prs: $prs}'
}
# Firing and pending Grafana alert rules, most severe first.
cmd_alerts() {
if [ -z "$DEVDESK_GRAFANA_URL" ]; then
jq -cMn '{configured: false, alerts: []}'
return
fi
local raw
if ! raw=$({
if [ -r "$DEVDESK_GRAFANA_TOKEN_FILE" ]; then
# shellcheck disable=SC1003 # tr deletes whitespace, quotes and backslashes from the token
printf 'header = "Authorization: Bearer %s"\n' "$(tr -d '[:space:]"\\' <"$DEVDESK_GRAFANA_TOKEN_FILE")"
fi
} | curl -fsS --max-time 6 -K - "$DEVDESK_GRAFANA_URL/api/prometheus/grafana/api/v1/rules" 2>&1); then
jq -cMn --arg e "${raw:-no response}" '{configured: true, error: $e, alerts: []}'
return
fi
jq -cM --arg pub "$DEVDESK_GRAFANA_PUBLIC_URL" '
def num: (tonumber? // .) | tostring;
{configured: true, checked: (now | floor), alerts: [
.data.groups[] | .name as $group | .file as $folder | .rules[]
| select(.state == "firing" or .state == "pending")
| . as $r
| ([.alerts[]? | select(.state == "Alerting" or .state == "Pending")]) as $active
| {name, state, severity: (.labels.severity // null), group: $group, folder: $folder,
since: (.activeAt // $active[0].activeAt // null),
summary: ((.annotations.summary // .annotations.description // "")
| gsub("\\{\\{ *\\$value *\\}\\}"; ($active[0].value // "?" | num))),
instances: [$active[] | .labels
| del(.__alert_rule_uid__, .__grafana_origin__, .alertname, .grafana_folder, .__name__, .severity)
| to_entries | map("\(.key)=\(.value)") | join(" ")
| select(. != "")],
url: "\($pub)/alerting/grafana/\(.uid)/view"}
] | sort_by(if .severity == "critical" then 0 elif .severity == "warning" then 1 else 2 end)}' <<<"$raw"
}
# Receive a file for SESSION over the terminal: the browser waits for the ready
# line, then sends exactly ceil(BYTES/3)*4 bytes of base64 as input.
cmd_upload() {
@@ -323,6 +291,12 @@ cmd_attach() {
dd_linger
return
;;
--investigate)
shift
cmd_investigate "$@"
dd_linger
return
;;
--upload)
shift
cmd_upload "$@"
@@ -408,6 +382,7 @@ new) cmd_new "$@" ;;
rename | mv) cmd_rename "$@" ;;
repo) cmd_repo "$@" ;;
alerts) cmd_alerts ;;
guests) cmd_guests "$@" ;;
ls | list) cmd_ls ;;
--list) cmd_list ;;
end) cmd_end "$@" ;;