3 Commits

Author SHA1 Message Date
jared 597e1b1eea fix: correct phpcs indentation on SLA banner conditional block
Lint / PHP (phpcs PSR-12) (push) Successful in 24s
Lint / JS (eslint) (push) Successful in 11s
Security / PHP Security (semgrep) (push) Successful in 1m13s
Lint / Deploy (push) Successful in 3s
Lint / Notify on failure (push) Has been skipped
PHP inline conditionals inside HTML context must use 4-space indentation
to satisfy PSR-12 Generic.WhiteSpace.ScopeIndent rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:43:49 -04:00
jared 35a2b66038 refactor: migrate P1/P2 SLA banner to lt-sla-p1/lt-sla-p2 component
Lint / PHP (phpcs PSR-12) (push) Failing after 24s
Lint / JS (eslint) (push) Successful in 11s
Lint / Deploy (push) Has been cancelled
Lint / Notify on failure (push) Has been cancelled
Security / PHP Security (semgrep) (push) Has been cancelled
Replaces the lt-alert workaround with the new purpose-built SLA banner
component now in base.css:
- lt-sla-p1 (pulsing red) / lt-sla-p2 (static amber) wrapper classes
- Structured subcomponents: lt-sla-icon, lt-sla-info, lt-sla-title,
  lt-sla-bar + lt-sla-fill (gradient fill), lt-sla-meta, lt-sla-dismiss
- Dismiss now uses banner.hidden + sessionStorage key lt_sla_dismissed_<id>
  (aligns with web_template pattern; previous code used classList 'dismissed')
- Elapsed/remaining/breach state driven by same tick() interval, now updating
  lt-sla-fill width instead of a separate lt-progress bar inside lt-alert-msg

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:40:15 -04:00
jared b7aea8c683 sync: pull progress gradient fills and SLA banner from web_template v1.2
Lint / PHP (phpcs PSR-12) (push) Successful in 26s
Lint / JS (eslint) (push) Successful in 12s
Security / PHP Security (semgrep) (push) Successful in 1m12s
Lint / Deploy (push) Successful in 3s
Lint / Notify on failure (push) Has been skipped
Progress bars now use linear-gradient fills for a more dramatic terminal
readout appearance (matches web_template 39862fa):
- Default (orange), --cyan, --green, --red variants all upgraded from flat
  accent colors to directional gradients with highlight endpoints

