334 lines
7.9 KiB
HTML
334 lines
7.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ACDL Evidence Timeline</title>
|
|
<style>
|
|
:root {
|
|
--stage-dev: #2563eb;
|
|
--stage-qa: #ca8a04;
|
|
--stage-prod: #ea580c;
|
|
--stage-finalize: #16a34a;
|
|
--stage-genesis: #6b7280;
|
|
--stage-rejected: #dc2626;
|
|
--bg: #f8fafc;
|
|
--card-bg: #ffffff;
|
|
--text: #0f172a;
|
|
--muted: #64748b;
|
|
--border: #e2e8f0;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
line-height: 1.5;
|
|
}
|
|
header {
|
|
padding: 24px 32px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--card-bg);
|
|
}
|
|
header h1 {
|
|
margin: 0 0 6px;
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
}
|
|
header p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
font-size: 0.95rem;
|
|
}
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px 32px;
|
|
background: var(--card-bg);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
button#refresh {
|
|
appearance: none;
|
|
border: 1px solid var(--border);
|
|
background: var(--text);
|
|
color: #fff;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
button#refresh:hover { opacity: 0.9; }
|
|
button#refresh:active { transform: translateY(1px); }
|
|
.toolbar .status {
|
|
color: var(--muted);
|
|
font-size: 0.85rem;
|
|
}
|
|
main {
|
|
padding: 24px 32px 48px;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
.empty {
|
|
padding: 48px 24px;
|
|
text-align: center;
|
|
color: var(--muted);
|
|
background: var(--card-bg);
|
|
border: 1px dashed var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
ol.timeline {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
position: relative;
|
|
}
|
|
ol.timeline::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 11px;
|
|
top: 6px;
|
|
bottom: 6px;
|
|
width: 2px;
|
|
background: var(--border);
|
|
}
|
|
li.event {
|
|
position: relative;
|
|
padding: 12px 0 12px 40px;
|
|
}
|
|
li.event::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 6px;
|
|
top: 18px;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: var(--dot, var(--muted));
|
|
border: 2px solid var(--card-bg);
|
|
box-shadow: 0 0 0 1px var(--border);
|
|
}
|
|
.card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border);
|
|
border-left: 4px solid var(--dot, var(--muted));
|
|
border-radius: 8px;
|
|
padding: 12px 16px;
|
|
}
|
|
.card .row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.seq {
|
|
display: inline-block;
|
|
min-width: 28px;
|
|
padding: 2px 6px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
border-radius: 4px;
|
|
background: #eef2ff;
|
|
color: #3730a3;
|
|
border: 1px solid #c7d2fe;
|
|
}
|
|
.chip {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
border-radius: 999px;
|
|
color: #fff;
|
|
background: var(--dot, var(--muted));
|
|
}
|
|
.ts {
|
|
font-size: 0.8rem;
|
|
color: var(--muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.event-text {
|
|
margin: 4px 0 6px;
|
|
font-size: 0.95rem;
|
|
}
|
|
.hash {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
word-break: break-all;
|
|
}
|
|
footer {
|
|
padding: 16px 32px 24px;
|
|
border-top: 1px solid var(--border);
|
|
color: var(--muted);
|
|
font-size: 0.8rem;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
footer code {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
background: #f1f5f9;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>ACDL Evidence Timeline</h1>
|
|
<p>ACDL — Agentic Cloud Delivery Platform · Audit Timeline</p>
|
|
</header>
|
|
<div class="toolbar">
|
|
<button id="refresh" type="button">Refresh</button>
|
|
<span class="status" id="status"></span>
|
|
</div>
|
|
<main>
|
|
<div id="container">
|
|
<div class="empty">Loading…</div>
|
|
</div>
|
|
</main>
|
|
<footer>
|
|
<div id="footer"></div>
|
|
</footer>
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
var AUDIT_URL = "./audit.json";
|
|
var STAGE_COLORS = {
|
|
dev: "var(--stage-dev)",
|
|
qa: "var(--stage-qa)",
|
|
prod: "var(--stage-prod)",
|
|
finalize: "var(--stage-finalize)",
|
|
genesis: "var(--stage-genesis)"
|
|
};
|
|
|
|
function $(id) { return document.getElementById(id); }
|
|
|
|
function stageColor(stage, eventText) {
|
|
var evt = (eventText || "").toString().toLowerCase();
|
|
if (evt.indexOf("rejected") !== -1) {
|
|
return "var(--stage-rejected)";
|
|
}
|
|
return STAGE_COLORS[stage] || "var(--stage-genesis)";
|
|
}
|
|
|
|
function dash(v) {
|
|
return (v === null || v === undefined || v === "") ? "—" : v;
|
|
}
|
|
|
|
function hashPreview(hash) {
|
|
if (hash === null || hash === undefined || hash === "") return "—";
|
|
var s = String(hash);
|
|
return s.slice(0, 12) + "…";
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function auditUrlDisplay() {
|
|
try {
|
|
var href = window.location.href;
|
|
var slash = href.lastIndexOf("/");
|
|
if (slash >= 0) {
|
|
return href.slice(0, slash + 1) + "audit.json";
|
|
}
|
|
} catch (e) {}
|
|
return AUDIT_URL;
|
|
}
|
|
|
|
function renderEmpty(msg) {
|
|
$("container").innerHTML =
|
|
'<div class="empty">' + esc(msg) + "</div>";
|
|
}
|
|
|
|
function renderTimeline(events) {
|
|
if (!Array.isArray(events)) {
|
|
renderEmpty("No audit data yet");
|
|
return;
|
|
}
|
|
if (events.length === 0) {
|
|
renderEmpty("No audit data yet");
|
|
return;
|
|
}
|
|
var sorted = events.slice().sort(function (a, b) {
|
|
var sa = (a && typeof a.seq === "number") ? a.seq : 0;
|
|
var sb = (b && typeof b.seq === "number") ? b.seq : 0;
|
|
return sa - sb;
|
|
});
|
|
var html = '<ol class="timeline">';
|
|
for (var i = 0; i < sorted.length; i++) {
|
|
var e = sorted[i] || {};
|
|
var stage = dash(e.stage);
|
|
var color = stageColor(e.stage, e.event);
|
|
html += '<li class="event" style="--dot:' + color + ';">';
|
|
html += '<div class="card" style="--dot:' + color + ';">';
|
|
html += '<div class="row">';
|
|
html += '<span class="seq">#' + esc(dash(e.seq)) + "</span>";
|
|
html += '<span class="chip">' + esc(stage) + "</span>";
|
|
html += '<span class="ts">' + esc(dash(e.ts)) + "</span>";
|
|
html += "</div>";
|
|
html += '<div class="event-text">' + esc(dash(e.event)) + "</div>";
|
|
html += '<div class="hash">' + esc(hashPreview(e.hash)) + "</div>";
|
|
html += "</div>";
|
|
html += "</li>";
|
|
}
|
|
html += "</ol>";
|
|
$("container").innerHTML = html;
|
|
}
|
|
|
|
function renderFooter(ok) {
|
|
var when = new Date().toISOString();
|
|
var url = auditUrlDisplay();
|
|
var prefix = "Fetched at " + when + " · audit.json: ";
|
|
$("footer").innerHTML =
|
|
esc(prefix) + '<code>' + esc(url) + "</code>" +
|
|
(ok ? "" : " (fetch failed)");
|
|
}
|
|
|
|
function setStatus(msg) {
|
|
$("status").textContent = msg || "";
|
|
}
|
|
|
|
function fetchAudit() {
|
|
setStatus("Fetching…");
|
|
fetch(AUDIT_URL, { cache: "no-store" })
|
|
.then(function (res) {
|
|
if (!res.ok) {
|
|
throw new Error("HTTP " + res.status);
|
|
}
|
|
return res.json();
|
|
})
|
|
.then(function (data) {
|
|
if (!Array.isArray(data)) {
|
|
throw new Error("not an array");
|
|
}
|
|
renderTimeline(data);
|
|
renderFooter(true);
|
|
setStatus("Loaded " + data.length + " event(s)");
|
|
})
|
|
.catch(function (err) {
|
|
renderEmpty("No audit data yet");
|
|
renderFooter(false);
|
|
setStatus("Fetch failed: " + (err && err.message ? err.message : "error"));
|
|
});
|
|
}
|
|
|
|
$("refresh").addEventListener("click", fetchAudit);
|
|
fetchAudit();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |