Restore

O Operator
Restore

Restore file to captured state

Every captured change carries its SHA-256-addressed content in BLOB_STORE. Restoring writes that content back to the original path — current file gets backed up to <path>.drift_restore_backup_<epoch> before overwrite.

What will change
Preview -- nothing has been written yet.
Target file/var/www/vhosts/3dshawn.com/site1/alerts.cgi
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-11 18:27:43
Captured SHAec54b82a47de610849197478f56cd0bde45930de1eaaf0e36736a18b2e73c5fd
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size9628 bytes
Current SHA3738a3d43d87
Same as captured?no -- restore will change it
What restore will change — live diff
Left column shows current on-disk content; right shows what restore will write. +3 additions, -12 deletions, 219 unchanged context lines.
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;
5047
5148 my $q = sub { $dbh->quote($_[0] // '') };
5249
5350 if ($op eq 'create') {
5451 $db->db_readwrite($dbh, qq~
5552 INSERT INTO ALERT_RULES
5653 (rule_name, is_active, match_kind, match_path_glob,
5754 match_status_list, exclude_ts_only,
5855 delivery_kind, delivery_url, delivery_email,
59 rate_limit_per_min, coalesce_window_s,
60 frequency_threshold, frequency_window_min)
56 rate_limit_per_min, coalesce_window_s)
6157 VALUES
6258 (@{[ $q->($name) ]}, $act, @{[ $q->($kind) ]},
6359 @{[ $q->($glob) ]}, @{[ $q->($stat) ]}, $xts,
6460 @{[ $q->($dkind) ]}, @{[ $q->($durl) ]}, @{[ $q->($dmail) ]},
65 $rlim, $csec, $fthr, $fwin)
61 $rlim, $csec)
6662 ~, __FILE__, __LINE__);
6763 } else {
6864 $db->db_readwrite($dbh, qq~
6965 UPDATE ALERT_RULES
7066 SET rule_name = @{[ $q->($name) ]},
7167 is_active = $act,
7268 match_kind = @{[ $q->($kind) ]},
7369 match_path_glob = @{[ $q->($glob) ]},
7470 match_status_list = @{[ $q->($stat) ]},
7571 exclude_ts_only = $xts,
7672 delivery_kind = @{[ $q->($dkind) ]},
7773 delivery_url = @{[ $q->($durl) ]},
7874 delivery_email = @{[ $q->($dmail) ]},
7975 rate_limit_per_min = $rlim,
80 coalesce_window_s = $csec,
81 frequency_threshold = $fthr,
82 frequency_window_min = $fwin
76 coalesce_window_s = $csec
8377 WHERE alert_rule_id = $rid
8478 ~, __FILE__, __LINE__);
8579 }
8680 }
8781 elsif ($op eq 'delete') {
8882 my $rid = int($cgi->param('alert_rule_id') || 0);
8983 $db->db_readwrite($dbh,
9084 "UPDATE ALERT_RULES SET is_active = 0 WHERE alert_rule_id = $rid",
9185 __FILE__, __LINE__);
9286 }
9387 elsif ($op eq 'toggle') {
9488 my $rid = int($cgi->param('alert_rule_id') || 0);
9589 $db->db_readwrite($dbh,
9690 "UPDATE ALERT_RULES SET is_active = 1 - is_active WHERE alert_rule_id = $rid",
9791 __FILE__, __LINE__);
9892 }
9993
10094 $db->db_disconnect($dbh);
10195 print "Status: 302 Found\nLocation: /alerts.cgi?nc=" . int(rand() * 10_000_000) . "\n\n";
10296 exit;
10397}
10498
10599# ---- GET: list rules + delivery history -----------------------------
106100print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
107101
108102my @rules = $db->db_readwrite_multiple($dbh, q~
109103 SELECT alert_rule_id, rule_name, is_active,
110104 match_kind, match_path_glob, match_status_list, exclude_ts_only,
111105 delivery_kind, delivery_url, delivery_email,
112106 rate_limit_per_min, coalesce_window_s,
113 frequency_threshold, frequency_window_min,
114107 fire_count, last_fired_at,
115108 UNIX_TIMESTAMP(last_fired_at) AS last_fired_epoch,
116109 UNIX_TIMESTAMP(created_at) AS created_epoch
117110 FROM ALERT_RULES
118111 ORDER BY is_active DESC, alert_rule_id ASC
119112~, __FILE__, __LINE__);
120113
121114foreach my $r (@rules) {
122115 $r->{status_pill} = $r->{is_active} ? 'pill-ok' : 'pill-mute';
123116 $r->{status_label} = $r->{is_active} ? 'active' : 'paused';
124117 $r->{kind_pill} = $r->{delivery_kind} eq 'slack' ? 'pill-info'
125118 : $r->{delivery_kind} eq 'discord' ? 'pill-warn'
126119 : 'pill-mute';
127120 $r->{match_glob_h} = $r->{match_path_glob} || '(all paths)';
128121 $r->{url_h} = _shorten($r->{delivery_url} || $r->{delivery_email} || '(no target)');
129122 $r->{last_fired_epoch} //= 0;
130123 $r->{last_fired_h} = $r->{last_fired_at} || 'never';
131124 $r->{fire_count} //= 0;
132125 $r->{edit_url} = "/alerts.cgi?edit=$r->{alert_rule_id}";
133126}
134127
135128my @deliveries = $db->db_readwrite_multiple($dbh, q~
136129 SELECT d.delivery_id, d.alert_rule_id, r.rule_name,
137130 d.source_kind, d.source_id, d.match_summary,
138131 d.delivery_status, d.http_status,
139132 UNIX_TIMESTAMP(d.fired_at) AS fired_epoch,
140133 d.fired_at,
141134 LEFT(d.error_message, 200) AS err_short
142135 FROM ALERT_DELIVERIES d
143136 LEFT JOIN ALERT_RULES r ON r.alert_rule_id = d.alert_rule_id
144137 ORDER BY d.delivery_id DESC
145138 LIMIT 30
146139~, __FILE__, __LINE__);
147140foreach my $d (@deliveries) {
148141 $d->{delivery_pill} = $d->{delivery_status} eq 'sent' ? 'pill-ok'
149142 : $d->{delivery_status} eq 'failed' ? 'pill-bad'
150143 : $d->{delivery_status} eq 'skipped' ? 'pill-mute'
151144 : 'pill-warn';
152145 $d->{source_url} = "/diff.cgi?kind=$d->{source_kind}&id=$d->{source_id}";
153146 $d->{summary_h} = _shorten($d->{match_summary} || '', 90);
154147 $d->{err_h} = $d->{err_short} ? "err: $d->{err_short}" : ($d->{http_status} ? "HTTP $d->{http_status}" : '-');
155148 $d->{fired_epoch} //= 0;
156149}
157150
158151# ---- Edit mode: prefill form ---------------------------------------
159152my $edit_id = int($cgi->param('edit') || 0);
160153my $edit = {};
161154if ($edit_id) {
162155 ($edit) = grep { $_->{alert_rule_id} == $edit_id } @rules;
163156 $edit ||= {};
164157}
165158my $is_edit = $edit_id && $edit && $edit->{alert_rule_id} ? 1 : 0;
166159
167160# defaults for a fresh form
168161my %form = (
169162 rule_name => $edit->{rule_name} // '',
170163 is_active_checked => (!$is_edit || $edit->{is_active}) ? 'checked' : '',
171164 match_kind => $edit->{match_kind} // 'either',
172165 match_path_glob => $edit->{match_path_glob} // '',
173166 match_status_list => $edit->{match_status_list} // '',
174167 exclude_ts_only_checked => (!$is_edit || $edit->{exclude_ts_only}) ? 'checked' : '',
175168 delivery_kind => $edit->{delivery_kind} // 'generic_webhook',
176169 delivery_url => $edit->{delivery_url} // '',
177170 delivery_email => $edit->{delivery_email} // '',
178171 rate_limit_per_min => $edit->{rate_limit_per_min} // 0,
179172 coalesce_window_s => $edit->{coalesce_window_s} // 0,
180 frequency_threshold => $edit->{frequency_threshold} // 0,
181 frequency_window_min => $edit->{frequency_window_min} // 60,
182173);
183174# select-option "selected" markers
184175foreach my $k (qw(either file schema)) {
185176 $form{"mk_${k}_sel"} = $form{match_kind} eq $k ? 'selected' : '';
186177}
187178foreach my $k (qw(generic_webhook slack discord email)) {
188179 $form{"dk_${k}_sel"} = $form{delivery_kind} eq $k ? 'selected' : '';
189180}
190181
191182$db->db_disconnect($dbh);
192183
193184# ---- Render body ----------------------------------------------------
194185my $tvars = {
195186 rules => \@rules,
196187 has_rules => scalar(@rules) ? 1 : 0,
197188 deliveries => \@deliveries,
198189 has_deliveries => scalar(@deliveries) ? 1 : 0,
199190 is_edit => $is_edit,
200191 edit_id => $is_edit ? $edit->{alert_rule_id} : 0,
201192 form_op => $is_edit ? 'update' : 'create',
202193 %form,
203194};
204195
205196my @body = $tpl->template('alerts.html', $tvars);
206197MODS::PageWrapper->new->wrapper(
207198 page_title => 'Alerts', page_key => 'alerts',
208199 body_html => join('', @body),
209200 userinfo => { display_name => 'Operator' },
210201);
211202
212203#---------------------------------------------------------------------
213204sub _clean {
214205 my ($s, $max) = @_;
215206 $s //= '';
216207 $s =~ s/[\r\n]/ /g;
217208 $s =~ s/^\s+|\s+$//g;
218209 $s = substr($s, 0, $max) if defined $max && length($s) > $max;
219210 return $s;
220211}
221212sub _one_of {
222213 my ($v, $default, @allowed) = @_;
223214 $v //= '';
224215 return (grep { $_ eq $v } @allowed) ? $v : $default;
225216}
226217sub _shorten {
227218 my ($s, $max) = @_;
228219 $max //= 60;
229220 $s //= '';
230221 return length($s) > $max ? substr($s, 0, $max - 3) . '...' : $s;
231222}
Confirm restore
Backs current content to <target>.drift_restore_backup_<epoch>, writes the captured content, verifies SHA post-write.
Cancel Logged to RESTORE_LOG regardless of outcome.