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/site.cgi
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-12 01:04:49
Captured SHAa339c7d2fcb050d06a945425700d1c1e0129ac1582621f269754bea758a7d0e6
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size22712 bytes
Current SHAcccbd8589bf1
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. +4 additions, -4 deletions, 487 unchanged context lines.
11#!/usr/bin/perl
22#======================================================================
33# DriftSense -- /site.cgi?mid=N
44#
55# The awesome-detail drilldown for one monitored site. Every stat we
66# have. Server info, storage math, activity breakdown, compile summary,
77# hotspots, releases scoped here, cross-site dedup, recent restores,
88# pin drift, deletion log, and quick actions.
99#======================================================================
1010use strict;
1111use warnings;
1212use CGI ();
1313use POSIX ();
1414use MODS::Config;
1515use MODS::DBConnect;
1616use MODS::Template;
1717use MODS::PageWrapper;
1818use MODS::Charts;
1919
2020my $cgi = CGI->new;
2121my $db = MODS::DBConnect->new;
2222my $tpl = MODS::Template->new;
2323$|=1;
2424
2525my $mid = int($cgi->param('mid') || $cgi->param('monitor') || 0);
2626my $dbh = $db->db_connect or die "DB connect failed\n";
2727
2828# ---- POST: save current state as a Named Release ------------------
2929if (($ENV{REQUEST_METHOD} || '') eq 'POST' && $cgi->param('save_snapshot')) {
3030 my $name = $cgi->param('name') || ('snapshot-' . time());
3131 my $vlbl = $cgi->param('version_label') || $name;
3232 my $tier = $cgi->param('tier') || 'draft';
3333 $tier = 'draft' unless $tier =~ /^(draft|working|stable)$/;
3434 my $desc = "Snapshot of monitor #$mid taken at " . POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime);
3535 my $q_name = $dbh->quote($name);
3636 my $q_vlbl = $dbh->quote($vlbl);
3737 my $q_tier = $dbh->quote($tier);
3838 my $q_desc = $dbh->quote($desc);
3939 $db->db_readwrite($dbh, qq~
4040 INSERT INTO NAMED_RELEASES
4141 (name, version_label, tier, description, released_at, released_by,
4242 is_locked, created_at, scope_monitor_id)
4343 VALUES
4444 ($q_name, $q_vlbl, $q_tier, $q_desc, NOW(), 'Operator',
4545 1, NOW(), $mid)
4646 ~, __FILE__, __LINE__);
4747 my $r = $db->db_readwrite($dbh, "SELECT LAST_INSERT_ID() AS id", __FILE__, __LINE__);
4848 my $rid = $r && $r->{id};
4949 # Pin every current-latest FILE_CHANGES row for this monitor
5050 if ($rid) {
5151 foreach my $pin ($db->db_readwrite_multiple($dbh, qq~
5252 SELECT MAX(file_changes_id) AS id, file_name
5353 FROM FILE_CHANGES
5454 WHERE file_monitor_list_id = $mid
5555 AND blob_sha IS NOT NULL
5656 AND status != 'deleted'
5757 GROUP BY file_name
5858 ~, __FILE__, __LINE__)) {
5959 my $q_fname = $dbh->quote($pin->{file_name});
6060 my $b = $db->db_readwrite($dbh,
6161 "SELECT blob_sha FROM FILE_CHANGES WHERE file_changes_id = $pin->{id}",
6262 __FILE__, __LINE__);
6363 my $q_sha = $dbh->quote($b ? $b->{blob_sha} : '');
6464 $db->db_readwrite($dbh, qq~
6565 INSERT INTO NAMED_RELEASE_PINS
6666 (named_release_id, file_changes_id, file_name, blob_sha)
6767 VALUES ($rid, $pin->{id}, $q_fname, $q_sha)
6868 ~, __FILE__, __LINE__);
6969 }
7070 }
7171 $db->db_disconnect($dbh);
7272 print "Status: 302 Found\nLocation: /site.cgi?mid=$mid&saved=$rid\n\n"; exit;
7373}
7474
7575# ---- Monitor + server ----------------------------------------------
7676my $monitor = $mid ? $db->db_readwrite($dbh, qq~
7777 SELECT m.file_monitor_list_id AS mid,
7878 m.scan_name, m.scan_path, m.ignore_list, m.file_type_filter,
7979 m.max_file_size_bytes, m.retention_days,
8080 DATE_FORMAT(m.last_scanned, '%b %d, %Y %H:%i') AS last_scanned_h,
8181 UNIX_TIMESTAMP(m.last_scanned) AS last_scanned_epoch,
8282 m.server_id, s.server_name, s.kind AS server_kind,
8383 s.ssh_host, s.ssh_user, s.ssh_port,
84 DATE_FORMAT(s.last_heartbeat_at, '%b %d %H:%M') AS heartbeat_h,
84 DATE_FORMAT(s.last_heartbeat_at, '%b %d %H:%i') AS heartbeat_h,
8585 UNIX_TIMESTAMP(s.last_heartbeat_at) AS heartbeat_epoch,
8686 s.status AS server_status
8787 FROM FILE_MONITOR_SETTINGS m
8888 LEFT JOIN SERVERS s ON s.server_id = m.server_id
8989 WHERE m.file_monitor_list_id = $mid LIMIT 1
9090~, __FILE__, __LINE__) : undef;
9191
9292unless ($monitor && $monitor->{mid}) {
9393 print "Content-Type: text/html; charset=utf-8\n\n";
9494 my @body = $tpl->template('site.html', {
9595 has_error => 1,
9696 error_msg => "Monitor #$mid not found.",
9797 });
9898 MODS::PageWrapper->new->wrapper(
9999 page_title => 'Site', page_key => '',
100100 body_html => join('', @body), userinfo => { display_name => 'Operator' },
101101 );
102102 exit;
103103}
104104
105105# ---- KPI counts + status breakdown ---------------------------------
106106my $kpi = $db->db_readwrite($dbh, qq~
107107 SELECT SUM(CASE WHEN date_time >= DATE_SUB(NOW(), INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS c_24h,
108108 SUM(CASE WHEN date_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 ELSE 0 END) AS c_7d,
109109 SUM(CASE WHEN date_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) AS c_30d,
110110 SUM(CASE WHEN date_time >= DATE_SUB(NOW(), INTERVAL 2 DAY)
111111 AND date_time < DATE_SUB(NOW(), INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS c_24h_prev,
112112 COUNT(DISTINCT CASE WHEN status != 'deleted' THEN file_name END) AS active_files,
113113 COUNT(DISTINCT file_name) AS all_files,
114114 COUNT(*) AS c_all,
115115 SUM(CASE WHEN status = 'added' THEN 1 ELSE 0 END) AS n_added,
116116 SUM(CASE WHEN status = 'modified' THEN 1 ELSE 0 END) AS n_modified,
117117 SUM(CASE WHEN status = 'deleted' THEN 1 ELSE 0 END) AS n_deleted,
118118 SUM(CASE WHEN is_ts_only = 1 THEN 1 ELSE 0 END) AS n_ts_only,
119119 MAX(UNIX_TIMESTAMP(date_time)) AS last_epoch,
120120 MIN(UNIX_TIMESTAMP(date_time)) AS first_epoch
121121 FROM FILE_CHANGES
122122 WHERE file_monitor_list_id = $mid
123123~, __FILE__, __LINE__);
124124
125125# Trend arrow
126126my ($trend_class, $trend_txt) = ('flat', '&mdash;');
127127if ($kpi) {
128128 my $cur = $kpi->{c_24h} || 0;
129129 my $prev = $kpi->{c_24h_prev} || 0;
130130 if ($prev == 0 && $cur > 0) { $trend_class = 'up'; $trend_txt = '&uarr; new'; }
131131 elsif ($prev > 0) {
132132 my $d = ($cur - $prev) / $prev * 100;
133133 $trend_class = $d > 5 ? 'up' : $d < -5 ? 'down' : 'flat';
134134 my $arr = $trend_class eq 'up' ? '&uarr;' : $trend_class eq 'down' ? '&darr;' : '&#8226;';
135135 $trend_txt = sprintf('%s %+.0f%%', $arr, $d);
136136 }
137137}
138138
139139# ---- Storage: blob math for this monitor ---------------------------
140140my $storage = $db->db_readwrite($dbh, qq~
141141 SELECT COUNT(DISTINCT bs.blob_sha) AS unique_blobs,
142142 COALESCE(SUM(DISTINCT bs.size_uncompressed), 0) AS bytes_raw,
143143 COALESCE(SUM(DISTINCT bs.size_compressed), 0) AS bytes_gz,
144144 COALESCE(SUM(bs.ref_count), 0) AS ref_total
145145 FROM FILE_CHANGES fc
146146 JOIN BLOB_STORE bs ON bs.blob_sha = fc.blob_sha
147147 WHERE fc.file_monitor_list_id = $mid
148148 AND fc.blob_sha IS NOT NULL
149149~, __FILE__, __LINE__);
150150
151151my $unique_blobs = ($storage && $storage->{unique_blobs}) || 0;
152152my $bytes_raw = ($storage && $storage->{bytes_raw}) || 0;
153153my $bytes_gz = ($storage && $storage->{bytes_gz}) || 0;
154154my $dedup_ratio = ($bytes_gz && $bytes_raw)
155155 ? sprintf('%.1fx', $bytes_raw / $bytes_gz) : '-';
156156my $savings_pct = $bytes_raw
157157 ? sprintf('%.0f%%', 100 * (1 - ($bytes_gz / $bytes_raw))) : '-';
158158
159159# ---- 90-day activity chart -----------------------------------------
160160my @day_rows = $db->db_readwrite_multiple($dbh, qq~
161161 SELECT DATE_FORMAT(date_time, '%Y-%m-%d') AS d, COUNT(*) AS n
162162 FROM FILE_CHANGES
163163 WHERE file_monitor_list_id = $mid
164164 AND date_time >= DATE_SUB(NOW(), INTERVAL 90 DAY)
165165 GROUP BY d
166166~, __FILE__, __LINE__);
167167my %by_day; $by_day{$_->{d}} = $_->{n} for @day_rows;
168168my (@days, @day_keys, @file_series, @empty_schema);
169169my $now = time;
170170for (my $i = 89; $i >= 0; $i--) {
171171 my $t = $now - $i * 86400;
172172 my $key = POSIX::strftime('%Y-%m-%d', localtime($t));
173173 my $lbl = POSIX::strftime('%b %d', localtime($t));
174174 push @days, $lbl; push @day_keys, $key;
175175 push @file_series, ($by_day{$key} || 0);
176176 push @empty_schema, 0;
177177}
178178my $timeline_svg = MODS::Charts::stacked_bars(
179179 days => \@days, day_keys => \@day_keys,
180180 series => { file => \@file_series, schema => \@empty_schema },
181181 width => 900, height => 150,
182182 link_file => '/file_changes.cgi',
183183 link_schema => '/schema_changes.cgi',
184184);
185185
186186# ---- Recent changes (last 25) --------------------------------------
187187my @recent = $db->db_readwrite_multiple($dbh, qq~
188188 SELECT fc.file_changes_id AS id, fc.file_name, fc.status, fc.blob_sha,
189189 fc.is_ts_only, UNIX_TIMESTAMP(fc.date_time) AS ts_epoch, fc.date_time
190190 FROM FILE_CHANGES fc
191191 WHERE fc.file_monitor_list_id = $mid
192192 ORDER BY fc.file_changes_id DESC LIMIT 25
193193~, __FILE__, __LINE__);
194194foreach my $r (@recent) {
195195 $r->{status_pill} = $r->{status} eq 'added' ? 'pill-ok'
196196 : $r->{status} eq 'modified' ? 'pill-warn'
197197 : $r->{status} eq 'deleted' ? 'pill-bad'
198198 : 'pill-mute';
199199 $r->{file_short} = length($r->{file_name} // '') > 60
200200 ? '...' . substr($r->{file_name}, -57) : $r->{file_name};
201201 $r->{blob_short} = $r->{blob_sha} ? substr($r->{blob_sha}, 0, 8) : '-';
202202 $r->{diff_url} = "/diff.cgi?kind=file&id=$r->{id}";
203203 $r->{restore_url} = "/restore.cgi?id=$r->{id}";
204204 $r->{can_restore} = ($r->{blob_sha} && $r->{status} ne 'deleted') ? 1 : 0;
205205 $r->{ts_epoch} //= 0;
206206}
207207
208208# ---- Change-frequency (top 10) -------------------------------------
209209my @hotspots = $db->db_readwrite_multiple($dbh, qq~
210210 SELECT file_name, COUNT(*) AS ct,
211211 MAX(file_changes_id) AS latest_id,
212212 MAX(UNIX_TIMESTAMP(date_time)) AS latest_epoch
213213 FROM FILE_CHANGES
214214 WHERE file_monitor_list_id = $mid
215215 AND date_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
216216 GROUP BY file_name
217217 ORDER BY ct DESC LIMIT 10
218218~, __FILE__, __LINE__);
219219foreach my $h (@hotspots) {
220220 $h->{file_short} = length($h->{file_name} // '') > 55
221221 ? '...' . substr($h->{file_name}, -52) : $h->{file_name};
222222 $h->{diff_url} = "/diff.cgi?kind=file&id=" . ($h->{latest_id} || 0);
223223 $h->{latest_epoch} //= 0;
224224}
225225
226226# ---- Named releases covering this site -----------------------------
227227my @releases = $db->db_readwrite_multiple($dbh, qq~
228228 SELECT nr.named_release_id AS id, nr.name, nr.version_label, nr.tier,
229229 nr.description,
230230 UNIX_TIMESTAMP(nr.released_at) AS released_epoch,
231231 DATE_FORMAT(nr.released_at, '%b %d, %Y') AS released_h,
232232 (SELECT COUNT(*) FROM NAMED_RELEASE_PINS p WHERE p.named_release_id = nr.named_release_id) AS pin_ct
233233 FROM NAMED_RELEASES nr
234234 WHERE nr.scope_monitor_id = $mid
235235 OR nr.scope_server_id = $monitor->{server_id}
236236 OR (nr.scope_monitor_id IS NULL AND nr.scope_server_id IS NULL)
237237 ORDER BY (nr.tier = 'stable') DESC, (nr.tier = 'working') DESC, nr.named_release_id DESC
238238 LIMIT 12
239239~, __FILE__, __LINE__);
240240foreach my $r (@releases) {
241241 $r->{tier} //= 'draft';
242242 $r->{tier_pill} = $r->{tier} eq 'stable' ? 'pill-ok'
243243 : $r->{tier} eq 'working' ? 'pill-info'
244244 : 'pill-mute';
245245 $r->{label} = $r->{version_label} || $r->{name};
246246 $r->{restore_url} = "/restore_release.cgi?release_id=$r->{id}";
247247 $r->{released_epoch} //= 0;
248248 $r->{pin_ct} //= 0;
249249}
250250
251251# ---- Deletion log --------------------------------------------------
252252my @deletions = $db->db_readwrite_multiple($dbh, qq~
253253 SELECT fc.file_changes_id AS id, fc.file_name,
254254 UNIX_TIMESTAMP(fc.date_time) AS ts_epoch,
255255 fc.date_time,
256256 (SELECT MAX(fc2.file_changes_id) FROM FILE_CHANGES fc2
257257 WHERE fc2.file_monitor_list_id = $mid
258258 AND fc2.file_name = fc.file_name
259259 AND fc2.blob_sha IS NOT NULL
260260 AND fc2.file_changes_id < fc.file_changes_id) AS last_content_id
261261 FROM FILE_CHANGES fc
262262 WHERE fc.file_monitor_list_id = $mid
263263 AND fc.status = 'deleted'
264264 ORDER BY fc.file_changes_id DESC LIMIT 10
265265~, __FILE__, __LINE__);
266266foreach my $d (@deletions) {
267267 $d->{file_short} = length($d->{file_name} // '') > 55
268268 ? '...' . substr($d->{file_name}, -52) : $d->{file_name};
269269 $d->{ts_epoch} //= 0;
270270 $d->{restore_url} = $d->{last_content_id}
271271 ? "/restore.cgi?id=$d->{last_content_id}"
272272 : '';
273273 $d->{diff_url} = $d->{last_content_id}
274274 ? "/diff.cgi?kind=file&id=$d->{last_content_id}"
275275 : '';
276276 $d->{can_restore} = $d->{last_content_id} ? 1 : 0;
277277}
278278
279279# ---- Recent restores affecting this site ---------------------------
280280my @restores = $db->db_readwrite_multiple($dbh, qq~
281281 SELECT rl.restore_id, rl.target_file, rl.status,
282282 LEFT(rl.source_blob_sha, 12) AS sha_short,
283283 rl.restored_by,
284284 UNIX_TIMESTAMP(rl.restored_at) AS restored_epoch,
285 DATE_FORMAT(rl.restored_at, '%b %d %H:%M') AS restored_h
285 DATE_FORMAT(rl.restored_at, '%b %d %H:%i') AS restored_h
286286 FROM RESTORE_LOG rl
287287 WHERE rl.server_id = $monitor->{server_id}
288288 AND rl.target_file LIKE @{[ $dbh->quote($monitor->{scan_path} . '%') ]}
289289 ORDER BY rl.restore_id DESC LIMIT 8
290290~, __FILE__, __LINE__);
291291foreach my $r (@restores) {
292292 $r->{status_pill} = $r->{status} eq 'success' ? 'pill-ok' :
293293 $r->{status} eq 'failed' ? 'pill-bad' : 'pill-mute';
294294 $r->{file_short} = length($r->{target_file} // '') > 55
295295 ? '...' . substr($r->{target_file}, -52) : $r->{target_file};
296296 $r->{restored_epoch} //= 0;
297297}
298298
299299# ---- Compile-check summary -----------------------------------------
300300my $compile_summary = $db->db_readwrite($dbh, qq~
301301 SELECT SUM(CASE WHEN bs.compile_status = 'ok' THEN 1 ELSE 0 END) AS n_ok,
302302 SUM(CASE WHEN bs.compile_status = 'error' THEN 1 ELSE 0 END) AS n_err,
303303 COUNT(DISTINCT bs.blob_sha) AS n_checked
304304 FROM FILE_CHANGES fc
305305 JOIN BLOB_STORE bs ON bs.blob_sha = fc.blob_sha
306306 WHERE fc.file_monitor_list_id = $mid
307307 AND bs.compile_status IN ('ok', 'error')
308308~, __FILE__, __LINE__);
309309my @compile_errors = $db->db_readwrite_multiple($dbh, qq~
310310 SELECT fc.file_name, fc.file_changes_id AS id,
311311 LEFT(bs.compile_message, 200) AS msg
312312 FROM FILE_CHANGES fc
313313 JOIN BLOB_STORE bs ON bs.blob_sha = fc.blob_sha
314314 WHERE fc.file_monitor_list_id = $mid
315315 AND bs.compile_status = 'error'
316316 GROUP BY fc.file_name, fc.file_changes_id, bs.compile_message
317317 ORDER BY fc.file_changes_id DESC LIMIT 8
318318~, __FILE__, __LINE__);
319319foreach my $e (@compile_errors) {
320320 $e->{file_short} = length($e->{file_name} // '') > 55
321321 ? '...' . substr($e->{file_name}, -52) : $e->{file_name};
322322 $e->{diff_url} = "/diff.cgi?kind=file&id=$e->{id}";
323323}
324324
325325# ---- Cross-site dedup: blobs this site shares elsewhere ------------
326326my @shared = $db->db_readwrite_multiple($dbh, qq~
327327 SELECT fc.blob_sha,
328328 COUNT(DISTINCT fc2.file_monitor_list_id) AS site_ct,
329329 GROUP_CONCAT(DISTINCT m.scan_name SEPARATOR ' | ') AS scans,
330330 MAX(fc.file_changes_id) AS example_id,
331331 bs.size_uncompressed
332332 FROM FILE_CHANGES fc
333333 JOIN FILE_CHANGES fc2 ON fc2.blob_sha = fc.blob_sha
334334 AND fc2.file_monitor_list_id != $mid
335335 LEFT JOIN FILE_MONITOR_SETTINGS m ON m.file_monitor_list_id = fc2.file_monitor_list_id
336336 JOIN BLOB_STORE bs ON bs.blob_sha = fc.blob_sha
337337 WHERE fc.file_monitor_list_id = $mid
338338 AND fc.blob_sha IS NOT NULL
339339 GROUP BY fc.blob_sha, bs.size_uncompressed
340340 ORDER BY site_ct DESC LIMIT 6
341341~, __FILE__, __LINE__);
342342foreach my $b (@shared) {
343343 $b->{sha_short} = substr($b->{blob_sha} // '', 0, 12);
344344 $b->{size_h} = _fmt_bytes($b->{size_uncompressed} || 0);
345345 $b->{diff_url} = "/diff.cgi?kind=file&id=" . ($b->{example_id} || 0);
346346 $b->{scans_h} = length($b->{scans} // '') > 90
347347 ? substr($b->{scans}, 0, 87) . '...' : $b->{scans};
348348}
349349
350350# ---- Pin drift affecting this site ---------------------------------
351351my @drift = $db->db_readwrite_multiple($dbh, qq~
352352 SELECT d.file_name, d.pinned_sha, d.current_sha,
353353 r.name AS release_name, r.named_release_id AS rid,
354354 UNIX_TIMESTAMP(d.detected_at) AS detected_epoch,
355 DATE_FORMAT(d.detected_at, '%b %d %H:%M') AS detected_h
355 DATE_FORMAT(d.detected_at, '%b %d %H:%i') AS detected_h
356356 FROM PIN_DRIFT_LOG d
357357 LEFT JOIN NAMED_RELEASES r ON r.named_release_id = d.named_release_id
358358 LEFT JOIN FILE_CHANGES fc ON fc.file_changes_id = d.file_changes_id
359359 WHERE fc.file_monitor_list_id = $mid
360360 ORDER BY d.drift_id DESC LIMIT 6
361361~, __FILE__, __LINE__);
362362foreach my $d (@drift) {
363363 $d->{file_short} = length($d->{file_name} // '') > 50
364364 ? '...' . substr($d->{file_name}, -47) : $d->{file_name};
365365 $d->{pinned_short} = substr($d->{pinned_sha} // '', 0, 12);
366366 $d->{current_short}= substr($d->{current_sha} // '', 0, 12);
367367 $d->{restore_url} = "/restore_release.cgi?release_id=" . ($d->{rid} || 0);
368368 $d->{detected_epoch} //= 0;
369369}
370370
371371# ---- Longest-active files -----------------------------------------
372372my @longevity = $db->db_readwrite_multiple($dbh, qq~
373373 SELECT file_name,
374374 MIN(UNIX_TIMESTAMP(date_time)) AS first_epoch,
375375 MAX(UNIX_TIMESTAMP(date_time)) AS last_epoch,
376376 COUNT(*) AS ct,
377377 MAX(file_changes_id) AS latest_id
378378 FROM FILE_CHANGES
379379 WHERE file_monitor_list_id = $mid
380380 AND status != 'deleted'
381381 GROUP BY file_name
382382 ORDER BY first_epoch ASC LIMIT 8
383383~, __FILE__, __LINE__);
384384foreach my $L (@longevity) {
385385 $L->{file_short} = length($L->{file_name} // '') > 55
386386 ? '...' . substr($L->{file_name}, -52) : $L->{file_name};
387387 $L->{diff_url} = "/diff.cgi?kind=file&id=" . ($L->{latest_id} || 0);
388388 my $days = int((time - ($L->{first_epoch} || time)) / 86400);
389389 $L->{days_tracked} = $days;
390390 $L->{first_epoch} //= 0;
391391}
392392
393393$db->db_disconnect($dbh);
394394
395395my $tvars = {
396396 mid => $mid,
397397 scan_name => $monitor->{scan_name},
398398 scan_path => $monitor->{scan_path},
399399 server_name => $monitor->{server_name} // 'local',
400400 server_kind => $monitor->{server_kind} // 'local',
401401 server_status => $monitor->{server_status} // 'active',
402402 ssh_host => $monitor->{ssh_host} // '',
403403 ssh_user => $monitor->{ssh_user} // '',
404404 ssh_port => $monitor->{ssh_port} // 22,
405405 is_agent => (($monitor->{server_kind}//'') eq 'ssh_agent') ? 1 : 0,
406406 retention_h => defined($monitor->{retention_days}) && $monitor->{retention_days}
407407 ? "$monitor->{retention_days} days"
408408 : 'global default',
409409 ignore_list => $monitor->{ignore_list} // '(none)',
410410 file_type_filter => $monitor->{file_type_filter} // '(all types)',
411411 max_file_size_h => $monitor->{max_file_size_bytes}
412412 ? _fmt_bytes($monitor->{max_file_size_bytes}) : 'global default',
413413 last_scanned_h => $monitor->{last_scanned_h} // 'never',
414414 last_scanned_epoch => $monitor->{last_scanned_epoch} || 0,
415415 heartbeat_h => $monitor->{heartbeat_h} // 'never',
416416 heartbeat_epoch => $monitor->{heartbeat_epoch} || 0,
417417
418418 kpi_24h => ($kpi && $kpi->{c_24h}) || 0,
419419 kpi_24h_prev => ($kpi && $kpi->{c_24h_prev}) || 0,
420420 kpi_7d => ($kpi && $kpi->{c_7d}) || 0,
421421 kpi_30d => ($kpi && $kpi->{c_30d}) || 0,
422422 kpi_active => ($kpi && $kpi->{active_files})|| 0,
423423 kpi_all_files => ($kpi && $kpi->{all_files}) || 0,
424424 kpi_lifetime => ($kpi && $kpi->{c_all}) || 0,
425425 n_added => ($kpi && $kpi->{n_added}) || 0,
426426 n_modified => ($kpi && $kpi->{n_modified}) || 0,
427427 n_deleted => ($kpi && $kpi->{n_deleted}) || 0,
428428 n_ts_only => ($kpi && $kpi->{n_ts_only}) || 0,
429429 first_captured => $kpi && $kpi->{first_epoch}
430430 ? POSIX::strftime('%b %d, %Y', localtime($kpi->{first_epoch}))
431431 : '(none)',
432432 last_captured => $kpi && $kpi->{last_epoch}
433 ? POSIX::strftime('%b %d %H:%M', localtime($kpi->{last_epoch}))
433 ? POSIX::strftime('%b %d %H:%i', localtime($kpi->{last_epoch}))
434434 : '(none)',
435435 last_captured_epoch => ($kpi && $kpi->{last_epoch}) || 0,
436436 trend_class => $trend_class,
437437 trend_txt => $trend_txt,
438438
439439 unique_blobs => $unique_blobs,
440440 bytes_raw_h => _fmt_bytes($bytes_raw),
441441 bytes_gz_h => _fmt_bytes($bytes_gz),
442442 dedup_ratio => $dedup_ratio,
443443 savings_pct => $savings_pct,
444444 ref_total => $storage ? $storage->{ref_total} : 0,
445445
446446 timeline_svg => $timeline_svg,
447447 recent => \@recent,
448448 has_recent => scalar(@recent) ? 1 : 0,
449449 hotspots => \@hotspots,
450450 has_hotspots => scalar(@hotspots) ? 1 : 0,
451451 releases => \@releases,
452452 has_releases => scalar(@releases) ? 1 : 0,
453453 deletions => \@deletions,
454454 has_deletions => scalar(@deletions)? 1 : 0,
455455 restores => \@restores,
456456 has_restores => scalar(@restores) ? 1 : 0,
457457 cc_ok => ($compile_summary && $compile_summary->{n_ok}) || 0,
458458 cc_err => ($compile_summary && $compile_summary->{n_err}) || 0,
459459 cc_checked => ($compile_summary && $compile_summary->{n_checked})|| 0,
460460 has_compile_data => ($compile_summary && $compile_summary->{n_checked}) ? 1 : 0,
461461 compile_errors => \@compile_errors,
462462 has_compile_errors => scalar(@compile_errors) ? 1 : 0,
463463 shared_blobs => \@shared,
464464 has_shared => scalar(@shared) ? 1 : 0,
465465 drift => \@drift,
466466 has_drift => scalar(@drift) ? 1 : 0,
467467 longevity => \@longevity,
468468 has_longevity => scalar(@longevity) ? 1 : 0,
469469
470470 changes_url => "/file_changes.cgi?monitor=$mid",
471471 pin_url => "/pin_hotspots.cgi?monitor=$mid",
472472 export_url => "/export.cgi?server_id=$monitor->{server_id}",
473473 saved_id => int($cgi->param('saved') || 0),
474474};
475475
476476print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
477477my @body = $tpl->template('site.html', $tvars);
478478MODS::PageWrapper->new->wrapper(
479479 page_title => "Site: " . ($monitor->{scan_name} // '?'),
480480 page_key => 'sites',
481481 body_html => join('', @body),
482482 userinfo => { display_name => 'Operator' },
483483);
484484
485485sub _fmt_bytes {
486486 my $n = shift; $n //= 0;
487487 return "$n B" if $n < 1024;
488488 return sprintf('%.1f KB', $n/1024) if $n < 1024*1024;
489489 return sprintf('%.1f MB', $n/1024/1024) if $n < 1024*1024*1024;
490490 return sprintf('%.1f GB', $n/1024/1024/1024);
491491}
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.