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

O Operator
Diff

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

modified on local at 2026-07-12 00:02:06

Added
+12
lines
Removed
-3
lines
Context
219
unchanged
Blobs
from ec54b82a47de
to 3738a3d43d87
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11#!/usr/bin/perl
22#======================================================================
33# DriftSense -- /alerts.cgi
44#
55# List, add, and edit alert rules. Also shows recent delivery history
66# per rule (last 30 events). POST handling:
77# op=create -> insert
88# op=update -> update by alert_rule_id
99# op=delete -> soft delete (is_active=0)
1010# op=toggle -> flip is_active
1111#======================================================================
1212use strict;
1313use warnings;
1414use CGI ();
1515use MODS::Config;
1616use MODS::DBConnect;
1717use MODS::Template;
1818use MODS::PageWrapper;
1919
2020my $cgi = CGI->new;
2121my $db = MODS::DBConnect->new;
2222my $tpl = MODS::Template->new;
2323
2424$|=1;
2525
2626my $dbh = $db->db_connect or die "DriftSense: cannot connect to storage DB\n";
2727
2828# ---- POST actions ---------------------------------------------------
2929my $method = $ENV{REQUEST_METHOD} || 'GET';
3030if ($method eq 'POST') {
3131 my $op = $cgi->param('op') || '';
3232
3333 if ($op eq 'create' || $op eq 'update') {
3434 my $rid = $op eq 'update' ? int($cgi->param('alert_rule_id') || 0) : 0;
3535 my $name = _clean($cgi->param('rule_name'), 200);
3636 my $kind = _one_of($cgi->param('match_kind'), 'either', 'file', 'schema', 'either');
3737 my $glob = _clean($cgi->param('match_path_glob'), 500);
3838 my $stat = _clean($cgi->param('match_status_list'), 200);
3939 my $xts = $cgi->param('exclude_ts_only') ? 1 : 0;
4040 my $dkind = _one_of($cgi->param('delivery_kind'),
4141 'generic_webhook', qw(slack discord generic_webhook email));
4242 my $durl = _clean($cgi->param('delivery_url'), 2000);
4343 my $dmail = _clean($cgi->param('delivery_email'), 500);
4444 my $rlim = int($cgi->param('rate_limit_per_min') || 0);
4545 my $csec = int($cgi->param('coalesce_window_s') || 0);
4646 my $act = $cgi->param('is_active') ? 1 : 0;
47 my $fthr = int($cgi->param('frequency_threshold') || 0);
48 my $fwin = int($cgi->param('frequency_window_min') || 60);
49 $fwin = 60 if $fwin < 1;
4750
4851 my $q = sub { $dbh->quote($_[0] // '') };
4952
5053 if ($op eq 'create') {
5154 $db->db_readwrite($dbh, qq~
5255 INSERT INTO ALERT_RULES
5356 (rule_name, is_active, match_kind, match_path_glob,
5457 match_status_list, exclude_ts_only,
5558 delivery_kind, delivery_url, delivery_email,
56 rate_limit_per_min, coalesce_window_s)
59 rate_limit_per_min, coalesce_window_s,
60 frequency_threshold, frequency_window_min)
5761 VALUES
5862 (@{[ $q->($name) ]}, $act, @{[ $q->($kind) ]},
5963 @{[ $q->($glob) ]}, @{[ $q->($stat) ]}, $xts,
6064 @{[ $q->($dkind) ]}, @{[ $q->($durl) ]}, @{[ $q->($dmail) ]},
61 $rlim, $csec)
65 $rlim, $csec, $fthr, $fwin)
6266 ~, __FILE__, __LINE__);
6367 } else {
6468 $db->db_readwrite($dbh, qq~
6569 UPDATE ALERT_RULES
6670 SET rule_name = @{[ $q->($name) ]},
6771 is_active = $act,
6872 match_kind = @{[ $q->($kind) ]},
6973 match_path_glob = @{[ $q->($glob) ]},
7074 match_status_list = @{[ $q->($stat) ]},
7175 exclude_ts_only = $xts,
7276 delivery_kind = @{[ $q->($dkind) ]},
7377 delivery_url = @{[ $q->($durl) ]},
7478 delivery_email = @{[ $q->($dmail) ]},
7579 rate_limit_per_min = $rlim,
76 coalesce_window_s = $csec
80 coalesce_window_s = $csec,
81 frequency_threshold = $fthr,
82 frequency_window_min = $fwin
7783 WHERE alert_rule_id = $rid
7884 ~, __FILE__, __LINE__);
7985 }
8086 }
8187 elsif ($op eq 'delete') {
8288 my $rid = int($cgi->param('alert_rule_id') || 0);
8389 $db->db_readwrite($dbh,
8490 "UPDATE ALERT_RULES SET is_active = 0 WHERE alert_rule_id = $rid",
8591 __FILE__, __LINE__);
8692 }
8793 elsif ($op eq 'toggle') {
8894 my $rid = int($cgi->param('alert_rule_id') || 0);
8995 $db->db_readwrite($dbh,
9096 "UPDATE ALERT_RULES SET is_active = 1 - is_active WHERE alert_rule_id = $rid",
9197 __FILE__, __LINE__);
9298 }
9399
94100 $db->db_disconnect($dbh);
95101 print "Status: 302 Found\nLocation: /alerts.cgi?nc=" . int(rand() * 10_000_000) . "\n\n";
96102 exit;
97103}
98104
99105# ---- GET: list rules + delivery history -----------------------------
100106print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
101107
102108my @rules = $db->db_readwrite_multiple($dbh, q~
103109 SELECT alert_rule_id, rule_name, is_active,
104110 match_kind, match_path_glob, match_status_list, exclude_ts_only,
105111 delivery_kind, delivery_url, delivery_email,
106112 rate_limit_per_min, coalesce_window_s,
113 frequency_threshold, frequency_window_min,
107114 fire_count, last_fired_at,
108115 UNIX_TIMESTAMP(last_fired_at) AS last_fired_epoch,
109116 UNIX_TIMESTAMP(created_at) AS created_epoch
110117 FROM ALERT_RULES
111118 ORDER BY is_active DESC, alert_rule_id ASC
112119~, __FILE__, __LINE__);
113120
114121foreach my $r (@rules) {
115122 $r->{status_pill} = $r->{is_active} ? 'pill-ok' : 'pill-mute';
116123 $r->{status_label} = $r->{is_active} ? 'active' : 'paused';
117124 $r->{kind_pill} = $r->{delivery_kind} eq 'slack' ? 'pill-info'
118125 : $r->{delivery_kind} eq 'discord' ? 'pill-warn'
119126 : 'pill-mute';
120127 $r->{match_glob_h} = $r->{match_path_glob} || '(all paths)';
121128 $r->{url_h} = _shorten($r->{delivery_url} || $r->{delivery_email} || '(no target)');
122129 $r->{last_fired_epoch} //= 0;
123130 $r->{last_fired_h} = $r->{last_fired_at} || 'never';
124131 $r->{fire_count} //= 0;
125132 $r->{edit_url} = "/alerts.cgi?edit=$r->{alert_rule_id}";
126133}
127134
128135my @deliveries = $db->db_readwrite_multiple($dbh, q~
129136 SELECT d.delivery_id, d.alert_rule_id, r.rule_name,
130137 d.source_kind, d.source_id, d.match_summary,
131138 d.delivery_status, d.http_status,
132139 UNIX_TIMESTAMP(d.fired_at) AS fired_epoch,
133140 d.fired_at,
134141 LEFT(d.error_message, 200) AS err_short
135142 FROM ALERT_DELIVERIES d
136143 LEFT JOIN ALERT_RULES r ON r.alert_rule_id = d.alert_rule_id
137144 ORDER BY d.delivery_id DESC
138145 LIMIT 30
139146~, __FILE__, __LINE__);
140147foreach my $d (@deliveries) {
141148 $d->{delivery_pill} = $d->{delivery_status} eq 'sent' ? 'pill-ok'
142149 : $d->{delivery_status} eq 'failed' ? 'pill-bad'
143150 : $d->{delivery_status} eq 'skipped' ? 'pill-mute'
144151 : 'pill-warn';
145152 $d->{source_url} = "/diff.cgi?kind=$d->{source_kind}&id=$d->{source_id}";
146153 $d->{summary_h} = _shorten($d->{match_summary} || '', 90);
147154 $d->{err_h} = $d->{err_short} ? "err: $d->{err_short}" : ($d->{http_status} ? "HTTP $d->{http_status}" : '-');
148155 $d->{fired_epoch} //= 0;
149156}
150157
151158# ---- Edit mode: prefill form ---------------------------------------
152159my $edit_id = int($cgi->param('edit') || 0);
153160my $edit = {};
154161if ($edit_id) {
155162 ($edit) = grep { $_->{alert_rule_id} == $edit_id } @rules;
156163 $edit ||= {};
157164}
158165my $is_edit = $edit_id && $edit && $edit->{alert_rule_id} ? 1 : 0;
159166
160167# defaults for a fresh form
161168my %form = (
162169 rule_name => $edit->{rule_name} // '',
163170 is_active_checked => (!$is_edit || $edit->{is_active}) ? 'checked' : '',
164171 match_kind => $edit->{match_kind} // 'either',
165172 match_path_glob => $edit->{match_path_glob} // '',
166173 match_status_list => $edit->{match_status_list} // '',
167174 exclude_ts_only_checked => (!$is_edit || $edit->{exclude_ts_only}) ? 'checked' : '',
168175 delivery_kind => $edit->{delivery_kind} // 'generic_webhook',
169176 delivery_url => $edit->{delivery_url} // '',
170177 delivery_email => $edit->{delivery_email} // '',
171178 rate_limit_per_min => $edit->{rate_limit_per_min} // 0,
172179 coalesce_window_s => $edit->{coalesce_window_s} // 0,
180 frequency_threshold => $edit->{frequency_threshold} // 0,
181 frequency_window_min => $edit->{frequency_window_min} // 60,
173182);
174183# select-option "selected" markers
175184foreach my $k (qw(either file schema)) {
176185 $form{"mk_${k}_sel"} = $form{match_kind} eq $k ? 'selected' : '';
177186}
178187foreach my $k (qw(generic_webhook slack discord email)) {
179188 $form{"dk_${k}_sel"} = $form{delivery_kind} eq $k ? 'selected' : '';
180189}
181190
182191$db->db_disconnect($dbh);
183192
184193# ---- Render body ----------------------------------------------------
185194my $tvars = {
186195 rules => \@rules,
187196 has_rules => scalar(@rules) ? 1 : 0,
188197 deliveries => \@deliveries,
189198 has_deliveries => scalar(@deliveries) ? 1 : 0,
190199 is_edit => $is_edit,
191200 edit_id => $is_edit ? $edit->{alert_rule_id} : 0,
192201 form_op => $is_edit ? 'update' : 'create',
193202 %form,
194203};
195204
196205my @body = $tpl->template('alerts.html', $tvars);
197206MODS::PageWrapper->new->wrapper(
198207 page_title => 'Alerts', page_key => 'alerts',
199208 body_html => join('', @body),
200209 userinfo => { display_name => 'Operator' },
201210);
202211
203212#---------------------------------------------------------------------
204213sub _clean {
205214 my ($s, $max) = @_;
206215 $s //= '';
207216 $s =~ s/[\r\n]/ /g;
208217 $s =~ s/^\s+|\s+$//g;
209218 $s = substr($s, 0, $max) if defined $max && length($s) > $max;
210219 return $s;
211220}
212221sub _one_of {
213222 my ($v, $default, @allowed) = @_;
214223 $v //= '';
215224 return (grep { $_ eq $v } @allowed) ? $v : $default;
216225}
217226sub _shorten {
218227 my ($s, $max) = @_;
219228 $max //= 60;
220229 $s //= '';
221230 return length($s) > $max ? substr($s, 0, $max - 3) . '...' : $s;
222231}