SLA banner component (lt-sla-p1 / lt-sla-p2) added to base.css, replacing
the lt-alert workaround previously used for P1/P2 SLA display:
- lt-sla-p1: pulsing red banner (animation: lt-sla-pulse 2s)
- lt-sla-p2: static amber banner
- Subcomponents: icon, info, title, bar, fill, meta, dismiss
- Both fills use gradients for visual consistency (P2 amber→#ffd740)
- lt-sla-dismiss includes transition + :focus-visible ring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:29:57 -04:00
2 changed files with 123 additions and 61 deletions
+81 -5
View File
@@ -2458,7 +2458,7 @@ select option:checked {
} }
.lt-progress-bar { .lt-progress-bar {
height: 100%; height: 100%;
background: var(--accent-orange); background: linear-gradient(90deg, var(--accent-orange), #ff8c2b);
box-shadow: var(--glow-orange); box-shadow: var(--glow-orange);
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
@@ -2471,9 +2471,9 @@ select option:checked {
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4)); background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4));
} }
.lt-progress--cyan .lt-progress-bar { background: var(--accent-cyan); box-shadow: var(--glow-cyan); } .lt-progress--cyan .lt-progress-bar { background: linear-gradient(90deg, var(--accent-cyan), #33dfff); box-shadow: var(--glow-cyan); }
.lt-progress--green .lt-progress-bar { background: var(--accent-green); box-shadow: var(--glow-green); } .lt-progress--green .lt-progress-bar { background: linear-gradient(90deg, var(--accent-green), #33ffaa); box-shadow: var(--glow-green); }
.lt-progress--red .lt-progress-bar { background: var(--accent-red); box-shadow: var(--glow-red); } .lt-progress--red .lt-progress-bar { background: linear-gradient(90deg, var(--accent-red), #ff4466); box-shadow: var(--glow-red); }
.lt-progress--striped .lt-progress-bar { .lt-progress--striped .lt-progress-bar {
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, transparent, transparent 4px, 45deg, transparent, transparent 4px,
@@ -4479,7 +4479,83 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
61. TIMELINE / ACTIVITY FEED 61. SLA BANNER
----------------------------------------------------------------
lt-sla-p1 — pulsing red banner for critical SLA breach
lt-sla-p2 — static amber banner for high-priority SLA warning
---------------------------------------------------------------- */
.lt-sla-p1,
.lt-sla-p2 {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 1rem;
border: 1px solid;
font-family: var(--font-mono);
}
.lt-sla-p1 {
border-color: rgba(255,45,85,0.4);
background: rgba(255,45,85,0.08);
animation: lt-sla-pulse 2s infinite;
}
.lt-sla-p2 {
border-color: rgba(255,179,0,0.4);
background: rgba(255,179,0,0.08);
}
@keyframes lt-sla-pulse {
0%, 100% { box-shadow: 0 0 8px rgba(255,45,85,0.20); }
50% { box-shadow: 0 0 20px rgba(255,45,85,0.45); }
}
.lt-sla-icon { font-size: 1rem; flex-shrink: 0; }
.lt-sla-info { flex: 1; min-width: 0; }
.lt-sla-title {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 4px;
}
.lt-sla-p1 .lt-sla-title { color: var(--accent-red); text-shadow: var(--glow-red); }
.lt-sla-p2 .lt-sla-title { color: var(--accent-amber); text-shadow: var(--glow-amber); }
.lt-sla-bar {
height: 5px;
background: rgba(255,255,255,0.08);
position: relative;
overflow: hidden;
}
.lt-sla-fill {
height: 100%;
width: 0%;
transition: width 0.4s ease;
}
.lt-sla-p1 .lt-sla-fill { background: linear-gradient(90deg, var(--accent-red), var(--accent-orange)); box-shadow: 0 0 8px rgba(255,45,85,0.6); }
.lt-sla-p2 .lt-sla-fill { background: linear-gradient(90deg, var(--accent-amber), #ffd740); box-shadow: 0 0 8px rgba(255,179,0,0.6); }
.lt-sla-meta {
font-size: 0.60rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.10em;
flex-shrink: 0;
}
.lt-sla-dismiss {
font-size: 0.70rem;
color: var(--text-dim);
cursor: pointer;
background: none;
border: none;
flex-shrink: 0;
padding: 0 0.25rem;
font-family: var(--font-mono);
transition: color 0.15s ease;
}
.lt-sla-dismiss:hover { color: var(--text-secondary); }
.lt-sla-dismiss:focus-visible { outline: 1px dashed var(--accent-cyan); outline-offset: 2px; }
html[data-theme="light"] .lt-sla-p1 { background: rgba(180,30,50,0.06); border-color: rgba(180,30,50,0.35); }
html[data-theme="light"] .lt-sla-p2 { background: rgba(138,90,0,0.06); border-color: rgba(138,90,0,0.35); }
/* ----------------------------------------------------------------
62. TIMELINE / ACTIVITY FEED
---------------------------------------------------------------- */ ---------------------------------------------------------------- */
.lt-timeline { .lt-timeline {
display: flex; display: flex;
+42 -56
View File
@@ -215,56 +215,58 @@ include __DIR__ . '/layout_header.php';
1 => 8, 2 => 24, default => 72 1 => 8, 2 => 24, default => 72
}; };
$elapsedSeconds = time() - strtotime($ticket['created_at']); $elapsedSeconds = time() - strtotime($ticket['created_at']);
$elapsedHours = round($elapsedSeconds / 3600, 1);
$slaPct = min(100, round(($elapsedSeconds / ($slaTargetHours * 3600)) * 100)); $slaPct = min(100, round(($elapsedSeconds / ($slaTargetHours * 3600)) * 100));
$slaBreached = $elapsedSeconds >= ($slaTargetHours * 3600); $slaBreached = $elapsedSeconds >= ($slaTargetHours * 3600);
$alertClass = $priorityNum === 1 ? 'lt-alert--error' : 'lt-alert--warning'; $slaClass = $priorityNum === 1 ? 'lt-sla-p1' : 'lt-sla-p2';
$alertIcon = $priorityNum === 1 ? '[ ! ]' : '[ ~ ]'; $slaIcon = $priorityNum === 1 ? '[ ! ]' : '[ ~ ]';
$alertLabel = $priorityNum === 1 ? 'CRITICAL — P1 Ticket' : 'HIGH PRIORITY — P2 Ticket'; $slaLabel = $priorityNum === 1 ? 'P1 Critical' : 'P2 High';
$progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progress--red' : 'lt-progress--green'); $slaId = 'sla-' . htmlspecialchars($ticket['ticket_id'], ENT_QUOTES, 'UTF-8');
?> ?>
<!-- Priority alert banner — P1/P2 only, dismissible per session --> <!-- SLA banner — P1/P2 only, dismissible per session -->
<div class="lt-alert <?= $alertClass ?>" id="priorityAlertBanner" <div class="<?= $slaClass ?>" id="priorityAlertBanner" role="alert" aria-live="polite"
role="alert" aria-live="polite" data-sla-id="<?= $slaId ?>"
data-alert-id="priority-banner-<?= htmlspecialchars($ticket['ticket_id']) ?>"
data-created-at="<?= (int)strtotime($ticket['created_at']) ?>" data-created-at="<?= (int)strtotime($ticket['created_at']) ?>"
data-sla-hours="<?= $slaTargetHours ?>" data-sla-hours="<?= $slaTargetHours ?>"
style="margin-bottom:0.75rem"> style="margin-bottom:0.75rem">
<span class="lt-alert-icon" aria-hidden="true"><?= $alertIcon ?></span> <span class="lt-sla-icon" aria-hidden="true"><?= $slaIcon ?></span>
<div class="lt-alert-body"> <div class="lt-sla-info">
<div class="lt-alert-title"><?= $alertLabel ?></div> <div class="lt-sla-title">
<div class="lt-alert-msg"> <?= $slaLabel ?> — SLA: <span id="slaElapsedTimer"></span> elapsed of <?= $slaTargetHours ?>h limit
SLA target: <strong><?= $slaTargetHours ?>h</strong> &mdash; <?php if ($slaBreached) : ?>
Elapsed: <strong id="slaElapsedTimer"><?= $elapsedHours ?>h</strong> &nbsp;<span class="lt-text-danger" id="slaBreachLabel">BREACHED</span>
<?php if (!$slaBreached) : ?>
&mdash; Remaining: <strong id="slaCountdownTimer" class="lt-text-cyan"></strong>
<?php else : ?>
&mdash; <span class="lt-text-danger" id="slaCountdownTimer">SLA BREACHED (+<strong id="slaOverrunTimer"><?= round(($elapsedSeconds - $slaTargetHours * 3600) / 3600, 1) ?>h</strong>)</span>
<?php endif ?> <?php endif ?>
<div class="lt-progress lt-progress--sm <?= $progressClass ?>" id="slaProgress" style="margin-top:0.35rem" </div>
aria-label="SLA progress <?= $slaPct ?>%"> <div class="lt-sla-bar" aria-label="SLA progress <?= $slaPct ?>%" id="slaProgress">
<div class="lt-progress-bar" id="slaProgressBar" style="width:<?= $slaPct ?>%"></div> <div class="lt-sla-fill" id="slaProgressBar" style="width:<?= $slaPct ?>%"></div>
</div>
</div> </div>
</div> </div>
<button type="button" class="lt-alert-close" data-action="dismiss-priority-banner" aria-label="Dismiss">&#x2715;</button> <?php if (!$slaBreached) : ?>
<div class="lt-sla-meta" id="slaCountdownTimer"></div>
<?php else : ?>
<div class="lt-sla-meta lt-text-danger" id="slaCountdownTimer">+<span id="slaOverrunTimer"><?= round(($elapsedSeconds - $slaTargetHours * 3600) / 3600, 1) ?>h</span> over</div>
<?php endif ?>
<button type="button" class="lt-sla-dismiss" aria-label="Dismiss">&#x2715;</button>
</div> </div>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>"> <script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
(function(){ (function(){
var banner = document.getElementById('priorityAlertBanner'); var banner = document.getElementById('priorityAlertBanner');
var id = 'priority-banner-<?= htmlspecialchars($ticket['ticket_id']) ?>'; var id = banner.dataset.slaId;
try { if(sessionStorage.getItem('lt_dismissed_'+id)) banner.classList.add('dismissed'); } catch(e) {} try { if (id && sessionStorage.getItem('lt_sla_dismissed_' + id)) banner.hidden = true; } catch(e) {}
banner.querySelector('.lt-sla-dismiss').addEventListener('click', function() {
banner.hidden = true;
try { if (id) sessionStorage.setItem('lt_sla_dismissed_' + id, '1'); } catch(e) {}
});
// Live SLA timers — start after base.js initialises lt
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (!banner || banner.classList.contains('dismissed')) return; if (banner.hidden) return;
var createdAt = parseInt(banner.dataset.createdAt, 10) * 1000; var createdAt = parseInt(banner.dataset.createdAt, 10) * 1000;
var slaMs = parseInt(banner.dataset.slaHours, 10) * 3600 * 1000; var slaMs = parseInt(banner.dataset.slaHours, 10) * 3600 * 1000;
var deadline = new Date(createdAt + slaMs); var deadline = new Date(createdAt + slaMs);
var elapsedEl = document.getElementById('slaElapsedTimer'); var elapsedEl = document.getElementById('slaElapsedTimer');
var countdownEl = document.getElementById('slaCountdownTimer'); var countdownEl = document.getElementById('slaCountdownTimer');
var overrunEl = document.getElementById('slaOverrunTimer'); var overrunEl = document.getElementById('slaOverrunTimer');
var progressBar = document.getElementById('slaProgressBar'); var fillBar = document.getElementById('slaProgressBar');
var progressWrap = document.getElementById('slaProgress'); var progressWrap = document.getElementById('slaProgress');
function fmtHMS(ms) { function fmtHMS(ms) {
@@ -274,35 +276,19 @@ include __DIR__ . '/layout_header.php';
} }
function tick() { function tick() {
var now = Date.now(); var now = Date.now();
var elapsed = now - createdAt; var elapsed = now - createdAt;
var remaining = deadline - now; var remaining = deadline - now;
var pct = Math.min(100, Math.round((elapsed / slaMs) * 100)); var pct = Math.min(100, Math.round((elapsed / slaMs) * 100));
if (elapsedEl) elapsedEl.textContent = fmtHMS(elapsed); if (elapsedEl) elapsedEl.textContent = fmtHMS(elapsed);
if (progressBar) progressBar.style.width = pct + '%'; if (fillBar) fillBar.style.width = pct + '%';
if (progressWrap) progressWrap.setAttribute('aria-label', 'SLA progress ' + pct + '%'); if (progressWrap) progressWrap.setAttribute('aria-label', 'SLA progress ' + pct + '%');
if (remaining > 0) { if (remaining > 0) {
// SLA not yet breached if (countdownEl) countdownEl.textContent = fmtHMS(remaining) + ' remaining';
if (countdownEl) {
countdownEl.textContent = fmtHMS(remaining) + ' remaining';
countdownEl.className = pct >= 75 ? 'lt-text-danger' : 'lt-text-cyan';
}
if (progressWrap && pct >= 75) {
progressWrap.className = progressWrap.className.replace('lt-progress--green','lt-progress--red');
}
} else { } else {
// Breached if (overrunEl) overrunEl.textContent = fmtHMS(-remaining);
if (countdownEl && !overrunEl) {
countdownEl.innerHTML = 'SLA BREACHED (+' + fmtHMS(-remaining) + ')';
countdownEl.className = 'lt-text-danger';
} else if (overrunEl) {
overrunEl.textContent = fmtHMS(-remaining);
}
if (progressWrap && !progressWrap.classList.contains('lt-progress--red')) {
progressWrap.className = progressWrap.className.replace('lt-progress--green','').replace('lt-progress--red','') + ' lt-progress--red';
}
} }
} }