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 ,
2026-10-10 20:37:46 -04:00
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' )))));
2026-10-07 19:26:06 -04:00
}