Diff -- /var/www/vhosts/3dshawn.com/admin.3dshawn.com/auto_ack.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/admin.3dshawn.com/auto_ack.cgi

added on local at 2026-07-10 14:13:09

Added
+0
lines
Removed
-0
lines
Context
446
unchanged
Blobs
from 8834775a8592
to 8834775a8592
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# Meta-Admin -- /auto_ack.cgi
44#
55# Auto-acknowledge scheduler control panel.
66# /auto_ack.cgi -> list rules + kill switch + queue + history
77# /auto_ack.cgi?edit=NEW|ID -> rule form
88# POST save=1 -> insert/update rule
99# POST toggle=ID -> flip rule active
1010# POST kill_switch=on|off -> flip global_enabled setting
1111#======================================================================
1212use strict;
1313use warnings;
1414use lib '/var/www/vhosts/3dshawn.com/admin.3dshawn.com';
1515use CGI ();
1616use MODS::Login;
1717use MODS::MetaAdmin::Config;
1818use MODS::MetaAdmin::Wrapper;
1919use MODS::MetaAdmin::AutoAck;
2020use MODS::MetaAdmin::CannedReplies;
2121use MODS::MetaAdmin::SupportInbox;
2222
2323my $auth = MODS::Login->new;
2424my $u = $auth->login_verify;
2525unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
2626
2727my $cgi = CGI->new;
2828my $aa = MODS::MetaAdmin::AutoAck->new;
2929my $cr = MODS::MetaAdmin::CannedReplies->new;
3030
3131sub _esc { my $s=shift; $s//=''; $s=~s/&/&amp;/g; $s=~s/</&lt;/g; $s=~s/>/&gt;/g; $s=~s/"/&quot;/g; return $s; }
3232
3333sub _rel_age {
3434 my $ts = shift;
3535 return '-' unless $ts && $ts =~ /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})$/;
3636 require Time::Local;
3737 my $epoch = eval { Time::Local::timelocal($6,$5,$4,$3,$2-1,$1-1900) } || 0;
3838 return '-' unless $epoch;
3939 my $delta = time - $epoch;
4040 my $sign = $delta < 0 ? 'in ' : '';
4141 my $abs = abs($delta);
4242 my $suffix= $delta < 0 ? '' : ' ago';
4343 return $sign . 'just now' . $suffix if $abs < 60;
4444 return $sign . int($abs/60) . ' m'.$suffix if $abs < 3600;
4545 return $sign . int($abs/3600) . ' h'.$suffix if $abs < 86400;
4646 return $sign . int($abs/86400).' d'.$suffix;
4747}
4848
4949my $method = $ENV{REQUEST_METHOD} || 'GET';
5050
5151# --- POST: kill switch flip
5252if ($method eq 'POST' && defined $cgi->param('kill_switch')) {
5353 my $val = $cgi->param('kill_switch');
5454 $aa->set_global_enabled($val eq 'on' ? 1 : 0);
5555 print "Status: 302 Found\nLocation: /auto_ack.cgi?ks=".($val eq 'on'?'on':'off')."\n\n";
5656 exit;
5757}
5858# --- POST: save rule
5959if ($method eq 'POST' && $cgi->param('save')) {
6060 my $id = int($cgi->param('id') || 0);
6161 my $rid = $aa->save_rule(
6262 id => $id,
6363 name => scalar($cgi->param('name')),
6464 description => scalar($cgi->param('description')),
6565 template_id => scalar($cgi->param('template_id')),
6666 site_scope => scalar($cgi->param('site_scope')) || 'all',
6767 min_delay_seconds => scalar($cgi->param('min_delay_seconds')),
6868 max_delay_seconds => scalar($cgi->param('max_delay_seconds')),
6969 match_new_only => $cgi->param('match_new_only') ? 1 : 0,
7070 match_no_admin_reply => $cgi->param('match_no_admin_reply') ? 1 : 0,
7171 match_customer_other_threads_min => scalar($cgi->param('match_customer_other_threads_min')),
7272 match_customer_other_threads_max => scalar($cgi->param('match_customer_other_threads_max')),
7373 business_hours_only => $cgi->param('business_hours_only') ? 1 : 0,
7474 business_hours_start => scalar($cgi->param('business_hours_start')),
7575 business_hours_end => scalar($cgi->param('business_hours_end')),
7676 priority => scalar($cgi->param('priority')),
7777 is_active => $cgi->param('is_active') ? 1 : 0,
7878 );
7979 print "Status: 302 Found\nLocation: /auto_ack.cgi?saved=$rid\n\n";
8080 exit;
8181}
8282# --- POST: toggle rule
8383if ($method eq 'POST' && $cgi->param('toggle')) {
8484 my $id = int($cgi->param('toggle'));
8585 $aa->toggle_rule($id);
8686 print "Status: 302 Found\nLocation: /auto_ack.cgi?toggled=$id\n\n";
8787 exit;
8888}
8989
9090# --- Edit form
9191my $edit = $cgi->param('edit') || '';
9292if ($edit) {
9393 my $r = ($edit eq 'NEW') ? {} : $aa->get_rule($edit);
9494 $r ||= {};
9595 my %d = (
9696 id => $r->{id} // '', name => $r->{name} // '',
9797 description => $r->{description} // '',
9898 template_id => $r->{template_id} // '',
9999 site_scope => $r->{site_scope} // 'all',
100100 min_delay_seconds => $r->{min_delay_seconds} // 120,
101101 max_delay_seconds => $r->{max_delay_seconds} // 480,
102102 match_customer_other_threads_min => $r->{match_customer_other_threads_min} // 0,
103103 match_customer_other_threads_max => $r->{match_customer_other_threads_max} // 999,
104104 business_hours_start => $r->{business_hours_start} // 8,
105105 business_hours_end => $r->{business_hours_end} // 23,
106106 priority => $r->{priority} // 100,
107107 );
108108 my $c_new = (defined $r->{match_new_only} && !$r->{match_new_only}) ? '' : 'checked';
109109 my $c_noadm = (defined $r->{match_no_admin_reply} && !$r->{match_no_admin_reply}) ? '' : 'checked';
110110 my $c_bh = ($r->{business_hours_only}) ? 'checked' : '';
111111 my $c_active = (defined $r->{is_active} && !$r->{is_active}) ? '' : 'checked';
112112
113113 my $templates = $cr->list;
114114 my $tpl_opts = '';
115115 foreach my $t (@$templates) {
116116 my $sel = ($t->{id} == ($d{template_id} || 0)) ? ' selected' : '';
117117 my $name = _esc($t->{name}); my $tid = _esc($t->{id});
118118 my $inactive_note = $t->{is_active} ? '' : ' (inactive)';
119119 $tpl_opts .= qq~<option value="$tid"$sel>#$tid: $name$inactive_note</option>~;
120120 }
121121
122122 my $supported = MODS::MetaAdmin::SupportInbox->new->supported_slugs;
123123 my $scope_hint = 'all, or comma-list of: ' . join(', ', @$supported);
124124
125125 for my $k (keys %d) { $d{$k} = _esc($d{$k}) }
126126 my $title = ($edit eq 'NEW') ? 'New auto-ack rule' : "Edit rule #$d{id}";
127127
128128 my $body = qq~
129129 <div class="page-head"><div class="left">
130130 <a href="/auto_ack.cgi" class="page-eyebrow" style="text-decoration:none"><span class="dot"></span> &larr; Back to auto-ack</a>
131131 <h1 class="page-title">$title</h1>
132132 <p class="page-subtitle">Rules select which threads get auto-acked. Every rule points at one canned template. Match conditions AND together -- ALL must be true for a rule to fire. Multiple rules can be active; they fire independently.</p>
133133 </div></div>
134134 <style>
135135 .form-grid { display:grid; gap:14px; max-width:900px; }
136136 .form-grid label.field { display:block; font-size:11px; letter-spacing:1.4px; text-transform:uppercase; color:var(--col-text-dim); font-weight:700; margin-bottom:6px; }
137137 .form-grid input[type=text], .form-grid input[type=number], .form-grid textarea, .form-grid select {
138138 width:100%; box-sizing:border-box; background:var(--col-surface); color:var(--col-text);
139139 border:1px solid var(--col-border); border-radius:10px; padding:9px 12px;
140140 font-family:inherit; font-size:13px; outline:none;
141141 }
142142 .form-grid select { cursor:pointer; }
143143 .form-grid input:focus, .form-grid textarea:focus, .form-grid select:focus { border-color:var(--col-brand, var(--col-primary, #b53048)); }
144144 .form-row-inline { display:flex; gap:14px; align-items:end; }
145145 .form-row-inline > * { flex:1; min-width:0; }
146146 .form-checkbox { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--col-text); }
147147 .btn-primary { background:var(--col-brand, var(--col-primary, #b53048)); color:#fff; border:none; padding:10px 20px; border-radius:999px; font-weight:600; cursor:pointer; font-size:13px; }
148148 .btn-secondary { background:transparent; color:var(--col-text-muted); border:1px solid var(--col-border); padding:10px 18px; border-radius:999px; font-size:13px; text-decoration:none; }
149149 .section-title { font-size:11px; letter-spacing:1.4px; text-transform:uppercase; color:var(--col-text-muted); font-weight:700; margin:6px 0 4px; }
150150 hr.form-sep { border:none; border-top:1px solid var(--col-border); margin:4px 0; }
151151 </style>
152152 <form method="post" action="/auto_ack.cgi" class="form-grid">
153153 <input type="hidden" name="save" value="1">
154154 <input type="hidden" name="id" value="$d{id}">
155155
156156 <div>
157157 <label class="field">Rule name</label>
158158 <input type="text" name="name" value="$d{name}" required maxlength="80" placeholder="e.g. First-touch ack on new threads">
159159 </div>
160160 <div>
161161 <label class="field">Description</label>
162162 <input type="text" name="description" value="$d{description}" maxlength="255" placeholder="Optional notes for future you">
163163 </div>
164164 <div>
165165 <label class="field">Template to fire</label>
166166 <select name="template_id" required>$tpl_opts</select>
167167 </div>
168168 <div>
169169 <label class="field">Site scope</label>
170170 <input type="text" name="site_scope" value="$d{site_scope}" placeholder="$scope_hint">
171171 </div>
172172
173173 <div class="section-title">Delay</div>
174174 <div class="form-row-inline">
175175 <div><label class="field">Min delay (seconds)</label><input type="number" name="min_delay_seconds" value="$d{min_delay_seconds}" min="10" max="7200"></div>
176176 <div><label class="field">Max delay (seconds)</label><input type="number" name="max_delay_seconds" value="$d{max_delay_seconds}" min="10" max="7200"></div>
177177 </div>
178178
179179 <hr class="form-sep">
180180 <div class="section-title">Match conditions (all must be true)</div>
181181 <div class="form-checkbox"><input type="checkbox" name="match_new_only" $c_new> Only fire on threads created in the last 72h</div>
182182 <div class="form-checkbox"><input type="checkbox" name="match_no_admin_reply" $c_noadm> Only fire if no admin has replied yet (double-check at fire time)</div>
183183 <div class="form-row-inline">
184184 <div><label class="field">Customer's OTHER open threads &ge;</label><input type="number" name="match_customer_other_threads_min" value="$d{match_customer_other_threads_min}" min="0" max="999"></div>
185185 <div><label class="field">Customer's OTHER open threads &le;</label><input type="number" name="match_customer_other_threads_max" value="$d{match_customer_other_threads_max}" min="0" max="999"></div>
186186 </div>
187187 <p style="color:var(--col-text-dim);font-size:11.5px;margin:0">Tip: set min=0 max=0 for "this is their only open thread". Set min=1 for a "second-touch" template that references their other open items.</p>
188188
189189 <hr class="form-sep">
190190 <div class="section-title">Business hours (Mountain Time)</div>
191191 <div class="form-checkbox"><input type="checkbox" name="business_hours_only" $c_bh> Only fire during these hours</div>
192192 <div class="form-row-inline">
193193 <div><label class="field">Start hour (0-23)</label><input type="number" name="business_hours_start" value="$d{business_hours_start}" min="0" max="23"></div>
194194 <div><label class="field">End hour (0-23)</label><input type="number" name="business_hours_end" value="$d{business_hours_end}" min="0" max="23"></div>
195195 </div>
196196
197197 <hr class="form-sep">
198198 <div class="form-row-inline">
199199 <div><label class="field">Priority</label><input type="number" name="priority" value="$d{priority}" min="0" max="9999"></div>
200200 <div><label class="field">Active</label><div class="form-checkbox"><input type="checkbox" name="is_active" $c_active> Rule participates in the scheduler</div></div>
201201 </div>
202202
203203 <div style="display:flex;align-items:center;gap:12px;margin-top:8px">
204204 <button type="submit" class="btn-primary">Save rule</button>
205205 <a href="/auto_ack.cgi" class="btn-secondary">Cancel</a>
206206 </div>
207207 </form>
208208 ~;
209209
210210 MODS::MetaAdmin::Wrapper->new->render(
211211 title => $title, page_key => 'auto_ack', body => $body, userinfo => $u,
212212 );
213213 exit;
214214}
215215
216216#---------------------------------------------------------------------
217217# List view
218218#---------------------------------------------------------------------
219219my $enabled = $aa->is_globally_enabled;
220220my $rules = $aa->list_rules;
221221my $queue = $aa->list_queued(limit => 20);
222222my $history = $aa->list_history(limit => 20);
223223
224224my $body = q~
225225<div class="page-head"><div class="left">
226226 <span class="page-eyebrow"><span class="dot"></span> Scheduler</span>
227227 <h1 class="page-title">Auto-Acknowledge</h1>
228228 <p class="page-subtitle">Rules watch for new support threads and auto-fire a canned acknowledgment after a randomized delay. Every reply goes out through each site's own send endpoint (same as manual). Templates are strictly hand-raises -- never answers -- so a customer knows we saw them while the real work happens on your schedule.</p>
229229</div></div>
230230~;
231231
232232# Global kill switch tile + status
233233my ($ks_state, $ks_class, $ks_next, $ks_btn_label) = $enabled
234234 ? ('ENABLED', 'kill-on', 'off', 'Turn off')
235235 : ('DISABLED', 'kill-off', 'on', 'Turn on');
236236my $ks_note = $enabled
237237 ? 'Discovery + firing are active. Cron ticks every minute.'
238238 : 'Nothing will fire. Rules are still editable, queue insertions are paused.';
239239
240240my $saved = $cgi->param('saved') || '';
241241my $toggled = $cgi->param('toggled') || '';
242242my $ks = $cgi->param('ks') || '';
243243if ($saved) {
244244 $body .= qq~<div class="banner banner-ok" style="margin:10px 0;padding:10px 14px;border-radius:10px;background:rgba(34,197,94,.10);border:1px solid rgba(34,197,94,.35);color:#a7f3d0;font-size:13px">Saved rule #$saved.</div>~;
245245}
246246if ($toggled) {
247247 $body .= qq~<div class="banner" style="margin:10px 0;padding:10px 14px;border-radius:10px;background:rgba(6,182,212,.10);border:1px solid rgba(6,182,212,.35);color:#7dd3fc;font-size:13px">Toggled rule #$toggled.</div>~;
248248}
249249if ($ks eq 'on') {
250250 $body .= q~<div class="banner" style="margin:10px 0;padding:10px 14px;border-radius:10px;background:rgba(6,182,212,.10);border:1px solid rgba(6,182,212,.35);color:#7dd3fc;font-size:13px">Auto-ack turned ON. First tick will pick up eligible threads within a minute.</div>~;
251251}
252252if ($ks eq 'off') {
253253 $body .= q~<div class="banner" style="margin:10px 0;padding:10px 14px;border-radius:10px;background:rgba(180,150,50,.10);border:1px solid rgba(180,150,50,.35);color:#facc15;font-size:13px">Auto-ack turned OFF. Nothing new will queue or fire.</div>~;
254254}
255255
256256$body .= qq~
257257<style>
258258.kill-tile { position:relative; padding:28px 24px; border-radius:14px; overflow:hidden; }
259259.kill-tile.kill-off { background:linear-gradient(135deg, #0f0f0f, #1a1414); border:1px solid var(--col-border); }
260260.kill-tile.kill-on { background:linear-gradient(135deg, #451a1f, #1c0f11); border:1px solid rgba(181,48,72,.5); box-shadow:0 0 32px rgba(181,48,72,.15) inset; }
261261.kill-tile h2 { margin:0; font-size:24px; letter-spacing:.5px; color:var(--col-text); }
262262.kill-tile .state { font-size:11px; letter-spacing:1.4px; text-transform:uppercase; font-weight:700; }
263263.kill-tile.kill-on .state { color:#fca5a5; }
264264.kill-tile.kill-off .state { color:var(--col-text-dim); }
265265.kill-tile p { color:var(--col-text-2); margin:6px 0 14px; font-size:13px; }
266266.kill-btn { background:transparent; color:var(--col-text); border:1px solid var(--col-border); padding:8px 18px; border-radius:999px; font-weight:600; cursor:pointer; font-size:12.5px; }
267267.kill-tile.kill-off .kill-btn:hover { background:rgba(34,197,94,.10); border-color:rgba(34,197,94,.5); color:#a7f3d0; }
268268.kill-tile.kill-on .kill-btn:hover { background:rgba(255,255,255,.05); }
269269</style>
270270<div class="dash-grid" style="grid-template-columns: 1.6fr 1fr; margin-bottom:16px">
271271 <div class="kill-tile $ks_class">
272272 <div class="state">$ks_state</div>
273273 <h2>Global auto-ack</h2>
274274 <p>$ks_note</p>
275275 <form method="post" action="/auto_ack.cgi" style="display:inline">
276276 <input type="hidden" name="kill_switch" value="$ks_next">
277277 <button type="submit" class="kill-btn">$ks_btn_label</button>
278278 </form>
279279 </div>
280280 <div class="module glow-cyan">
281281 <div class="module-head"><div class="left">
282282 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
283283 <div class="module-title">Cron</div>
284284 <span class="module-subtitle">Ticks every minute from <code>/etc/cron.d/meta_admin_auto_ack</code>. Log at <code>/var/www/vhosts/3dshawn.com/admin.3dshawn.com/auto_ack.log</code>.</span>
285285 </div></div>
286286 <div class="module-body kpi kpi-cyan">
287287 <div class="num">60s</div><div class="lbl">tick interval</div>
288288 <div class="sub">safe by default -- discover + fire run only when global is ON</div>
289289 </div>
290290 </div>
291291</div>
292292~;
293293
294294# Rules table
295295my $newbtn = '<a class="btn-new" href="/auto_ack.cgi?edit=NEW" style="background:var(--col-brand, var(--col-primary, #b53048));color:#fff;border:none;padding:8px 16px;border-radius:999px;font-weight:600;font-size:12px;text-decoration:none;display:inline-flex;align-items:center;gap:6px">+ New rule</a>';
296296my $rules_html = '';
297297if (!@$rules) {
298298 $rules_html = q~<div style="padding:36px 14px;text-align:center;color:var(--col-text-dim);font-size:13px">No rules yet. Create one to teach the scheduler what to look for.</div>~;
299299} else {
300300 $rules_html .= q~
301301 <style>
302302 .ar-list { display:grid; row-gap:2px; font-size:13px; }
303303 .ar-row { display:grid; grid-template-columns: 50px 1.6fr 1.4fr 1.2fr 100px 100px; gap:14px; align-items:center; padding:12px 14px; border-bottom:1px solid rgba(255,255,255,.03); }
304304 .ar-row.inactive { opacity:.5; }
305305 .ar-row:hover { background:rgba(255,255,255,.02); }
306306 .ar-pri { color:var(--col-text-dim); font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-size:11px; }
307307 .ar-name { font-weight:600; color:var(--col-text); }
308308 .ar-desc { color:var(--col-text-dim); font-size:11.5px; margin-top:2px; }
309309 .ar-tpl { color:var(--col-text-2); font-size:12.5px; }
310310 .ar-scope{ color:var(--col-text-dim); font-size:11px; font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; }
311311 .ar-delay{ color:var(--col-text-2); font-size:12px; font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; text-align:center; }
312312 .ar-status{ text-align:center; font-size:10.5px; letter-spacing:.6px; text-transform:uppercase; padding:3px 8px; border-radius:999px; font-weight:700; }
313313 .ar-status.on { background:rgba(34,197,94,.10); color:#a7f3d0; border:1px solid rgba(34,197,94,.35); }
314314 .ar-status.off { background:rgba(255,255,255,.04); color:var(--col-text-dim); border:1px solid var(--col-border); }
315315 .ar-actions a, .ar-actions button { background:transparent; border:1px solid var(--col-border); color:var(--col-text-muted); padding:4px 10px; border-radius:6px; font-size:11px; cursor:pointer; text-decoration:none; font-family:inherit; }
316316 .ar-actions a:hover, .ar-actions button:hover { color:var(--col-text); border-color:var(--col-brand, var(--col-primary, #b53048)); }
317317 </style>~;
318318 foreach my $r (@$rules) {
319319 my $cls = $r->{is_active} ? '' : ' inactive';
320320 my $tpl_row = $cr->get($r->{template_id});
321321 my $tpl_nm = _esc($tpl_row ? $tpl_row->{name} : '(missing template)');
322322 my ($sc, $sl) = $r->{is_active} ? ('on','ACTIVE') : ('off','OFF');
323323 my $id_h = _esc($r->{id});
324324 my $delay = _esc($r->{min_delay_seconds}) . '-' . _esc($r->{max_delay_seconds}) . 's';
325325 $rules_html .= qq~
326326 <div class="ar-row$cls">
327327 <div class="ar-pri">#~._esc($r->{priority}).qq~</div>
328328 <div>
329329 <div class="ar-name">~._esc($r->{name}).qq~</div>
330330 <div class="ar-desc">~._esc($r->{description}||'').qq~</div>
331331 </div>
332332 <div class="ar-tpl">$tpl_nm <span class="ar-scope">/ #~._esc($r->{template_id}).qq~</span></div>
333333 <div class="ar-scope">~._esc($r->{site_scope}).qq~</div>
334334 <div class="ar-delay">$delay</div>
335335 <div style="display:flex;gap:6px;justify-content:flex-end;align-items:center">
336336 <span class="ar-status $sc">$sl</span>
337337 <div class="ar-actions" style="display:flex;gap:4px">
338338 <a href="/auto_ack.cgi?edit=$id_h">Edit</a>
339339 <form method="post" action="/auto_ack.cgi" style="display:inline">
340340 <input type="hidden" name="toggle" value="$id_h">
341341 <button type="submit">Toggle</button>
342342 </form>
343343 </div>
344344 </div>
345345 </div>~;
346346 }
347347}
348348$body .= qq~
349349<div class="module glow-magenta" style="margin-bottom:16px">
350350 <div class="module-head">
351351 <div class="left">
352352 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>
353353 <div class="module-title">Rules</div>
354354 <span class="module-subtitle">Each rule maps one canned template to a match condition set. Priority: lower fires first; rules are independent, all matching rules can fire.</span>
355355 </div>
356356 <div class="right" style="padding-right:14px">$newbtn</div>
357357 </div>
358358 <div class="module-body" style="padding:0"><div class="ar-list">$rules_html</div></div>
359359</div>
360360~;
361361
362362# Queue view
363363my $q_html = '';
364364if (!@$queue) {
365365 $q_html = q~<div style="padding:24px 14px;text-align:center;color:var(--col-text-dim);font-size:13px">Nothing queued.</div>~;
366366} else {
367367 $q_html .= q~<style>
368368 .qq-row { display:grid; grid-template-columns: 90px 90px 1.8fr 1.4fr 1fr; gap:12px; align-items:center; padding:10px 14px; border-bottom:1px solid rgba(255,255,255,.03); font-size:12.5px; }
369369 .qq-when { color:var(--col-text-dim); font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-size:11.5px; }
370370 .qq-site { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; color:var(--col-text-muted); font-size:11px; }
371371 </style>~;
372372 foreach my $q (@$queue) {
373373 my $site = _esc($q->{site});
374374 my $tid = _esc($q->{thread_id});
375375 my $tpl = _esc($q->{template_name} || '#'.$q->{template_id});
376376 my $rule = _esc($q->{rule_name} || '#'.$q->{rule_id});
377377 my $when = _rel_age($q->{fire_at});
378378 my $href = "/support.cgi?thread=$site:$tid";
379379 $q_html .= qq~
380380 <div class="qq-row">
381381 <div class="qq-when">fire $when</div>
382382 <div class="qq-site">$site</div>
383383 <div><a href="$href" style="color:var(--col-text);text-decoration:none">thread #$tid</a></div>
384384 <div>$tpl</div>
385385 <div>via rule: $rule</div>
386386 </div>~;
387387 }
388388}
389389$body .= qq~
390390<div class="module glow-cyan" style="margin-bottom:16px">
391391 <div class="module-head"><div class="left">
392392 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
393393 <div class="module-title">Queued (waiting to fire)</div>
394394 <span class="module-subtitle">Rows are re-verified at fire time -- if a human replied to that thread first, the row is auto-skipped and marked in history.</span>
395395 </div></div>
396396 <div class="module-body" style="padding:0">$q_html</div>
397397</div>
398398~;
399399
400400# History view
401401my $h_html = '';
402402if (!@$history) {
403403 $h_html = q~<div style="padding:24px 14px;text-align:center;color:var(--col-text-dim);font-size:13px">No history yet.</div>~;
404404} else {
405405 $h_html .= q~<style>
406406 .hh-row { display:grid; grid-template-columns: 80px 80px 90px 1.5fr 1.4fr 2fr; gap:10px; align-items:center; padding:9px 14px; border-bottom:1px solid rgba(255,255,255,.03); font-size:12px; }
407407 .hh-when { color:var(--col-text-dim); font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-size:11px; }
408408 .hh-status { text-align:center; font-size:10px; letter-spacing:.5px; text-transform:uppercase; padding:2px 7px; border-radius:999px; font-weight:700; }
409409 .hh-status.sent { background:rgba(34,197,94,.10); color:#a7f3d0; border:1px solid rgba(34,197,94,.35); }
410410 .hh-status.skipped { background:rgba(180,150,50,.10); color:#facc15; border:1px solid rgba(180,150,50,.35); }
411411 .hh-status.failed { background:rgba(239,68,68,.10); color:#fca5a5; border:1px solid rgba(239,68,68,.35); }
412412 .hh-note { color:var(--col-text-dim); font-size:11px; }
413413 </style>~;
414414 foreach my $h (@$history) {
415415 my $when = _rel_age($h->{sent_at} || $h->{discovered_at});
416416 my $site = _esc($h->{site});
417417 my $tid = _esc($h->{thread_id});
418418 my $tpl = _esc($h->{template_name} || '#'.$h->{template_id});
419419 my $stat = $h->{status};
420420 my $note = _esc($h->{skip_reason} || $h->{error_message} || ($stat eq 'sent' ? 'msg #' . ($h->{message_id} || '?') : ''));
421421 my $href = "/support.cgi?thread=$site:$tid";
422422 $h_html .= qq~
423423 <div class="hh-row">
424424 <div class="hh-when">$when</div>
425425 <div class="hh-status $stat">$stat</div>
426426 <div class="hh-when">$site</div>
427427 <div><a href="$href" style="color:var(--col-text);text-decoration:none">thread #$tid</a></div>
428428 <div>$tpl</div>
429429 <div class="hh-note">$note</div>
430430 </div>~;
431431 }
432432}
433433$body .= qq~
434434<div class="module glow-lime">
435435 <div class="module-head"><div class="left">
436436 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></div>
437437 <div class="module-title">Recent history</div>
438438 <span class="module-subtitle">Sent / skipped / failed fires, newest first. Skip reasons: <code>admin_replied_first</code> = a human beat us to it; <code>thread_closed</code> = customer closed it; <code>outside_hours</code> = business-hours guard tripped.</span>
439439 </div></div>
440440 <div class="module-body" style="padding:0">$h_html</div>
441441</div>
442442~;
443443
444444MODS::MetaAdmin::Wrapper->new->render(
445445 title => 'Auto-Ack', page_key => 'auto_ack', body => $body, userinfo => $u,
446446);