Skip to content

Instantly share code, notes, and snippets.

@ceesco53
Last active October 27, 2025 23:30
Show Gist options
  • Select an option

  • Save ceesco53/e42761dde6abf96824d46d7cdfc7bb3a to your computer and use it in GitHub Desktop.

Select an option

Save ceesco53/e42761dde6abf96824d46d7cdfc7bb3a to your computer and use it in GitHub Desktop.
maestro_dashboard_watch.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Maestro Certificate Rotation Dashboard
- Chains: chip layout; scrollable; per-card Sort Mode (Urgency / Name / Issuer depth exact)
- Exact issuer-depth calculation per node
- Hover tooltips show full issuer-chain trace
- Breadcrumbs above each card
- Dependency violation highlight (child expires before parent)
- Timeline / Table / Insights
- Watch folder + auto-refresh; CSV/JSON/Runbook; optional PDF export (WeasyPrint)
"""
import os, io, csv, glob, hashlib, datetime as dt
from typing import Any, Dict, List, Optional, Tuple
from flask import Flask, request, render_template_string, jsonify, Response
import yaml
# Optional PDF engine
try:
from weasyprint import HTML # type: ignore
HAS_WEASYPRINT = True
except Exception:
HAS_WEASYPRINT = False
app = Flask(__name__)
SLA_COLORS = {"<=30":"#e11d48","<=60":"#f97316","<=90":"#ca8a04",">90":"#16a34a","no-date":"#6b7280"}
HTML_PAGE = r"""
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Maestro Certificate Rotation Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif; margin: 24px; background:#f8fafc; color:#0f172a;}
.row { display:flex; gap:12px; flex-wrap:wrap; align-items:center; }
.card { background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:16px; }
.muted { color:#64748b; font-size:13px; }
.legend { display:flex; gap:8px; align-items:center; }
.pill { border-radius:8px; padding:2px 8px; color:#fff; font-size:12px; }
.tabs { display:flex; gap:8px; border-bottom:1px solid #e5e7eb; margin-top:8px; }
.tab { padding:8px 12px; border:1px solid #e5e7eb; border-bottom:none; border-radius:8px 8px 0 0; background:#f1f5f9; cursor:pointer; }
.tab.active { background:#fff; font-weight:600; }
.tab-panel { display:none; }
.tab-panel.active { display:block; }
table { border-collapse:collapse; width:100%; font-size:14px; }
th,td { border-top:1px solid #e5e7eb; padding:8px 10px; text-align:left; vertical-align:top; }
thead th { position:sticky; top:0; background:#f1f5f9; }
code { background:#f1f5f9; padding:2px 6px; border-radius:6px; }
.badge { color:#fff; border-radius:999px; padding:2px 8px; font-size:12px; background:#334155;}
.grid { display:grid; gap:12px; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.btn { padding:6px 10px; border:1px solid #e5e7eb; border-radius:8px; background:#fff; cursor:pointer; }
.btn:hover { background:#f8fafc; }
/* Chains chip layout */
.chain-viewport { overflow: auto; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px; background: #fff; }
.tier { margin: 6px 0 12px 0; }
.tier h5 { margin: 0 0 6px 0; color: #475569; font-size: 13px; }
.chips { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.chip { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; border-radius: 8px; background: #f1f5f9; border: 1px solid #e5e7eb; }
.chip .line1 { display: flex; justify-content: space-between; font-size: 12px; }
.chip .line2 { display: flex; justify-content: space-between; font-size: 11px; color: #475569; }
.chip .badges { display: flex; gap: 6px; }
.chip .badge { font-size:10px; padding:1px 6px; border-radius:999px; }
.chip .sla { width: 10px; height: 10px; border-radius: 50%; align-self: flex-end; }
.chip.violation { border:2px solid #ef4444; box-shadow: 0 0 0 2px rgba(239,68,68,0.15) inset; }
.crumbs { font-size:12px; color:#475569; }
.ins-grid { display:grid; gap:12px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.mini { border:1px solid #e5e7eb; border-radius:8px; padding:8px; background:#fff; }
.mini h5 { margin:0 0 6px 0; font-size:13px; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
</head>
<body>
<h2>Maestro Certificate Rotation Dashboard</h2>
<p class="muted">Auto-refresh on change (watch folder). Chains show tiered chips; Sort Mode per-card; dependency violations highlighted.</p>
<div class="card row" style="align-items:center;">
<label>Watch Folder:&nbsp;<input id="watchDir" style="min-width:340px;" placeholder="/path/to/dir"/></label>
<button class="btn" onclick="loadData()">Load</button>
<button class="btn" onclick="manualRefresh()">Refresh</button>
<button class="btn" onclick="exportCSV()">Export CSV</button>
<button class="btn" onclick="exportJSON()">Export JSON</button>
<button class="btn" onclick="downloadRunbook()">Runbook.md</button>
<button id="pdfBtn" class="btn" onclick="exportPDF()" disabled>Export PDF</button>
<span class="muted" id="status"></span>
<div class="legend" style="margin-left:auto;">
<span class="pill" style="background:#e11d48">≤30d</span>
<span class="pill" style="background:#f97316">≤60d</span>
<span class="pill" style="background:#ca8a04">≤90d</span>
<span class="pill" style="background:#16a34a">&gt;90d</span>
<span class="pill" style="background:#6b7280">No date</span>
</div>
</div>
<div class="card" style="margin-top:12px;">
<div class="tabs">
<div class="tab active" data-tab="timeline">Timeline</div>
<div class="tab" data-tab="table">Table</div>
<div class="tab" data-tab="chains">Chains</div>
<div class="tab" data-tab="insights">Insights</div>
</div>
<div id="panel-timeline" class="tab-panel active">
<div class="row" style="margin:8px 0;">
<label class="muted">Foundation</label>
<select id="fnd" onchange="applyFilters()"><option value="all">All</option></select>
<label class="muted">SLA</label>
<select id="sla" onchange="applyFilters()">
<option value="all">All</option>
<option value="<=30">≤30</option>
<option value="<=60">≤60</option>
<option value="<=90">≤90</option>
<option value=">90">&gt;90</option>
<option value="no-date">No date</option>
</select>
<label style="display:flex; align-items:center; gap:6px;"><input type="checkbox" id="onlyActive" onchange="applyFilters()"/>Only active</label>
<label style="display:flex; align-items:center; gap:6px;"><input type="checkbox" id="onlyCA" onchange="applyFilters()"/>Only CA</label>
<label style="display:flex; align-items:center; gap:6px;"><input type="checkbox" id="onlyTrans" onchange="applyFilters()"/>Only transitional</label>
</div>
<canvas id="chart" height="520"></canvas>
</div>
<div id="panel-table" class="tab-panel">
<div style="max-height:520px; overflow:auto;">
<table id="tbl">
<thead>
<tr>
<th>Foundation</th><th>Cert</th><th>Version</th><th>Issuer</th><th>Active</th><th>CA</th><th>T</th><th>Deployments</th><th>Valid Until</th><th>Days</th><th>SLA</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="panel-chains" class="tab-panel">
<div class="row" style="margin:8px 0; gap:16px;">
<label style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" id="chainsUseFilters" checked onchange="applyFilters()"/>
Chains use current filters
</label>
<label style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" id="groupByFoundation" onchange="applyFilters()"/>
Group by foundation
</label>
<label>Zoom <input type="range" min="50" max="200" value="100" id="chainsZoom" oninput="applyFilters()" /> <span id="chainsZoomVal">100%</span></label>
<label>Card height <input type="number" id="chainsMaxH" value="420" style="width:80px;" oninput="applyFilters()" /> px</label>
</div>
<div id="chains" class="grid"></div>
</div>
<div id="panel-insights" class="tab-panel">
<div class="row" style="margin-bottom:8px;">
<label class="muted">What‑if target date:&nbsp;</label>
<input id="whatIfDate" type="date" onchange="renderInsights(filteredRows())" />
<label class="muted" style="margin-left:12px;">Heatmap view</label>
<select id="insightsView" onchange="renderInsights(filteredRows())">
<option value="calendar">Calendar strip</option>
<option value="monthly">Monthly panels</option>
<option value="hist">Weekly histogram</option>
</select>
<label style="display:flex; align-items:center; gap:6px; margin-left:12px;">
<input type="checkbox" id="groupHeatByFoundation" onchange="renderInsights(filteredRows())"/>
Group heatmap by foundation
</label>
</div>
<div id="insights" class="grid"></div>
</div>
</div>
<script>
const COLORS = {"<=30":"#e11d48","<=60":"#f97316","<=90":"#ca8a04",">90":"#16a34a","no-date":"#6b7280"};
let POLL_HANDLE = null, SIG_HANDLE = null;
let RAW_ROWS = [];
let chart = null;
let CUR_DIR = "";
let CUR_SIG = "";
let CARD_DATA = {};
function toBucket(n) {
if (n === null || n === undefined || n === "") return 'no-date';
n = Number(n);
if (n <= 30) return '<=30';
if (n <= 60) return '<=60';
if (n <= 90) return '<=90';
return '>90';
}
// Tabs
document.querySelectorAll('.tab').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
el.classList.add('active');
document.getElementById('panel-' + el.dataset.tab).classList.add('active');
if (el.dataset.tab === 'timeline') renderChart(filteredRows());
if (el.dataset.tab === 'table') renderTable(filteredRows());
if (el.dataset.tab === 'chains') renderChains(document.getElementById('chainsUseFilters')?.checked ? filteredRows() : RAW_ROWS);
if (el.dataset.tab === 'insights') renderInsights(filteredRows());
});
});
function status(msg) { document.getElementById('status').textContent = msg; }
async function checkPDFAvailability() {
try { const res = await fetch('/api/pdf_available'); const data = await res.json(); return !!data.available; }
catch { return false; }
}
async function updatePDFButton() {
const btn = document.getElementById('pdfBtn'); if (!btn) return;
const available = await checkPDFAvailability();
const fndSel = document.getElementById('fnd');
const oneFoundation = (fndSel && fndSel.value && fndSel.value !== 'all');
btn.disabled = !(available && oneFoundation);
if (!available) document.getElementById('status').textContent = 'PDF export unavailable — install WeasyPrint';
}
function loadData() {
const dir = document.getElementById('watchDir').value.trim();
if (!dir) { alert('Enter a watch folder path'); return; }
CUR_DIR = dir;
fetchRows(dir);
if (POLL_HANDLE) clearInterval(POLL_HANDLE);
POLL_HANDLE = setInterval(()=>fetchRows(dir), 30000);
if (SIG_HANDLE) clearInterval(SIG_HANDLE);
SIG_HANDLE = setInterval(()=>checkSig(dir), 10000);
}
function manualRefresh() { if (CUR_DIR) fetchRows(CUR_DIR); }
async function checkSig(dir) {
try { const res = await fetch('/api/version?dir=' + encodeURIComponent(dir));
if (!res.ok) return; const data = await res.json();
if (data.sig && data.sig !== CUR_SIG) { CUR_SIG = data.sig; fetchRows(dir); }
} catch (e) {}
}
async function fetchRows(dir) {
try {
status('Loading...');
const res = await fetch('/api/rows?dir=' + encodeURIComponent(dir));
if (!res.ok) throw new Error('HTTP ' + res.status);
RAW_ROWS = await res.json();
populateFoundationSelect();
applyFilters();
status(`Loaded ${RAW_ROWS.length} rows @ ` + new Date().toLocaleTimeString());
} catch (e) { console.error(e); status('Error: ' + e.message); }
}
function populateFoundationSelect() {
const fndSel = document.getElementById('fnd');
const prev = fndSel.value;
const fnds = Array.from(new Set(RAW_ROWS.map(r => r.foundation))).sort();
fndSel.innerHTML = '<option value="all">All</option>' + fnds.map(f=>`<option value="${f}">${f}</option>`).join('');
if (fnds.includes(prev)) fndSel.value = prev;
updatePDFButton();
}
function filteredRows() {
const fnd = document.getElementById('fnd').value;
const onlyActive = document.getElementById('onlyActive').checked;
const onlyCA = document.getElementById('onlyCA').checked;
const onlyTrans = document.getElementById('onlyTrans').checked;
const sla = document.getElementById('sla').value;
let data = RAW_ROWS.slice();
if (fnd !== 'all') data = data.filter(r => r.foundation === fnd);
if (onlyActive) data = data.filter(r => r.active);
if (onlyCA) data = data.filter(r => r.certificate_authority);
if (onlyTrans) data = data.filter(r => r.transitional);
if (sla !== 'all') data = data.filter(r => toBucket(r.days_remaining) === sla);
data.sort((a,b)=> (a.days_remaining ?? 9e9) - (b.days_remaining ?? 9e9) || a.foundation.localeCompare(b.foundation) || a.cert_name.localeCompare(b.cert_name));
return data;
}
function applyFilters() {
const z = parseInt(document.getElementById('chainsZoom')?.value || '100',10);
const zv = document.getElementById('chainsZoomVal'); if (zv) zv.textContent = z + '%';
const data = filteredRows();
renderChart(data);
renderTable(data);
const useFilters = document.getElementById('chainsUseFilters')?.checked !== false;
renderChains(useFilters ? data : RAW_ROWS);
renderInsights(data);
updatePDFButton();
}
function ensureChart() {
const ctx = document.getElementById('chart');
if (!chart) {
chart = new Chart(ctx, {
type: 'bar',
data: { labels: [], datasets: [{ label: 'Days Remaining', data: [], backgroundColor: [] }] },
options: { indexAxis: 'y', responsive:true, plugins:{ legend:{ display:false } }, scales:{ x:{ title:{ display:true, text:'Days Remaining' } } } }
});
}
return chart;
}
function renderChart(rows) {
const c = ensureChart();
const labels = rows.map(r => `${r.foundation} • ${r.cert_name} • ${r.version_id_short}${r.active ? ' • ACTIVE' : ''}`);
c.data.labels = labels;
c.data.datasets[0].data = rows.map(r => (r.days_remaining ?? 0));
c.data.datasets[0].backgroundColor = rows.map(r => COLORS[toBucket(r.days_remaining)]);
c.update();
}
function renderTable(rows) {
const tbody = document.querySelector('#tbl tbody');
tbody.innerHTML = '';
rows.forEach(r => {
const vu = r.valid_until || r.valid_until_raw;
const bucket = toBucket(r.days_remaining);
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${r.foundation}</td>
<td>${r.cert_name}</td>
<td><code>${r.version_id_short}</code></td>
<td>${r.issuer || ''}</td>
<td>${r.active ? '<span class="badge">ACTIVE</span>' : ''}</td>
<td>${r.certificate_authority ? '<span class="badge">CA</span>' : ''}</td>
<td>${r.transitional ? '<span class="badge">T</span>' : ''}</td>
<td>${r.deployments || ''}</td>
<td>${vu || ''}</td>
<td style="text-align:right;">${r.days_remaining ?? ''}</td>
<td><span class="pill" style="background:${COLORS[bucket]}">${bucket}</span></td>
`;
tbody.appendChild(tr);
});
}
// ------- Chains (chip layout + exact depth + tooltips + violations) -------
function renderChains(rows) {
const el = document.getElementById('chains');
el.innerHTML = '';
CARD_DATA = {};
const byFoundationOnly = document.getElementById('groupByFoundation')?.checked === true;
const groups = {};
rows.forEach(r => {
const key = byFoundationOnly ? r.foundation : (r.foundation + '::' + r.cert_name);
(groups[key] = groups[key] || []).push(r);
});
const keys = Object.keys(groups).sort();
if (keys.length === 0) {
const fSel = document.getElementById('fnd')?.value || 'all';
const slaSel = document.getElementById('sla')?.value || 'all';
const onlyA = document.getElementById('onlyActive')?.checked ? 'ON' : 'OFF';
const onlyCA = document.getElementById('onlyCA')?.checked ? 'ON' : 'OFF';
const onlyT = document.getElementById('onlyTrans')?.checked ? 'ON' : 'OFF';
const useFilters = document.getElementById('chainsUseFilters')?.checked ? 'ON' : 'OFF';
const byF = document.getElementById('groupByFoundation')?.checked ? 'ON' : 'OFF';
const card = document.createElement('div'); card.className = 'card';
card.innerHTML = `
<h4 style="margin:0 0 6px 0;">No chains to display</h4>
<div class="muted">Try: unchecking "Only CA"/"Only transitional", switching foundation to "All", turning OFF "Chains use current filters", or enabling "Group by foundation".</div>
<div style="margin-top:8px; font-size:13px;"><strong>Current filters</strong>: foundation=<code>${fSel}</code>, SLA=<code>${slaSel}</code>, active=<code>${onlyA}</code>, CA=<code>${onlyCA}</code>, T=<code>${onlyT}</code>, ChainsUseFilters=<code>${useFilters}</code>, GroupByFoundation=<code>${byF}</code></div>
`;
el.appendChild(card);
return;
}
let cardIdx = 0;
keys.forEach(key => {
const [foundation, ...rest] = key.split('::');
const cert = byFoundationOnly ? '(all certificates)' : rest.join('::');
const gr = groups[key].slice();
const byVid = {}; gr.forEach(x => byVid[x.version_id] = x);
// exact depth + chain trace
function computeDepth(n, guard=0){
if (!n) return 0;
if (!n.issuer_version) return n.certificate_authority ? 1 : 99; // root CA depth 1; leaves default deep
if (guard>64) return 99;
const p = byVid[n.issuer_version];
if (!p) return n.certificate_authority ? 2 : 99;
const parentIsRoot = (p.certificate_authority && !p.issuer_version);
const base = parentIsRoot ? 1 : (computeDepth(p, guard+1));
return base + 1;
}
function chainTrace(n){
const out = [];
let cur = n; let guard=0;
while (cur && guard<64){
out.push(`${cur.cert_name}(${cur.version_id_short})`);
if (!cur.issuer_version) break;
cur = byVid[cur.issuer_version];
guard++;
}
return out.reverse(); // root -> node
}
// derive depth & parent lookup for violation detection
gr.forEach(n => { n.__depth = computeDepth(n); n.__parent = n.issuer_version ? byVid[n.issuer_version] : null; n.__trace = chainTrace(n).join(' → '); });
const roots = gr.filter(n => n.certificate_authority && !n.issuer_version);
const transCAs = gr.filter(n => n.certificate_authority && n.transitional && n.issuer_version);
const interCAsAll = gr.filter(n => n.certificate_authority && !n.transitional && n.issuer_version);
const leaves = gr.filter(n => !n.certificate_authority);
const interLevels = {};
interCAsAll.forEach(n => { const d = n.__depth; (interLevels[d] = interLevels[d] || []).push(n); });
const interDepths = Object.keys(interLevels).map(k=>parseInt(k,10));
const maxInterDepth = interDepths.length ? Math.max(...interDepths) : 0;
// cache raw arrays for resorting
CARD_DATA[cardIdx] = { roots, transCAs, interLevels, maxInterDepth, leaves };
const card = document.createElement('div');
card.className = 'card';
const heightPx = (document.getElementById('chainsMaxH')?.value||420);
const zoom = (parseInt(document.getElementById('chainsZoom')?.value||'100',10))/100;
card.innerHTML = `
<div style="display:flex; align-items:center; gap:12px; margin-bottom:6px; flex-wrap:wrap;">
<div>
<strong>${foundation} • ${cert}</strong><br/>
<span class="crumbs">Hierarchy: ROOT → TRANSITIONAL → INTERMEDIATE d1..dn → LEAF</span>
</div>
<span style="margin-left:auto;" class="legend">
<span class="pill" style="background:#e11d48">≤30</span>
<span class="pill" style="background:#f97316">≤60</span>
<span class="pill" style="background:#ca8a04">≤90</span>
<span class="pill" style="background:#16a34a">&gt;90</span>
<span class="pill" style="background:#6b7280">no-date</span>
</span>
<label>Sort mode:
<select onchange="resortCard(${cardIdx}, this.value)">
<option value="urgency">Urgency (days)</option>
<option value="name">Name</option>
<option value="depth">Issuer depth</option>
</select>
</label>
</div>
<div class="chain-viewport" style="max-height:${heightPx}px;">
<div class="chain-canvas" style="transform: scale(${zoom}); transform-origin: top left;"></div>
</div>
`;
el.appendChild(card);
// initial render with default 'urgency'
resortCard(cardIdx, 'urgency');
cardIdx += 1;
});
}
// Resort & rebuild a single card by index
function resortCard(idx, mode) {
const data = CARD_DATA[idx];
if (!data) return;
const { roots, transCAs, interLevels, maxInterDepth, leaves } = data;
function byUrg(a,b){ return (a.days_remaining??9e9)-(b.days_remaining??9e9) || (a.version_id_short||'').localeCompare(b.version_id_short||''); }
function byName(a,b){ return (a.version_id_short||'').localeCompare(b.version_id_short||''); }
function byDepth(a,b){ return (a.__depth||99)-(b.__depth||99) || byUrg(a,b); }
const sortFn = (mode==='name') ? byName : (mode==='depth' ? byDepth : byUrg);
const cardEls = document.querySelectorAll('#chains .card');
const cardEl = cardEls[idx];
const canvas = cardEl.querySelector('.chain-canvas');
canvas.innerHTML = '';
function chipHTML(n) {
const bucket = toBucket(n.days_remaining);
const color = COLORS[bucket];
const badges = [n.certificate_authority?'CA':'', n.transitional?'T':'', n.active?'ACTIVE':'']
.filter(Boolean).map(b=>`<span class="badge">${b}</span>`).join('');
// dependency violation: child expires before parent
const p = n.__parent;
const viol = p && (p.days_remaining!=null) && (n.days_remaining!=null) && (n.days_remaining < p.days_remaining);
const cls = 'chip' + (viol ? ' violation' : '');
const ttip = `${n.cert_name} • ${n.version_id}\nvalid_until: ${n.valid_until || n.valid_until_raw || ''}\ndays_remaining: ${n.days_remaining ?? 'NA'}\nissuer_chain: ${n.__trace}`;
return `<div class="${cls}" title="${ttip.replace(/"/g,'&quot;')}">
<div class="line1"><strong class="mono">${n.version_id_short}</strong><span class="sla" style="background:${color}"></span></div>
<div class="line2"><span>${n.days_remaining??'NA'}d</span><span>${n.valid_until || n.valid_until_raw || ''}</span></div>
<div class="line2"><span>${n.issuer_version_short?('issuer '+n.issuer_version_short):''} ${n.__depth?('• d'+n.__depth):''}</span><div class="badges">${badges}</div></div>
</div>`;
}
function buildTier(title, arr) {
const div = document.createElement('div');
div.className = 'tier';
div.innerHTML = `<h5>${title}</h5><div class="chips"></div>`;
const grid = div.querySelector('.chips');
arr.slice().sort(sortFn).forEach(n => grid.insertAdjacentHTML('beforeend', chipHTML(n)));
canvas.appendChild(div);
}
buildTier('ROOT CAs', roots);
buildTier('TRANSITIONAL CAs', transCAs);
for (let d = 1; d <= maxInterDepth; d++) buildTier(`INTERMEDIATE d${d}`, (interLevels[d]||[]));
buildTier('LEAVES', leaves);
}
// ------- Insights (heatmap, grouped toggle, multiple views) -------
function renderInsights(rows) {
const root = document.getElementById('insights');
if (!root) return;
root.innerHTML = '';
const groupByFnd = document.getElementById('groupHeatByFoundation')?.checked === true;
const view = document.getElementById('insightsView')?.value || 'calendar';
function parseEvents(inRows){
function parseISO(s){ try { return s ? new Date(s) : null; } catch { return null; } }
const evts = [];
inRows.forEach(r => {
const d = parseISO(r.valid_until) || parseISO(r.valid_until_raw);
if (!d || isNaN(d.getTime())) return;
evts.push({ date:d, foundation:r.foundation, cert:r.cert_name, version:r.version_id_short });
});
return evts;
}
function startOfWeek(d){ const x = new Date(d); const day=x.getDay(); const diff=(day+6)%7; x.setDate(x.getDate()-diff); x.setHours(0,0,0,0); return x; }
function fmt(d){ return d.toISOString().slice(0,10); }
function buildWeekMap(events){
const weekMap = new Map();
events.forEach(e => { const wk = startOfWeek(e.date); const k = fmt(wk);
if (!weekMap.has(k)) weekMap.set(k,{count:0, items:[]});
const rec = weekMap.get(k); rec.count++; rec.items.push(e);
});
return weekMap;
}
function palette(val, max){ if (val<=0) return '#e5e7eb'; const t=Math.max(0,Math.min(1,val/max)); const a=0.25+0.75*t; return `rgba(22,163,74,${a})`; }
function appendCalendarStrip(title, events){
const weekMap = buildWeekMap(events);
if (!events.length) return;
const minDt = new Date(Math.min(...events.map(e=>e.date.getTime())));
const maxDtEvent = new Date(Math.max(...events.map(e=>e.date.getTime())));
const horizon = new Date(); horizon.setMonth(horizon.getMonth()+12);
const endDt = maxDtEvent > horizon ? maxDtEvent : horizon;
const minWeek = startOfWeek(minDt);
const weeks = []; for (let d=new Date(minWeek); d<=endDt; d=new Date(d.getTime()+7*86400000)) weeks.push(new Date(d));
const counts = weeks.map(w => (weekMap.get(fmt(w))||{count:0}).count);
const maxCount = Math.max(1, ...counts);
const cell=14, gap=2, padL=60, padT=18;
const cols = weeks.length;
const vbW = padL + cols*(cell+gap) + 10;
const vbH = padT + 7*(cell+gap) + 18;
let svg = `<svg viewBox="0 0 ${vbW} ${vbH}" width="100%" height="${vbH}" xmlns="http://www.w3.org/2000/svg">`;
const dnames = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
for (let r=0; r<7; r++){ svg += `<text x="8" y="${padT + r*(cell+gap) + cell - 2}" font-size="10" fill="#475569">${dnames[r]}</text>`; }
function monthName(d){ return d.toLocaleString(undefined,{month:'short'}); }
let lastMonth=-1;
weeks.forEach((wk, idx)=>{ const m=wk.getMonth(); if (m!==lastMonth){ svg += `<text x="${padL + idx*(cell+gap)}" y="12" font-size="11" fill="#0f172a">${monthName(wk)}</text>`; lastMonth=m; } });
weeks.forEach((wk,c)=>{
for (let r=0; r<7; r++){
const k=fmt(startOfWeek(new Date(wk.getTime()+r*86400000)));
const val=(weekMap.get(k)||{count:0}).count;
const x=padL + c*(cell+gap), y=padT + r*(cell+gap);
svg += `<rect x="${x}" y="${y}" width="${cell}" height="${cell}" fill="${palette(val,maxCount)}"><title>${k}
${val} expiration(s)</title></rect>`;
}
});
svg += `</svg>`;
const card = document.createElement('div'); card.className='card'; card.innerHTML=`<h4 style="margin:0 0 6px 0;">${title}</h4>${svg}`;
root.appendChild(card);
appendTopWeeks(title, weekMap);
}
function appendMonthlyPanels(title, events){
const weekMap = buildWeekMap(events);
if (!events.length) return;
const minDt = new Date(Math.min(...events.map(e=>e.date.getTime())));
const maxDtEvent = new Date(Math.max(...events.map(e=>e.date.getTime())));
const start = new Date(minDt.getFullYear(), minDt.getMonth(), 1);
const horizon = new Date(); horizon.setMonth(horizon.getMonth()+12);
const endDt = maxDtEvent > horizon ? maxDtEvent : horizon;
const end = new Date(endDt.getFullYear(), endDt.getMonth(), 1);
// produce months inclusive
const months = [];
for (let d=new Date(start); d<=end; d=new Date(d.getFullYear(), d.getMonth()+1, 1)) months.push(new Date(d));
const wrap = document.createElement('div'); wrap.className='card'; wrap.innerHTML = `<h4 style="margin:0 0 6px 0;">${title}</h4><div class="ins-grid"></div>`;
const grid = wrap.querySelector('.ins-grid');
months.forEach(md => {
const first = new Date(md.getFullYear(), md.getMonth(), 1);
const last = new Date(md.getFullYear(), md.getMonth()+1, 0);
const weeks = [];
// compute Monday-aligned start
const s = new Date(first); const sdiff=(s.getDay()+6)%7; s.setDate(s.getDate()-sdiff);
for (let d=new Date(s); d<=last || d.getDay()!==1; d=new Date(d.getTime()+7*86400000)) weeks.push(new Date(d));
// max count for palette scaling within the month
const counts = weeks.map(w => (weekMap.get(fmt(w))||{count:0}).count);
const maxCount = Math.max(1, ...counts);
const cell=14, gap=2, padL=26, padT=18;
const cols = weeks.length;
const vbW = padL + cols*(cell+gap) + 8;
const vbH = padT + 7*(cell+gap) + 12;
let svg = `<svg viewBox="0 0 ${vbW} ${vbH}" width="100%" height="${vbH}" xmlns="http://www.w3.org/2000/svg">`;
const dnames=['M','T','W','T','F','S','S'];
for (let r=0; r<7; r++){ svg += `<text x="6" y="${padT + r*(cell+gap) + cell - 2}" font-size="9" fill="#475569">${dnames[r]}</text>`; }
weeks.forEach((wk,c)=>{
for (let r=0; r<7; r++){
const k=fmt(startOfWeek(new Date(wk.getTime()+r*86400000)));
const val=(weekMap.get(k)||{count:0}).count;
const x=padL + c*(cell+gap), y=padT + r*(cell+gap);
svg += `<rect x="${x}" y="${y}" width="${cell}" height="${cell}" fill="${palette(val,maxCount)}"><title>${k}
${val} expiration(s)</title></rect>`;
}
});
svg += `</svg>`;
const mini = document.createElement('div'); mini.className='mini';
mini.innerHTML = `<h5>${md.toLocaleString(undefined,{month:'long', year:'numeric'})}</h5>${svg}`;
grid.appendChild(mini);
});
root.appendChild(wrap);
appendTopWeeks(title, weekMap);
}
function appendTopWeeks(title, weekMap){
const sortedWeeks = Array.from(weekMap.entries())
.sort((a,b)=> b[1].count - a[1].count)
.slice(0, 8);
const top = document.createElement('div'); top.className = 'card';
let rowsHtml = '';
sortedWeeks.forEach(([k,rec])=>{
const list = rec.items
.slice(0,5)
.map(e => `${e.foundation} • ${e.cert} \`${e.version}\``)
.join('<br/>');
rowsHtml += `<tr><td><code>${k}</code></td><td style="text-align:right;"><strong>${rec.count}</strong></td><td>${list}${rec.items.length>5?'…':''}</td></tr>`;
});
if (!rowsHtml) rowsHtml = '<tr><td colspan="3" class="muted">No upcoming expirations found.</td></tr>';
top.innerHTML = `<h4 style="margin:0 0 6px 0;">Busiest Weeks — ${title}</h4>
<table style="width:100%; border-collapse:collapse;">
<thead><tr><th>Week (Mon)</th><th style="text-align:right;">Count</th><th>Examples</th></tr></thead>
<tbody>${rowsHtml}</tbody>
</table>`;
root.appendChild(top);
}
function appendWeeklyHistogram(title, events){
const weekMap = buildWeekMap(events);
const weeks = Array.from(weekMap.keys()).sort();
const data = weeks.map(k => weekMap.get(k).count);
const card = document.createElement('div'); card.className='card';
const id = 'hist_' + Math.random().toString(36).slice(2);
card.innerHTML = `<h4 style="margin:0 0 6px 0;">${title} — Weekly histogram</h4><canvas id="${id}" height="180"></canvas>`;
root.appendChild(card);
const ctx = document.getElementById(id).getContext('2d');
new Chart(ctx, { type:'bar', data:{ labels: weeks, datasets:[{ label:'Expirations', data, backgroundColor:'#16a34a' }]},
options:{ responsive:true, plugins:{legend:{display:false}}, scales:{ x:{ ticks:{ maxRotation:0, autoSkip:true, maxTicksLimit:12 }}}});
appendTopWeeks(title, weekMap);
}
if (!rows || !rows.length) {
const card = document.createElement('div'); card.className = 'card';
card.innerHTML = '<div class="muted">No data in scope for insights.</div>';
root.appendChild(card); return;
}
const events = parseEvents(rows);
if (!events.length) {
const card = document.createElement('div'); card.className = 'card';
card.innerHTML = '<div class="muted">No valid expiration dates in scope — try widening filters.</div>';
root.appendChild(card); return;
}
if (!groupByFnd){
if (view==='monthly') appendMonthlyPanels('Rotation Splash (All Foundations)', events);
else if (view==='hist') appendWeeklyHistogram('All Foundations', events);
else appendCalendarStrip('Rotation Splash (All Foundations)', events);
} else {
const byF = {}; events.forEach(e => { (byF[e.foundation]=byF[e.foundation]||[]).push(e); });
Object.keys(byF).sort().forEach(f => {
if (view==='monthly') appendMonthlyPanels(`Rotation Splash — ${f}`, byF[f]);
else if (view==='hist') appendWeeklyHistogram(f, byF[f]);
else appendCalendarStrip(`Rotation Splash — ${f}`, byF[f]);
});
}
}
// Exports & Runbook
function queryFromFilters() {
const params = new URLSearchParams();
params.set('dir', CUR_DIR || '');
params.set('foundation', document.getElementById('fnd').value);
params.set('sla', document.getElementById('sla').value);
params.set('onlyActive', document.getElementById('onlyActive').checked ? '1' : '0');
params.set('onlyCA', document.getElementById('onlyCA').checked ? '1' : '0');
params.set('onlyTrans', document.getElementById('onlyTrans').checked ? '1' : '0');
return params.toString();
}
function exportCSV() { const qs = queryFromFilters(); window.open('/api/export/csv?' + qs, '_blank'); }
function exportJSON() { const qs = queryFromFilters(); window.open('/api/export/json?' + qs, '_blank'); }
function downloadRunbook() { const qs = queryFromFilters(); window.open('/api/runbook?' + qs, '_blank'); }
function exportPDF() { const qs = queryFromFilters(); window.open('/api/export/pdf?' + qs, '_blank'); }
</script>
</body>
</html>
"""
# ----------------- Python helpers -----------------
def _parse_dt(s: Optional[str]) -> Optional[dt.datetime]:
if not s: return None
s = str(s).strip().replace(" ", "T")
if s.endswith("Z"): s = s[:-1] + "+00:00"
try:
return dt.datetime.fromisoformat(s)
except Exception:
return None
def _days_remaining(d: Optional[dt.datetime]) -> Optional[int]:
if not isinstance(d, dt.datetime): return None
if d.tzinfo is None:
d = d.replace(tzinfo=dt.timezone.utc)
now = dt.datetime.now(dt.timezone.utc)
return (d - now).days
def _short(s: Optional[str]) -> str:
if not s: return ""
return s[:8] + "…" if len(s) > 9 else s
def _flatten_doc(doc: Dict[str, Any], foundation: str) -> List[Dict[str, Any]]:
topo = (doc or {}).get("output", {}).get("topology", [])
index = {}
for cert in topo:
for v in (cert.get("versions") or []):
vid = v.get("version_id")
if vid:
index[vid] = cert.get("name") or "UNKNOWN_CERT"
rows = []
for cert in topo:
cname = cert.get("name") or "UNKNOWN_CERT"
for v in (cert.get("versions") or []):
vu = _parse_dt(v.get("valid_until"))
issuer_vid = v.get("signed_by_version") or ""
issuer = f"{index.get(issuer_vid, issuer_vid)}({_short(issuer_vid)})" if issuer_vid else ""
rows.append({
"foundation": foundation,
"cert_name": cname,
"version_id": v.get("version_id") or "",
"version_id_short": _short(v.get("version_id") or ""),
"issuer_version": issuer_vid,
"issuer_version_short": _short(issuer_vid) if issuer_vid else "",
"issuer": issuer,
"active": bool(v.get("active", False)),
"certificate_authority": bool(v.get("certificate_authority", False)),
"transitional": bool(v.get("transitional", False)),
"deployments": ", ".join(v.get("deployment_names") or []) if isinstance(v.get("deployment_names"), list) else (v.get("deployment_names") or ""),
"valid_until": (vu.isoformat() if vu else (v.get("valid_until") or "")),
"valid_until_raw": v.get("valid_until") or "",
"days_remaining": _days_remaining(vu),
})
return rows
def _load_dir(dirpath: str) -> List[Dict[str, Any]]:
out: List[Dict[str, Any]] = []
ymls = []
ymls.extend(glob.glob(os.path.join(dirpath, "*.yml")))
ymls.extend(glob.glob(os.path.join(dirpath, "*.yaml")))
for path in sorted(ymls):
foundation = os.path.splitext(os.path.basename(path))[0]
try:
with open(path, "r", encoding="utf-8") as f:
doc = yaml.safe_load(f)
out.extend(_flatten_doc(doc, foundation))
except Exception as e:
print("Failed to parse", path, ":", e)
return out
def _dir_signature(dirpath: str) -> str:
files = []
files.extend(glob.glob(os.path.join(dirpath, "*.yml")))
files.extend(glob.glob(os.path.join(dirpath, "*.yaml")))
sig_items = []
for p in sorted(files):
try:
st = os.stat(p)
sig_items.append(f"{os.path.basename(p)}:{int(st.st_mtime)}:{st.st_size}")
except Exception:
continue
return hashlib.sha256("|".join(sig_items).encode("utf-8")).hexdigest()
def _dir_file_hashes(dirpath: str):
files = []
files.extend(glob.glob(os.path.join(dirpath, '*.yml')))
files.extend(glob.glob(os.path.join(dirpath, '*.yaml')))
out = []
for p in sorted(files):
try:
with open(p, 'rb') as f:
h = hashlib.sha256(f.read()).hexdigest()
st = os.stat(p)
out.append({'file': os.path.basename(p), 'sha256': h, 'mtime': int(st.st_mtime), 'size': st.st_size})
except Exception:
continue
return out
# ----------------- Routes -----------------
@app.route("/", methods=["GET"])
def page():
return render_template_string(HTML_PAGE)
@app.route("/api/rows", methods=["GET"])
def api_rows():
dirpath = request.args.get("dir", "").strip()
if not dirpath or not os.path.isdir(dirpath):
return jsonify([])
rows = _load_dir(dirpath)
return jsonify(rows)
@app.route("/api/version", methods=["GET"])
def api_version():
dirpath = request.args.get("dir", "").strip()
if not dirpath or not os.path.isdir(dirpath):
return jsonify({"sig": ""})
return jsonify({"sig": _dir_signature(dirpath)})
@app.route("/api/export/csv", methods=["GET"])
def api_export_csv():
dirpath = request.args.get("dir", "").strip()
if not dirpath or not os.path.isdir(dirpath):
return Response("", mimetype="text/csv")
rows = _apply_filters(_load_dir(dirpath), request.args)
out = io.StringIO()
w = csv.writer(out)
w.writerow(["foundation","cert_name","version_id_short","issuer","active","certificate_authority","transitional","deployments","valid_until","days_remaining"])
for r in rows:
w.writerow([r["foundation"], r["cert_name"], r["version_id_short"], r["issuer"], r["active"],
r["certificate_authority"], r["transitional"], r["deployments"], r["valid_until"], r["days_remaining"]])
return Response(out.getvalue(), mimetype="text/csv",
headers={"Content-Disposition":"attachment; filename=cert_rotation_plan.csv"})
@app.route("/api/export/json", methods=["GET"])
def api_export_json():
dirpath = request.args.get("dir", "").strip()
if not dirpath or not os.path.isdir(dirpath):
return jsonify([])
rows = _apply_filters(_load_dir(dirpath), request.args)
return jsonify(rows)
@app.route("/api/runbook", methods=["GET"])
def api_runbook():
dirpath = request.args.get("dir", "").strip()
if not dirpath or not os.path.isdir(dirpath):
return Response("# Runbook\n\n_No directory provided._\n", mimetype="text/markdown")
rows = _apply_filters(_load_dir(dirpath), request.args)
def md_escape(s: str) -> str:
return s.replace("|","\\|")
cas = [r for r in rows if r["certificate_authority"]]
leaves = [r for r in rows if not r["certificate_authority"]]
cas.sort(key=lambda r: (r["days_remaining"] if r["days_remaining"] is not None else 9_000_000))
leaves.sort(key=lambda r: (r["days_remaining"] if r["days_remaining"] is not None else 9_000_000))
earliest: Dict[Tuple[str,str], Dict[str, Any]] = {}
for r in rows:
key = (r["foundation"], r["cert_name"])
if key not in earliest or (r["days_remaining"] or 9_000_000) < (earliest[key]["days_remaining"] or 9_000_000):
earliest[key] = r
out = io.StringIO()
out.write(f"# Certificate Rotation Runbook\n\n")
out.write("## Phase 1 — Rotate Certificate Authorities (CA-first)\n\n")
if not cas:
out.write("_No CA expirations in scope._\n\n")
else:
for r in cas:
out.write(f"- **{md_escape(r['foundation'])}** • **{md_escape(r['cert_name'])}** • `{r['version_id_short']}` — **{r['days_remaining']}d** (issuer: {md_escape(r.get('issuer',''))})\n")
out.write("\n")
out.write("## Phase 2 — Rotate Dependent Leaves\n\n")
if not leaves:
out.write("_No leaf expirations in scope._\n\n")
else:
for r in leaves:
out.write(f"- {md_escape(r['foundation'])} • {md_escape(r['cert_name'])} • `{r['version_id_short']}` — {r['days_remaining']}d (issuer: {md_escape(r.get('issuer',''))})\n")
out.write("\n")
out.write("## Earliest Expiring per Certificate (by foundation)\n\n")
for (f, c), r in sorted(earliest.items(), key=lambda kv: (kv[1]['days_remaining'] or 9_000_000)):
out.write(f"- {md_escape(f)} • {md_escape(c)} → `{r['version_id_short']}` in {r['days_remaining']}d\n")
out.write("\n")
return Response(out.getvalue(), mimetype="text/markdown",
headers={"Content-Disposition":"attachment; filename=Runbook.md"})
@app.route("/api/pdf_available", methods=["GET"])
def api_pdf_available():
return jsonify({"available": bool(HAS_WEASYPRINT)})
@app.route("/api/export/pdf", methods=["GET"])
def api_export_pdf():
if not HAS_WEASYPRINT:
return Response("WeasyPrint not installed", status=409)
dirpath = request.args.get("dir","").strip()
if not dirpath or not os.path.isdir(dirpath):
return Response("Invalid dir", status=400)
rows = _apply_filters(_load_dir(dirpath), request.args)
foundation = request.args.get("foundation","all")
fnds = sorted(set([r["foundation"] for r in rows])) if foundation=="all" else [foundation]
if len(fnds) != 1 or fnds[0] == "all":
return Response("Select exactly one foundation for PDF export", status=412)
fnd = fnds[0]
scoped = [r for r in rows if r["foundation"]==fnd]
now = dt.datetime.now()
stamp = now.strftime("%Y-%m-%d %H:%M")
def _bucket(n):
if n is None: return 'no-date'
if n <= 30: return '<=30'
if n <= 60: return '<=60'
if n <= 90: return '<=90'
return '>90'
# simple timeline SVG
rows_sorted = sorted(scoped, key=lambda r: (r['days_remaining'] if r['days_remaining'] is not None else 9_000_000))
h = 24; pad = 10; width = 900
height = pad + len(rows_sorted)*(h+8) + pad + 20
max_days = max([r.get('days_remaining') or 0 for r in rows_sorted] + [90])
parts = [f"<svg width='{width}' height='{height}' xmlns='http://www.w3.org/2000/svg'>"]
y = pad + 10
for r in rows_sorted:
days = r.get('days_remaining') or 0
b = _bucket(r.get('days_remaining'))
color = SLA_COLORS.get(b, '#6b7280')
barw = int((days/max_days) * (width - 260)) if max_days else 0
label = f"{r['cert_name']} • {r['version_id_short']}"
parts.append(f"<text x='6' y='{y+16}' font-size='11' fill='#0f172a'>{label}</text>")
parts.append(f"<rect x='260' y='{y}' width='{barw}' height='{h}' fill='{color}' rx='4' ry='4'/>")
parts.append(f"<text x='{260+barw+8}' y='{y+16}' font-size='11' fill='#334155'>{days}d</text>")
y += h + 8
parts.append('</svg>')
svg_timeline = ''.join(parts)
# Runbook (brief)
from io import StringIO
sio = StringIO()
sio.write(f"Certificate Rotation Runbook — {fnd}\nGenerated: {stamp}\n\n")
cas = [r for r in scoped if r['certificate_authority']]
leaves = [r for r in scoped if not r['certificate_authority']]
cas.sort(key=lambda r: (r['days_remaining'] if r['days_remaining'] is not None else 9_000_000))
leaves.sort(key=lambda r: (r['days_remaining'] if r['days_remaining'] is not None else 9_000_000))
sio.write("Phase 1 — CAs\n")
for r in cas: sio.write(f"- {r['cert_name']} `{r['version_id_short']}` — {r['days_remaining']}d (issuer: {r.get('issuer','')})\n")
sio.write("\nPhase 2 — Leaves\n")
for r in leaves: sio.write(f"- {r['cert_name']} `{r['version_id_short']}` — {r['days_remaining']}d (issuer: {r.get('issuer','')})\n")
runbook_pre = f"<pre style='white-space:pre-wrap; font-size:12px; background:#f8fafc; padding:12px; border:1px solid #e5e7eb; border-radius:8px;'>{sio.getvalue()}</pre>"
# Traceability appendix
files = _dir_file_hashes(dirpath)
trace_rows = ''.join([f"<tr><td>{f['file']}</td><td>{f['sha256']}</td><td>{f['size']}</td><td>{dt.datetime.fromtimestamp(f['mtime']).isoformat(sep=' ', timespec='minutes')}</td></tr>" for f in files])
trace_html = f"""
<table style='width:100%; border-collapse:collapse; font-size:12px;'>
<thead>
<tr><th align='left'>File</th><th align='left'>SHA-256</th><th align='right'>Bytes</th><th align='left'>Modified</th></tr>
</thead>
<tbody>{trace_rows}</tbody>
</table>
"""
style = """
<style>
@page { size: A4; margin: 20mm; @bottom-center { content: counter(page); font-size: 10px; } }
body { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif; color:#0f172a; }
h1,h2,h3,h4 { margin: 0 0 8px 0; }
.section { page-break-inside: avoid; margin: 8px 0 18px 0; }
.cover { text-align:center; margin-top: 120px; }
.muted { color:#475569; }
.divider { height:1px; background:#e5e7eb; margin: 12px 0; }
table, th, td { border: 1px solid #e5e7eb; }
th, td { padding: 6px 8px; }
</style>
"""
body = f"""
<html><head>{style}</head><body>
<div class='cover'>
<h1>Certificate Rotation Report</h1>
<h2>{fnd}</h2>
<div class='muted'>Generated {stamp}</div>
</div>
<div class='divider'></div>
<div class='section'>
<h2>Timeline</h2>
{svg_timeline}
</div>
<div class='section'>
<h2>Runbook</h2>
{runbook_pre}
</div>
<div class='section'>
<h2>Traceability Appendix</h2>
<div class='muted'>Hash of input YAMLs at generation time</div>
{trace_html}
</div>
</body></html>
"""
pdf = HTML(string=body).write_pdf()
fname = f"{fnd}-{now.strftime('%Y%m%d-%H%M')}.pdf"
return Response(pdf, mimetype='application/pdf', headers={'Content-Disposition': f'attachment; filename={fname}'})
def _apply_filters(rows: List[Dict[str, Any]], args) -> List[Dict[str, Any]]:
fnd = args.get("foundation", "all")
sla = args.get("sla", "all")
onlyActive = args.get("onlyActive") in ("1","true","True")
onlyCA = args.get("onlyCA") in ("1","true","True")
onlyTrans = args.get("onlyTrans") in ("1","true","True")
out = rows[:]
if fnd != "all":
out = [r for r in out if r["foundation"] == fnd]
if onlyActive:
out = [r for r in out if r["active"]]
if onlyCA:
out = [r for r in out if r["certificate_authority"]]
if onlyTrans:
out = [r for r in out if r["transitional"]]
if sla != "all":
def to_bucket(n):
if n is None: return "no-date"
if n <= 30: return "<=30"
if n <= 60: return "<=60"
if n <= 90: return "<=90"
return ">90"
out = [r for r in out if to_bucket(r["days_remaining"]) == sla]
out.sort(key=lambda r: ((r["days_remaining"] if r["days_remaining"] is not None else 9_000_000),
r["foundation"], r["cert_name"]))
return out
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment