Diff -- /var/www/vhosts/3dshawn.com/site1/alerts.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/site1/alerts.cgi

added on local at 2026-07-11 18:27:43

Added
+222
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to ec54b82a47de
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# DriftSense -- /alerts.cgi
4#
5# List, add, and edit alert rules. Also shows recent delivery history
6# per rule (last 30 events). POST handling:
7# op=create -> insert
8# op=update -> update by alert_rule_id
9# op=delete -> soft delete (is_active=0)
10# op=toggle -> flip is_active
11#======================================================================
12use strict;
13use warnings;
14use CGI ();
15use MODS::Config;
16use MODS::DBConnect;
17use MODS::Template;
18use MODS::PageWrapper;
19
20my $cgi = CGI->new;
21my $db = MODS::DBConnect->new;
22my $tpl = MODS::Template->new;
23
24$|=1;
25
26my $dbh = $db->db_connect or die "DriftSense: cannot connect to storage DB\n";
27
28# ---- POST actions ---------------------------------------------------
29my $method = $ENV{REQUEST_METHOD} || 'GET';
30if ($method eq 'POST') {
31 my $op = $cgi->param('op') || '';
32
33 if ($op eq 'create' || $op eq 'update') {
34 my $rid = $op eq 'update' ? int($cgi->param('alert_rule_id') || 0) : 0;
35 my $name = _clean($cgi->param('rule_name'), 200);
36 my $kind = _one_of($cgi->param('match_kind'), 'either', 'file', 'schema', 'either');
37 my $glob = _clean($cgi->param('match_path_glob'), 500);
38 my $stat = _clean($cgi->param('match_status_list'), 200);
39 my $xts = $cgi->param('exclude_ts_only') ? 1 : 0;
40 my $dkind = _one_of($cgi->param('delivery_kind'),
41 'generic_webhook', qw(slack discord generic_webhook email));
42 my $durl = _clean($cgi->param('delivery_url'), 2000);
43 my $dmail = _clean($cgi->param('delivery_email'), 500);
44 my $rlim = int($cgi->param('rate_limit_per_min') || 0);
45 my $csec = int($cgi->param('coalesce_window_s') || 0);
46 my $act = $cgi->param('is_active') ? 1 : 0;
47
48 my $q = sub { $dbh->quote($_[0] // '') };
49
50 if ($op eq 'create') {
51 $db->db_readwrite($dbh, qq~
52 INSERT INTO ALERT_RULES
53 (rule_name, is_active, match_kind, match_path_glob,
54 match_status_list, exclude_ts_only,
55 delivery_kind, delivery_url, delivery_email,
56 rate_limit_per_min, coalesce_window_s)
57 VALUES
58 (@{[ $q->($name) ]}, $act, @{[ $q->($kind) ]},
59 @{[ $q->($glob) ]}, @{[ $q->($stat) ]}, $xts,
60 @{[ $q->($dkind) ]}, @{[ $q->($durl) ]}, @{[ $q->($dmail) ]},
61 $rlim, $csec)
62 ~, __FILE__, __LINE__);
63 } else {
64 $db->db_readwrite($dbh, qq~
65 UPDATE ALERT_RULES
66 SET rule_name = @{[ $q->($name) ]},
67 is_active = $act,
68 match_kind = @{[ $q->($kind) ]},
69 match_path_glob = @{[ $q->($glob) ]},
70 match_status_list = @{[ $q->($stat) ]},
71 exclude_ts_only = $xts,
72 delivery_kind = @{[ $q->($dkind) ]},
73 delivery_url = @{[ $q->($durl) ]},
74 delivery_email = @{[ $q->($dmail) ]},
75 rate_limit_per_min = $rlim,
76 coalesce_window_s = $csec
77 WHERE alert_rule_id = $rid
78 ~, __FILE__, __LINE__);
79 }
80 }
81 elsif ($op eq 'delete') {
82 my $rid = int($cgi->param('alert_rule_id') || 0);
83 $db->db_readwrite($dbh,
84 "UPDATE ALERT_RULES SET is_active = 0 WHERE alert_rule_id = $rid",
85 __FILE__, __LINE__);
86 }
87 elsif ($op eq 'toggle') {
88 my $rid = int($cgi->param('alert_rule_id') || 0);
89 $db->db_readwrite($dbh,
90 "UPDATE ALERT_RULES SET is_active = 1 - is_active WHERE alert_rule_id = $rid",
91 __FILE__, __LINE__);
92 }
93
94 $db->db_disconnect($dbh);
95 print "Status: 302 Found\nLocation: /alerts.cgi?nc=" . int(rand() * 10_000_000) . "\n\n";
96 exit;
97}
98
99# ---- GET: list rules + delivery history -----------------------------
100print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
101
102my @rules = $db->db_readwrite_multiple($dbh, q~
103 SELECT alert_rule_id, rule_name, is_active,
104 match_kind, match_path_glob, match_status_list, exclude_ts_only,
105 delivery_kind, delivery_url, delivery_email,
106 rate_limit_per_min, coalesce_window_s,
107 fire_count, last_fired_at,
108 UNIX_TIMESTAMP(last_fired_at) AS last_fired_epoch,
109 UNIX_TIMESTAMP(created_at) AS created_epoch
110 FROM ALERT_RULES
111 ORDER BY is_active DESC, alert_rule_id ASC
112~, __FILE__, __LINE__);
113
114foreach my $r (@rules) {
115 $r->{status_pill} = $r->{is_active} ? 'pill-ok' : 'pill-mute';
116 $r->{status_label} = $r->{is_active} ? 'active' : 'paused';
117 $r->{kind_pill} = $r->{delivery_kind} eq 'slack' ? 'pill-info'
118 : $r->{delivery_kind} eq 'discord' ? 'pill-warn'
119 : 'pill-mute';
120 $r->{match_glob_h} = $r->{match_path_glob} || '(all paths)';
121 $r->{url_h} = _shorten($r->{delivery_url} || $r->{delivery_email} || '(no target)');
122 $r->{last_fired_epoch} //= 0;
123 $r->{last_fired_h} = $r->{last_fired_at} || 'never';
124 $r->{fire_count} //= 0;
125 $r->{edit_url} = "/alerts.cgi?edit=$r->{alert_rule_id}";
126}
127
128my @deliveries = $db->db_readwrite_multiple($dbh, q~
129 SELECT d.delivery_id, d.alert_rule_id, r.rule_name,
130 d.source_kind, d.source_id, d.match_summary,
131 d.delivery_status, d.http_status,
132 UNIX_TIMESTAMP(d.fired_at) AS fired_epoch,
133 d.fired_at,
134 LEFT(d.error_message, 200) AS err_short
135 FROM ALERT_DELIVERIES d
136 LEFT JOIN ALERT_RULES r ON r.alert_rule_id = d.alert_rule_id
137 ORDER BY d.delivery_id DESC
138 LIMIT 30
139~, __FILE__, __LINE__);
140foreach my $d (@deliveries) {
141 $d->{delivery_pill} = $d->{delivery_status} eq 'sent' ? 'pill-ok'
142 : $d->{delivery_status} eq 'failed' ? 'pill-bad'
143 : $d->{delivery_status} eq 'skipped' ? 'pill-mute'
144 : 'pill-warn';
145 $d->{source_url} = "/diff.cgi?kind=$d->{source_kind}&id=$d->{source_id}";
146 $d->{summary_h} = _shorten($d->{match_summary} || '', 90);
147 $d->{err_h} = $d->{err_short} ? "err: $d->{err_short}" : ($d->{http_status} ? "HTTP $d->{http_status}" : '-');
148 $d->{fired_epoch} //= 0;
149}
150
151# ---- Edit mode: prefill form ---------------------------------------
152my $edit_id = int($cgi->param('edit') || 0);
153my $edit = {};
154if ($edit_id) {
155 ($edit) = grep { $_->{alert_rule_id} == $edit_id } @rules;
156 $edit ||= {};
157}
158my $is_edit = $edit_id && $edit && $edit->{alert_rule_id} ? 1 : 0;
159
160# defaults for a fresh form
161my %form = (
162 rule_name => $edit->{rule_name} // '',
163 is_active_checked => (!$is_edit || $edit->{is_active}) ? 'checked' : '',
164 match_kind => $edit->{match_kind} // 'either',
165 match_path_glob => $edit->{match_path_glob} // '',
166 match_status_list => $edit->{match_status_list} // '',
167 exclude_ts_only_checked => (!$is_edit || $edit->{exclude_ts_only}) ? 'checked' : '',
168 delivery_kind => $edit->{delivery_kind} // 'generic_webhook',
169 delivery_url => $edit->{delivery_url} // '',
170 delivery_email => $edit->{delivery_email} // '',
171 rate_limit_per_min => $edit->{rate_limit_per_min} // 0,
172 coalesce_window_s => $edit->{coalesce_window_s} // 0,
173);
174# select-option "selected" markers
175foreach my $k (qw(either file schema)) {
176 $form{"mk_${k}_sel"} = $form{match_kind} eq $k ? 'selected' : '';
177}
178foreach my $k (qw(generic_webhook slack discord email)) {
179 $form{"dk_${k}_sel"} = $form{delivery_kind} eq $k ? 'selected' : '';
180}
181
182$db->db_disconnect($dbh);
183
184# ---- Render body ----------------------------------------------------
185my $tvars = {
186 rules => \@rules,
187 has_rules => scalar(@rules) ? 1 : 0,
188 deliveries => \@deliveries,
189 has_deliveries => scalar(@deliveries) ? 1 : 0,
190 is_edit => $is_edit,
191 edit_id => $is_edit ? $edit->{alert_rule_id} : 0,
192 form_op => $is_edit ? 'update' : 'create',
193 %form,
194};
195
196my @body = $tpl->template('alerts.html', $tvars);
197MODS::PageWrapper->new->wrapper(
198 page_title => 'Alerts', page_key => 'alerts',
199 body_html => join('', @body),
200 userinfo => { display_name => 'Operator' },
201);
202
203#---------------------------------------------------------------------
204sub _clean {
205 my ($s, $max) = @_;
206 $s //= '';
207 $s =~ s/[\r\n]/ /g;
208 $s =~ s/^\s+|\s+$//g;
209 $s = substr($s, 0, $max) if defined $max && length($s) > $max;
210 return $s;
211}
212sub _one_of {
213 my ($v, $default, @allowed) = @_;
214 $v //= '';
215 return (grep { $_ eq $v } @allowed) ? $v : $default;
216}
217sub _shorten {
218 my ($s, $max) = @_;
219 $max //= 60;
220 $s //= '';
221 return length($s) > $max ? substr($s, 0, $max - 3) . '...' : $s;
222}