package server import ( "context" "github.com/jmoiron/sqlx" "vdi-cloud/platform/internal/store" ) // Column names are exclusively from this allowlist, never request text. func terminalAlertColumn(metric string) string { return map[string]string{"terminal_cpu": "cpu_percent", "terminal_memory": "memory_percent", "terminal_temperature": "soc_temperature_c"}[metric] } func terminalAlertQuery(column string) string { return `SELECT t.id,t.name,false unavailable,m.sampled_at, CASE WHEN m.supported AND t.last_seen>now()-interval '90 seconds' AND m.sampled_at>now()-interval '90 seconds' AND m.sampled_at<=now() THEN m.` + column + ` ELSE NULL END value FROM terminals t LEFT JOIN terminal_metrics_current m ON m.terminal_id=t.id WHERE t.enabled AND t.token_hash IS NOT NULL AND t.status!='unregistered'` } // Non-terminal metrics keep a null freshness flag for backward compatibility. // A value becomes unknown on missing/old samples, unsupported agents or revocation. const terminalAlertFreshSQL = `CASE WHEN metric IN ('terminal_cpu','terminal_memory','terminal_temperature') THEN EXISTS( SELECT 1 FROM terminals t JOIN terminal_metrics_current m ON m.terminal_id=t.id WHERE t.id=alerts.target_id AND t.enabled AND t.token_hash IS NOT NULL AND t.status!='unregistered' AND t.last_seen>now()-interval '90 seconds' AND m.supported AND m.sampled_at>now()-interval '90 seconds' AND m.sampled_at<=now() AND CASE alerts.metric WHEN 'terminal_cpu' THEN m.cpu_percent WHEN 'terminal_memory' THEN m.memory_percent WHEN 'terminal_temperature' THEN m.soc_temperature_c END IS NOT NULL) ELSE NULL END` func evaluateTerminalHigh(ctx context.Context, tx *sqlx.Tx, rule alertRule, target observation, column string) error { // Count source time, not repeated evaluator ticks. A source gap over 90s or // an intervening null/normal history sample starts a new continuous segment. // The history range is at most 90 seconds in the non-gap case and uses its PK. _, err := tx.ExecContext(ctx, `INSERT INTO alert_states(rule_id,target_id,first_seen,last_sample_at) VALUES($1,$2,GREATEST($3::timestamptz,$5::timestamptz),$3) ON CONFLICT(rule_id,target_id) DO UPDATE SET first_seen=CASE WHEN alert_states.last_sample_at IS NULL OR EXCLUDED.last_sample_at-alert_states.last_sample_at>interval '90 seconds' OR EXISTS(SELECT 1 FROM terminal_metric_samples s WHERE s.terminal_id=$2 AND s.sampled_at>GREATEST(alert_states.last_sample_at,EXCLUDED.last_sample_at-interval '90 seconds') AND s.sampled_at<=EXCLUDED.last_sample_at AND (s.`+column+` IS NULL OR s.`+column+`<$4)) THEN EXCLUDED.first_seen ELSE alert_states.first_seen END, last_sample_at=EXCLUDED.last_sample_at WHERE alert_states.last_sample_at IS NULL OR EXCLUDED.last_sample_at>alert_states.last_sample_at`, rule.ID, target.ID, target.SampledAt, rule.Threshold, rule.UpdatedAt) if err != nil { return err } _, err = tx.ExecContext(ctx, `INSERT INTO alerts(id,rule_id,rule_name,metric,target_id,target_name,value,threshold,started_at) SELECT $1,$2,$3,$4,$5,$6,$7,$8,first_seen FROM alert_states WHERE rule_id=$2 AND target_id=$5 AND ($9=0 OR last_sample_at-first_seen>=make_interval(secs=>$9) OR EXISTS(SELECT 1 FROM alerts WHERE rule_id=$2 AND target_id=$5 AND status='firing')) ON CONFLICT(rule_id,target_id) WHERE status='firing' DO UPDATE SET value=EXCLUDED.value`, store.ID("alert_"), rule.ID, rule.Name, rule.Metric, target.ID, target.Name, *target.Value, rule.Threshold, rule.HoldSeconds) return err }