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

O Operator
Diff

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

added on local at 2026-07-12 00:48:43

Added
+205
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to c4e290a1653d
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 -- /site.cgi?mid=N
4#
5# Focused per-site drilldown. Aimed at the "what's happening in THIS
6# codebase specifically?" question. Programmer view: recent activity,
7# what changed, quick restore. Sysadmin view: activity chart, drift
8# score. Owner view: KPI totals + stable versions.
9#======================================================================
10use strict;
11use warnings;
12use CGI ();
13use POSIX ();
14use MODS::Config;
15use MODS::DBConnect;
16use MODS::Template;
17use MODS::PageWrapper;
18use MODS::Charts;
19
20my $cgi = CGI->new;
21my $db = MODS::DBConnect->new;
22my $tpl = MODS::Template->new;
23$|=1;
24
25my $mid = int($cgi->param('mid') || 0);
26$mid = int($cgi->param('monitor') || 0) unless $mid; # accept ?monitor= alias
27
28my $dbh = $db->db_connect or die "DB connect failed\n";
29
30# ---- Monitor + server ----------------------------------------------
31my $monitor = $mid ? $db->db_readwrite($dbh, qq~
32 SELECT m.file_monitor_list_id AS mid,
33 m.scan_name, m.scan_path, m.ignore_list, m.file_type_filter,
34 m.max_file_size_bytes, m.retention_days,
35 m.server_id, s.server_name, s.kind AS server_kind,
36 s.ssh_host, s.ssh_user
37 FROM FILE_MONITOR_SETTINGS m
38 LEFT JOIN SERVERS s ON s.server_id = m.server_id
39 WHERE m.file_monitor_list_id = $mid LIMIT 1
40~, __FILE__, __LINE__) : undef;
41
42unless ($monitor && $monitor->{mid}) {
43 print "Content-Type: text/html; charset=utf-8\n\n";
44 my @body = $tpl->template('site.html', {
45 has_error => 1,
46 error_msg => "Monitor #$mid not found.",
47 });
48 MODS::PageWrapper->new->wrapper(
49 page_title => 'Site', page_key => '',
50 body_html => join('', @body), userinfo => { display_name => 'Operator' },
51 );
52 exit;
53}
54
55# ---- KPI counts ----------------------------------------------------
56my $k_24h = $db->db_readwrite($dbh, qq~
57 SELECT COUNT(*) AS n FROM FILE_CHANGES
58 WHERE file_monitor_list_id = $mid
59 AND date_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
60~, __FILE__, __LINE__);
61my $k_7d = $db->db_readwrite($dbh, qq~
62 SELECT COUNT(*) AS n FROM FILE_CHANGES
63 WHERE file_monitor_list_id = $mid
64 AND date_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
65~, __FILE__, __LINE__);
66my $k_active = $db->db_readwrite($dbh, qq~
67 SELECT COUNT(DISTINCT file_name) AS n FROM FILE_CHANGES
68 WHERE file_monitor_list_id = $mid
69 AND status != 'deleted'
70~, __FILE__, __LINE__);
71my $k_all = $db->db_readwrite($dbh, qq~
72 SELECT COUNT(*) AS n FROM FILE_CHANGES WHERE file_monitor_list_id = $mid
73~, __FILE__, __LINE__);
74
75# ---- 90-day activity chart -----------------------------------------
76my @day_rows = $db->db_readwrite_multiple($dbh, qq~
77 SELECT DATE_FORMAT(date_time, '%Y-%m-%d') AS d, COUNT(*) AS n
78 FROM FILE_CHANGES
79 WHERE file_monitor_list_id = $mid
80 AND date_time >= DATE_SUB(NOW(), INTERVAL 90 DAY)
81 GROUP BY d
82~, __FILE__, __LINE__);
83my %by_day; $by_day{$_->{d}} = $_->{n} for @day_rows;
84my (@days, @day_keys, @file_series, @empty_schema);
85my $now = time;
86for (my $i = 89; $i >= 0; $i--) {
87 my $t = $now - $i * 86400;
88 my $key = POSIX::strftime('%Y-%m-%d', localtime($t));
89 my $lbl = POSIX::strftime('%b %d', localtime($t));
90 push @days, $lbl; push @day_keys, $key;
91 push @file_series, ($by_day{$key} || 0);
92 push @empty_schema, 0;
93}
94my $timeline_svg = MODS::Charts::stacked_bars(
95 days => \@days, day_keys => \@day_keys,
96 series => { file => \@file_series, schema => \@empty_schema },
97 width => 900, height => 150,
98 link_file => '/file_changes.cgi',
99 link_schema => '/schema_changes.cgi',
100);
101
102# ---- Recent changes for THIS site (last 40) ------------------------
103my @recent = $db->db_readwrite_multiple($dbh, qq~
104 SELECT fc.file_changes_id AS id, fc.file_name, fc.status, fc.blob_sha,
105 fc.is_ts_only,
106 UNIX_TIMESTAMP(fc.date_time) AS ts_epoch,
107 fc.date_time
108 FROM FILE_CHANGES fc
109 WHERE fc.file_monitor_list_id = $mid
110 ORDER BY fc.file_changes_id DESC
111 LIMIT 40
112~, __FILE__, __LINE__);
113foreach my $r (@recent) {
114 $r->{status_pill} = $r->{status} eq 'added' ? 'pill-ok'
115 : $r->{status} eq 'modified' ? 'pill-warn'
116 : $r->{status} eq 'deleted' ? 'pill-bad'
117 : 'pill-mute';
118 $r->{file_short} = length($r->{file_name} // '') > 60
119 ? '...' . substr($r->{file_name}, -57) : $r->{file_name};
120 $r->{blob_short} = $r->{blob_sha} ? substr($r->{blob_sha}, 0, 8) : '-';
121 $r->{diff_url} = "/diff.cgi?kind=file&id=$r->{id}";
122 $r->{restore_url} = "/restore.cgi?id=$r->{id}";
123 $r->{can_restore} = ($r->{blob_sha} && $r->{status} ne 'deleted') ? 1 : 0;
124 $r->{ts_epoch} //= 0;
125}
126
127# ---- Change-frequency for THIS site (top 8) -------------------------
128my @hotspots = $db->db_readwrite_multiple($dbh, qq~
129 SELECT file_name,
130 COUNT(*) AS ct,
131 MAX(file_changes_id) AS latest_id,
132 MAX(UNIX_TIMESTAMP(date_time)) AS latest_epoch
133 FROM FILE_CHANGES
134 WHERE file_monitor_list_id = $mid
135 AND date_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
136 GROUP BY file_name
137 ORDER BY ct DESC LIMIT 8
138~, __FILE__, __LINE__);
139foreach my $h (@hotspots) {
140 $h->{file_short} = length($h->{file_name} // '') > 55
141 ? '...' . substr($h->{file_name}, -52) : $h->{file_name};
142 $h->{diff_url} = "/diff.cgi?kind=file&id=" . ($h->{latest_id} || 0);
143}
144
145# ---- Named releases scoped to this site ----------------------------
146my @releases = $db->db_readwrite_multiple($dbh, qq~
147 SELECT nr.named_release_id AS id, nr.name, nr.version_label, nr.tier,
148 nr.description,
149 UNIX_TIMESTAMP(nr.released_at) AS released_epoch,
150 DATE_FORMAT(nr.released_at, '%b %d, %Y') AS released_h
151 FROM NAMED_RELEASES nr
152 WHERE nr.scope_monitor_id = $mid
153 OR nr.scope_server_id = $monitor->{server_id}
154 OR nr.scope_monitor_id IS NULL
155 ORDER BY (nr.tier = 'stable') DESC, (nr.tier = 'working') DESC, nr.named_release_id DESC
156 LIMIT 10
157~, __FILE__, __LINE__);
158foreach my $r (@releases) {
159 $r->{tier} //= 'draft';
160 $r->{tier_pill} = $r->{tier} eq 'stable' ? 'pill-ok'
161 : $r->{tier} eq 'working' ? 'pill-info'
162 : 'pill-mute';
163 $r->{label} = $r->{version_label} || $r->{name};
164 $r->{restore_url} = "/restore_release.cgi?release_id=$r->{id}";
165 $r->{released_epoch} //= 0;
166}
167
168$db->db_disconnect($dbh);
169
170my $tvars = {
171 mid => $mid,
172 scan_name => $monitor->{scan_name},
173 scan_path => $monitor->{scan_path},
174 server_name => $monitor->{server_name} // 'local',
175 server_kind => $monitor->{server_kind} // 'local',
176 ssh_host => $monitor->{ssh_host} // '',
177 ssh_user => $monitor->{ssh_user} // '',
178 retention_h => defined($monitor->{retention_days}) && $monitor->{retention_days}
179 ? "$monitor->{retention_days} days"
180 : '(global default)',
181 kpi_24h => ($k_24h && $k_24h->{n}) || 0,
182 kpi_7d => ($k_7d && $k_7d->{n}) || 0,
183 kpi_active => ($k_active && $k_active->{n}) || 0,
184 kpi_all => ($k_all && $k_all->{n}) || 0,
185 timeline_svg => $timeline_svg,
186 recent => \@recent,
187 has_recent => scalar(@recent) ? 1 : 0,
188 hotspots => \@hotspots,
189 has_hotspots => scalar(@hotspots) ? 1 : 0,
190 releases => \@releases,
191 has_releases => scalar(@releases) ? 1 : 0,
192 changes_url => "/file_changes.cgi?monitor=$mid",
193 pin_url => "/pin_hotspots.cgi?monitor=$mid",
194 export_url => "/export.cgi?server_id=$monitor->{server_id}",
195};
196
197print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
198my @body = $tpl->template('site.html', $tvars);
199MODS::PageWrapper->new->wrapper(
200 page_title => "Site: " . ($monitor->{scan_name} // '?'),
201 page_key => '',
202 active_mid => $mid,
203 body_html => join('', @body),
204 userinfo => { display_name => 'Operator' },
205);