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

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

modified on local at 2026-07-12 23:13:01

Added
+19
lines
Removed
-6
lines
Context
221
unchanged
Blobs
from 465d507a6121
to 2660314723c8
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 -- /named_releases.cgi
44#
55# List + create named releases. Each release is a bookmark that pins
66# every blob referenced by FILE_CHANGES rows in a chosen scope. A
77# release can be portfolio-wide (scope_monitor_id NULL) or scoped to
88# a single site (scope_monitor_id set). Pinned blobs are exempt from
99# auto-purge.
1010#======================================================================
1111use strict;
1212use warnings;
1313use CGI ();
1414use MODS::Config; use MODS::DBConnect; use MODS::Template; use MODS::PageWrapper;
1515use MODS::Webhook;
1616use JSON::PP ();
1717my $cgi = CGI->new; my $db = MODS::DBConnect->new; my $tpl = MODS::Template->new;
1818$|=1;
1919
2020my $dbh = $db->db_connect or die "DB connect failed\n";
2121
2222# ---- POST: create ---------------------------------------------------
2323if (($ENV{REQUEST_METHOD} || '') eq 'POST' && $cgi->param('save')) {
2424 my $name = $cgi->param('name') || '';
2525 my $desc = $cgi->param('description') || '';
2626 my $by = $cgi->param('released_by') || 'Operator';
2727 my $scope_mid = int($cgi->param('scope_monitor_id') || 0);
2828 my $scope_sid = int($cgi->param('scope_server_id') || 0);
2929 my $dkind = $cgi->param('alert_delivery_kind') || 'none';
3030 $dkind = 'none' unless $dkind =~ /^(none|generic_webhook|slack|discord)$/;
3131 my $durl = $cgi->param('alert_delivery_url') || '';
3232 my $tier = $cgi->param('tier') || 'draft';
3333 $tier = 'draft' unless $tier =~ /^(draft|working|stable)$/;
3434 my $vlabel = $cgi->param('version_label') || '';
3535 my $apdays = int($cgi->param('auto_promote_stable_days') || 0);
3636 $apdays = 0 if $apdays < 0 || $apdays > 365;
37 my $ann = $cgi->param('annotation') // '';
38 $ann = substr($ann, 0, 4000) if length($ann) > 4000;
3739
3840 if (length $name) {
3941 my $q_name = $dbh->quote($name);
4042 my $q_desc = $dbh->quote($desc);
4143 my $q_by = $dbh->quote($by);
4244 my $q_dk = $dbh->quote($dkind);
4345 my $q_du = $dbh->quote($durl);
4446 my $q_tier = $dbh->quote($tier);
4547 my $q_vl = $dbh->quote($vlabel);
4648 my $mid_expr = $scope_mid ? $scope_mid : 'NULL';
4749 my $sid_expr = $scope_sid ? $scope_sid : 'NULL';
48 my $has_ap = _has_col($dbh, 'NAMED_RELEASES', 'auto_promote_stable_days');
49 my $cols = "(name, description, released_at, released_by, is_locked, created_at,
50 my $has_ap = _has_col($dbh, 'NAMED_RELEASES', 'auto_promote_stable_days');
51 my $has_ann = _has_col($dbh, 'NAMED_RELEASES', 'annotation');
52 my $q_ann = $dbh->quote($ann);
53 my $cols = "(name, description, released_at, released_by, is_locked, created_at,
5054 scope_monitor_id, scope_server_id,
5155 alert_delivery_kind, alert_delivery_url,
52 tier, version_label" . ($has_ap ? ', auto_promote_stable_days' : '') . ")";
53 my $vals = "($q_name, $q_desc, NOW(), $q_by, 1, NOW(),
56 tier, version_label"
57 . ($has_ap ? ', auto_promote_stable_days' : '')
58 . ($has_ann ? ', annotation' : '') . ")";
59 my $vals = "($q_name, $q_desc, NOW(), $q_by, 1, NOW(),
5460 $mid_expr, $sid_expr,
5561 $q_dk, $q_du,
56 $q_tier, $q_vl" . ($has_ap ? ", $apdays" : '') . ")";
62 $q_tier, $q_vl"
63 . ($has_ap ? ", $apdays" : '')
64 . ($has_ann ? ", $q_ann" : '') . ")";
5765 $db->db_readwrite($dbh, "INSERT INTO NAMED_RELEASES $cols VALUES $vals",
5866 __FILE__, __LINE__);
5967 # Extra scopes for cross-scope releases (Wave 5 F5): multi-select
6068 # scope_monitor_ids[] additive
6169 my $new_rel = $db->db_readwrite($dbh, "SELECT LAST_INSERT_ID() AS id", __FILE__, __LINE__);
6270 my $rid = $new_rel && $new_rel->{id};
6371 if ($rid) {
6472 foreach my $extra ($cgi->param('extra_monitor_ids')) {
6573 my $eid = int($extra || 0);
6674 next unless $eid && $eid != $scope_mid;
6775 $db->db_readwrite($dbh, qq~
6876 INSERT INTO NAMED_RELEASE_SCOPES
6977 (named_release_id, file_monitor_list_id)
7078 VALUES ($rid, $eid)
7179 ~, __FILE__, __LINE__);
7280 }
7381 }
7482 }
7583 print "Status: 302 Found\nLocation: /named_releases.cgi\n\n"; exit;
7684}
7785
7886# ---- AJAX: test alert delivery ------------------------------------
7987if ($cgi->param('op') && $cgi->param('op') eq 'test_alert') {
8088 my $rid = int($cgi->param('release_id') || 0);
8189 print "Content-Type: application/json\nCache-Control: no-cache\n\n";
8290 my $r = $db->db_readwrite($dbh, qq~
8391 SELECT name, alert_delivery_kind, alert_delivery_url
8492 FROM NAMED_RELEASES WHERE named_release_id = $rid LIMIT 1
8593 ~, __FILE__, __LINE__);
8694 unless ($r && $r->{alert_delivery_url}) {
8795 print JSON::PP::encode_json({ ok => \0, message => 'no delivery URL configured' });
8896 exit;
8997 }
9098 my $summary = "TEST alert from DriftSense -- release '$r->{name}' -- ignore me";
9199 my $body = ($r->{alert_delivery_kind} eq 'slack') ? {
92100 text => "*DriftSense TEST*: $summary",
93101 attachments => [{ color => '#f59e0b', text => 'This is a synthetic drift payload. No files actually drifted.' }],
94102 } : ($r->{alert_delivery_kind} eq 'discord') ? {
95103 content => "**DriftSense TEST**: $summary",
96104 } : {
97105 source => 'drift_sense', alert_type => 'test',
98106 release_name => $r->{name}, summary => $summary,
99107 };
100108 my ($code, $rbody, $err) = MODS::Webhook::post_json(
101109 $r->{alert_delivery_url}, $body,
102110 timeout => 8,
103111 user_agent => 'DriftSense/1.0 (test-alert)',
104112 );
105113 my $ok = (!$err && $code >= 200 && $code < 400) ? \1 : \0;
106114 print JSON::PP::encode_json({
107115 ok => $ok, http_code => $code, message => $err ? $err : "HTTP $code",
108116 });
109117 exit;
110118}
111119
112120# ---- POST: promote / demote tier -----------------------------------
113121if (($ENV{REQUEST_METHOD} || '') eq 'POST' && $cgi->param('promote')) {
114122 my $rid = int($cgi->param('release_id') || 0);
115123 my $tier = $cgi->param('to_tier') || 'stable';
116124 $tier = 'draft' unless $tier =~ /^(draft|working|stable)$/;
117125 my $q_tier = $dbh->quote($tier);
118126 $db->db_readwrite($dbh,
119127 "UPDATE NAMED_RELEASES SET tier = $q_tier WHERE named_release_id = $rid",
120128 __FILE__, __LINE__);
121129 print "Status: 302 Found\nLocation: /named_releases.cgi\n\n"; exit;
122130}
123131
124132print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
125133
126134# ---- Existing releases + count how many blobs each pins -------------
127135my $has_ap = _has_col($dbh, 'NAMED_RELEASES', 'auto_promote_stable_days');
128my $ap_expr = $has_ap ? 'nr.auto_promote_stable_days' : '0';
136my $has_ann = _has_col($dbh, 'NAMED_RELEASES', 'annotation');
137my $ap_expr = $has_ap ? 'nr.auto_promote_stable_days' : '0';
138my $ann_expr = $has_ann ? 'nr.annotation' : 'NULL';
129139my @rows = $db->db_readwrite_multiple($dbh, qq~
130140 SELECT nr.named_release_id AS id, nr.name, nr.description, nr.tier, nr.version_label,
131141 DATE_FORMAT(nr.released_at, '%b %d, %Y %H:%i') AS released_h,
132142 UNIX_TIMESTAMP(nr.released_at) AS released_epoch,
133143 nr.released_by, nr.is_locked,
134144 nr.scope_monitor_id, nr.scope_server_id,
135145 $ap_expr AS auto_promote_days,
146 $ann_expr AS annotation,
136147 TIMESTAMPDIFF(DAY, nr.released_at, NOW()) AS age_days,
137148 m.scan_name, s.server_name
138149 FROM NAMED_RELEASES nr
139150 LEFT JOIN FILE_MONITOR_SETTINGS m ON m.file_monitor_list_id = nr.scope_monitor_id
140151 LEFT JOIN SERVERS s ON s.server_id = nr.scope_server_id
141152 ORDER BY (nr.tier = 'stable') DESC,
142153 (nr.tier = 'working') DESC,
143154 nr.released_at DESC
144155 LIMIT 100
145156~, __FILE__, __LINE__);
146157foreach my $r (@rows) {
147158 $r->{lock_pill} = $r->{is_locked} ? 'pill-ok' : 'pill-mute';
148159 $r->{lock_lbl} = $r->{is_locked} ? 'PROTECTED' : 'UNLOCKED';
149160 $r->{released_epoch} //= 0;
150161 $r->{auto_promote_days} //= 0;
151162 $r->{age_days} //= 0;
152163 $r->{has_auto_promote} = ($r->{auto_promote_days} > 0) ? 1 : 0;
153164 $r->{ap_ready} = ($r->{has_auto_promote} && $r->{tier} eq 'working'
154165 && $r->{age_days} >= $r->{auto_promote_days}) ? 1 : 0;
155166 $r->{ap_days_left} = $r->{has_auto_promote}
156167 ? ($r->{auto_promote_days} - $r->{age_days})
157168 : 0;
169 $r->{annotation} //= '';
170 $r->{has_annotation} = length($r->{annotation}) ? 1 : 0;
158171 $r->{tier} //= 'draft';
159172 $r->{tier_pill} = $r->{tier} eq 'stable' ? 'pill-ok'
160173 : $r->{tier} eq 'working' ? 'pill-info'
161174 : 'pill-mute';
162175 $r->{tier_lbl} = uc($r->{tier});
163176 $r->{display_label} = $r->{version_label} || $r->{name};
164177 $r->{is_stable} = $r->{tier} eq 'stable' ? 1 : 0;
165178 $r->{is_draft} = $r->{tier} eq 'draft' ? 1 : 0;
166179 $r->{is_working} = $r->{tier} eq 'working'? 1 : 0;
167180 $r->{restore_url} = "/restore_release.cgi?release_id=$r->{id}";
168181 if ($r->{scope_monitor_id}) {
169182 $r->{scope_pill} = 'pill-info';
170183 $r->{scope_lbl} = 'per-site';
171184 $r->{scope_h} = $r->{scan_name} || "monitor #$r->{scope_monitor_id}";
172185 } elsif ($r->{scope_server_id}) {
173186 $r->{scope_pill} = 'pill-info';
174187 $r->{scope_lbl} = 'per-server';
175188 $r->{scope_h} = $r->{server_name} || "server #$r->{scope_server_id}";
176189 } else {
177190 $r->{scope_pill} = 'pill-mute';
178191 $r->{scope_lbl} = 'portfolio';
179192 $r->{scope_h} = 'All monitored sites';
180193 }
181194}
182195
183196# ---- All monitors + servers for scope picker ------------------------
184197my @monitors = $db->db_readwrite_multiple($dbh, q~
185198 SELECT m.file_monitor_list_id AS mid, m.scan_name, s.server_name
186199 FROM FILE_MONITOR_SETTINGS m
187200 LEFT JOIN SERVERS s ON s.server_id = m.server_id
188201 WHERE m.status = 1
189202 ORDER BY s.server_id ASC, m.file_monitor_list_id ASC
190203~, __FILE__, __LINE__);
191204foreach my $m (@monitors) {
192205 $m->{label} = ($m->{scan_name} // '?') . ' (on ' . ($m->{server_name} // '?') . ')';
193206}
194207
195208my $total = scalar @rows;
196209$db->db_disconnect($dbh);
197210
198211my $tvars = {
199212 rows => \@rows,
200213 has_rows => $total ? 1 : 0,
201214 total => $total,
202215 monitors => \@monitors,
203216 has_monitors=> scalar(@monitors) ? 1 : 0,
204217};
205218my @body = $tpl->template('named_releases.html', $tvars);
206219MODS::PageWrapper->new->wrapper(
207220 page_title => 'Named releases', page_key => 'named_releases',
208221 body_html => join('', @body), userinfo => { display_name => 'Operator' },
209222);
210223
211224#---------------------------------------------------------------------
212225# _has_col($dbh, $table, $col) -- schema probe cached per-process
213226#---------------------------------------------------------------------
214227my %_COL_CACHE;
215228sub _has_col {
216229 my ($dbh, $table, $col) = @_;
217230 my $k = "$table/$col";
218231 return $_COL_CACHE{$k} if exists $_COL_CACHE{$k};
219232 my $q_tab = $dbh->quote($table);
220233 my $q_col = $dbh->quote($col);
221234 my $r = $db->db_readwrite($dbh, qq~
222235 SELECT 1 AS ok FROM INFORMATION_SCHEMA.COLUMNS
223236 WHERE TABLE_SCHEMA = DATABASE()
224237 AND TABLE_NAME = $q_tab AND COLUMN_NAME = $q_col
225238 ~, __FILE__, __LINE__);
226239 return $_COL_CACHE{$k} = ($r && $r->{ok}) ? 1 : 0;
227240}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help