Integrate TDS v1.2 lt.* APIs throughout app
Lint / Python (flake8) (push) Failing after 58s
Lint / JS (eslint) (push) Successful in 7s
Security / Python Security (bandit) (push) Failing after 44s
Test / Python Tests (pytest) (push) Successful in 1m24s
Lint / Notify on failure (push) Successful in 2s
Lint / Deploy (push) Has been skipped
Lint / Python (flake8) (push) Failing after 58s
Lint / JS (eslint) (push) Successful in 7s
Security / Python Security (bandit) (push) Failing after 44s
Test / Python Tests (pytest) (push) Successful in 1m24s
Lint / Notify on failure (push) Successful in 2s
Lint / Deploy (push) Has been skipped
- app.js: replace raw fetch/escHtml/fmtRelTime with lt.api, lt.escHtml, lt.time.ago; modal open/close via lt.modal; add _toIso() for timestamp normalisation
- index.html: data-action="refresh", data-duration pills, lt.autoRefresh.start, remove local fmtRelTime
- suppressions.html: lt.api.post/delete, data-dur pill delegation
- base.html: user avatar with initials, admin badge, lt.keys.on('r') replaces manual keydown handler
- base.css: add dot-*, chip, row-state aliases so apps can use unprefixed class names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+8
-7
@@ -102,7 +102,12 @@
|
||||
</div>
|
||||
|
||||
<div class="lt-header-right">
|
||||
<span class="lt-header-user">{{ user.name or user.username }}</span>
|
||||
{% set _uname = user.name or user.username %}
|
||||
<div class="lt-avatar" title="{{ _uname }}" aria-label="{{ _uname }}">{{ _uname[0] | upper }}</div>
|
||||
<span class="lt-header-user">{{ _uname }}</span>
|
||||
{% if user.groups and 'admin' in user.groups %}
|
||||
<span class="lt-badge lt-badge-admin">admin</span>
|
||||
{% endif %}
|
||||
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
|
||||
aria-label="Toggle theme" title="Toggle light/dark mode">☀</button>
|
||||
</div>
|
||||
@@ -206,12 +211,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
// R key to refresh on dashboard
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
|
||||
if (e.key === 'r' || e.key === 'R') {
|
||||
if (typeof refreshAll === 'function') { e.preventDefault(); refreshAll(); }
|
||||
}
|
||||
lt.keys.on('r', function() {
|
||||
if (typeof refreshAll === 'function') refreshAll();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+11
-24
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div class="status-meta">
|
||||
<span class="last-check" id="last-check">{{ last_check }}</span>
|
||||
<button class="lt-btn lt-btn-ghost lt-btn-sm" onclick="refreshAll()">↻ REFRESH</button>
|
||||
<button class="lt-btn lt-btn-ghost lt-btn-sm" data-action="refresh">↻ REFRESH</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -386,7 +386,7 @@
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header">
|
||||
<h3 class="lt-modal-title" id="suppress-modal-title">Suppress Alert</h3>
|
||||
<button type="button" class="lt-modal-close" onclick="closeSuppressModal()" aria-label="Close">✕</button>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="suppress-form" onsubmit="submitSuppress(event)">
|
||||
<div class="lt-modal-body">
|
||||
@@ -415,18 +415,18 @@
|
||||
<div class="lt-form-group" style="margin-bottom:0">
|
||||
<label class="lt-label">Duration</label>
|
||||
<div class="duration-pills">
|
||||
<button type="button" class="pill" onclick="setDuration(30, this)">30 min</button>
|
||||
<button type="button" class="pill" onclick="setDuration(60, this)">1 hr</button>
|
||||
<button type="button" class="pill" onclick="setDuration(240, this)">4 hr</button>
|
||||
<button type="button" class="pill" onclick="setDuration(480, this)">8 hr</button>
|
||||
<button type="button" class="pill pill-manual active" onclick="setDuration(null, this)">Manual ∞</button>
|
||||
<button type="button" class="pill" data-duration="30">30 min</button>
|
||||
<button type="button" class="pill" data-duration="60">1 hr</button>
|
||||
<button type="button" class="pill" data-duration="240">4 hr</button>
|
||||
<button type="button" class="pill" data-duration="480">8 hr</button>
|
||||
<button type="button" class="pill pill-manual active" data-duration="">Manual ∞</button>
|
||||
</div>
|
||||
<input type="hidden" id="sup-expires" name="expires_minutes" value="">
|
||||
<div class="lt-field-hint" id="duration-hint">Persists until manually removed.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button type="button" class="lt-btn lt-btn-secondary" onclick="closeSuppressModal()">Cancel</button>
|
||||
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
|
||||
<button type="submit" class="lt-btn lt-btn-primary">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -437,23 +437,11 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
setInterval(refreshAll, 30000);
|
||||
|
||||
// ── Relative time display for event age cells ──────────────────
|
||||
function fmtRelTime(tsStr) {
|
||||
if (!tsStr) return '–';
|
||||
const d = new Date(tsStr.replace(' UTC', 'Z').replace(' ', 'T'));
|
||||
if (isNaN(d)) return tsStr;
|
||||
const secs = Math.floor((Date.now() - d) / 1000);
|
||||
if (secs < 60) return `${secs}s ago`;
|
||||
if (secs < 3600) return `${Math.floor(secs/60)}m ago`;
|
||||
if (secs < 86400) return `${Math.floor(secs/3600)}h ago`;
|
||||
return `${Math.floor(secs/86400)}d ago`;
|
||||
}
|
||||
lt.autoRefresh.start(refreshAll, 30000);
|
||||
|
||||
function updateEventAges() {
|
||||
document.querySelectorAll('.event-age[data-ts]').forEach(el => {
|
||||
el.textContent = fmtRelTime(el.dataset.ts);
|
||||
el.textContent = lt.time.ago(_toIso(el.dataset.ts));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -463,8 +451,7 @@
|
||||
// ── Event duration (resolved_at - first_seen) ──────────────────
|
||||
function fmtDuration(firstTs, resolvedTs) {
|
||||
if (!firstTs || !resolvedTs) return '–';
|
||||
const parse = s => new Date(s.replace(' UTC', 'Z').replace(' ', 'T'));
|
||||
const secs = Math.floor((parse(resolvedTs) - parse(firstTs)) / 1000);
|
||||
const secs = Math.floor((new Date(_toIso(resolvedTs)) - new Date(_toIso(firstTs))) / 1000);
|
||||
if (secs < 0) return '–';
|
||||
if (secs < 60) return `${secs}s`;
|
||||
if (secs < 3600) return `${Math.floor(secs/60)}m`;
|
||||
|
||||
+20
-19
@@ -51,11 +51,11 @@
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label">Duration</label>
|
||||
<div class="duration-pills">
|
||||
<button type="button" class="pill" onclick="setDur(30, this)">30 min</button>
|
||||
<button type="button" class="pill" onclick="setDur(60, this)">1 hr</button>
|
||||
<button type="button" class="pill" onclick="setDur(240, this)">4 hr</button>
|
||||
<button type="button" class="pill" onclick="setDur(480, this)">8 hr</button>
|
||||
<button type="button" class="pill pill-manual active" onclick="setDur(null, this)">Manual ∞</button>
|
||||
<button type="button" class="pill" data-dur="30">30 min</button>
|
||||
<button type="button" class="pill" data-dur="60">1 hr</button>
|
||||
<button type="button" class="pill" data-dur="240">4 hr</button>
|
||||
<button type="button" class="pill" data-dur="480">8 hr</button>
|
||||
<button type="button" class="pill pill-manual active" data-dur="">Manual ∞</button>
|
||||
</div>
|
||||
<input type="hidden" id="s-expires" name="expires_minutes" value="">
|
||||
<div class="lt-field-hint" id="s-dur-hint">Persists until manually removed.</div>
|
||||
@@ -207,30 +207,31 @@
|
||||
reason: form.reason.value,
|
||||
expires_minutes: form.expires_minutes.value ? parseInt(form.expires_minutes.value) : null,
|
||||
};
|
||||
const resp = await fetch('/api/suppressions', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
try {
|
||||
await lt.api.post('/api/suppressions', payload);
|
||||
showToast('Suppression applied', 'success');
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} else {
|
||||
showToast(data.error || 'Error', 'error');
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSuppression(id) {
|
||||
if (!confirm('Remove this suppression?')) return;
|
||||
const resp = await fetch(`/api/suppressions/${id}`, {method:'DELETE'});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
try {
|
||||
await lt.api.delete(`/api/suppressions/${id}`);
|
||||
document.getElementById(`sup-row-${id}`)?.remove();
|
||||
showToast('Suppression removed', 'success');
|
||||
} else {
|
||||
showToast(data.error || 'Failed to remove suppression', 'error');
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Failed to remove suppression', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const pill = e.target.closest('.pill[data-dur]');
|
||||
if (!pill) return;
|
||||
const val = pill.dataset.dur;
|
||||
setDur(val ? parseInt(val) : null, pill);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user