-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy patharchive.html
More file actions
199 lines (182 loc) · 15 KB
/
Copy patharchive.html
File metadata and controls
199 lines (182 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
---
layout: null
permalink: /archive/
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="{{ '/brand/favicon.svg' | relative_url }}" />
<link rel="icon" type="image/png" sizes="32x32" href="{{ '/brand/favicon-32.png' | relative_url }}" />
<link rel="icon" type="image/png" sizes="16x16" href="{{ '/brand/favicon-16.png' | relative_url }}" />
<link rel="apple-touch-icon" href="{{ '/brand/apple-touch-icon.png' | relative_url }}" />
<title>S33R — Archive</title>
<script src="{{ '/ui.js' | relative_url }}"></script>
<link rel="stylesheet" href="{{ '/s33r-feed.css' | relative_url }}" />
<link rel="stylesheet" href="{{ '/s33r-pages.css' | relative_url }}" />
<style>
.arch-controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 16px; }
.arch-controls .grow { flex: 1 1 240px; display: flex; align-items: center; gap: 9px; height: 38px; padding: 0 12px; background: var(--panel); border: 1px solid var(--line-2); border-radius: 10px; }
.arch-controls .grow:focus-within { border-color: var(--brand); }
.arch-controls .grow svg { width: 15px; height: 15px; color: var(--faint); flex: 0 0 auto; }
.arch-controls .grow input { flex: 1; border: none; background: none; outline: none; font-size: 13.5px; color: var(--text); min-width: 0; }
.arch-stats { margin-top: 12px; }
.arch-stats-card { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 11px 14px; font-family: var(--mono); font-size: 11px; color: var(--dim); line-height: 1.7; display: flex; flex-wrap: wrap; gap: 6px 20px; }
.arch-stats-card b { color: var(--text-2); font-weight: 600; }
#archive-results { margin-top: 14px; display: flex; flex-direction: column; gap: 9px; }
.archive-source-header { margin: 12px 0 2px; font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--cyan); }
.archive-item { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 13px 16px; box-shadow: var(--shadow); transition: border-color .14s, transform .14s; }
.archive-item:hover { border-color: var(--line-2); transform: translateY(-1px); }
.archive-date { font-family: var(--mono); font-size: 10px; color: var(--faint); letter-spacing: .04em; margin-bottom: 5px; }
.archive-item a.title { color: var(--text); font-size: 15px; font-weight: 700; line-height: 1.32; letter-spacing: -.01em; }
.archive-item a.title:hover { color: var(--brand); }
.archive-source { font-family: var(--mono); font-size: 10.5px; color: var(--cyan); margin-top: 6px; }
.archive-summary { font-size: 12.5px; color: var(--dim); margin-top: 6px; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.archive-pagination { margin-top: 16px; display: flex; justify-content: center; align-items: center; gap: 12px; font-family: var(--mono); font-size: 11px; color: var(--faint); }
.arch-empty { padding: 30px; text-align: center; color: var(--dim); font-size: 13px; border: 1px dashed var(--line-2); border-radius: var(--radius); }
</style>
<script>window.S33R = { base: "{{ '/' | relative_url }}", data: "{{ '/data' | relative_url }}", feed: "{{ '/data/news_recent.json' | relative_url }}" };</script>
</head>
<body data-page="archive">
<div class="app app--page" id="app">
<main class="page-main">
<div class="crumb">
<span class="lab">SECTION</span>
<span class="here">ARCHIVE</span>
<span class="meta"><span id="crumb-sel">SELECT PERIOD</span></span>
</div>
<div class="page-wrap">
<header class="phead">
<div class="phead-l">
<span class="eyebrow amber">COLD STORAGE</span>
<h1>News <span>Archive</span></h1>
<p class="sub">Browse older security news by year and month, with search, grouping and per-period analytics.</p>
</div>
<div class="phead-actions">
<div class="select-wrap"><select id="year-select" aria-label="Year"><option value="">Select year…</option></select></div>
<div class="select-wrap"><select id="month-select" aria-label="Month" disabled><option value="">Select month…</option></select></div>
</div>
</header>
<div class="arch-controls">
<div class="grow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="search-input" type="text" placeholder="Search archive (title, summary, source)… ( press / )" autocomplete="off" />
</div>
<label class="archive-group-toggle"><input type="checkbox" id="group-by-source" /> Group by source</label>
</div>
<div id="archive-stats" class="arch-stats"></div>
<section class="analyst-tools" aria-label="Archive tools">
<span class="toolbar-label">Archive Tools</span>
<div class="toolbar-inline">
<button id="archive-copy-page-links" class="btn-action" type="button" disabled>Copy page links</button>
<button id="archive-export-page-csv" class="btn-action" type="button" disabled>Export page CSV</button>
<button id="archive-search-cve" class="btn-action" type="button">Search CVE</button>
<button id="archive-clear-search" class="btn-action" type="button">Clear search</button>
</div>
<span class="kbd-hint"><kbd>/</kbd> search</span>
</section>
<div id="archive-results"></div>
<div id="archive-pagination" class="archive-pagination"></div>
<footer class="site-footer">S33R Security News Feed · Powered by JSON cache + public RSS sources</footer>
</div>
</main>
</div>
<script src="{{ '/s33r-nav.js' | relative_url }}"></script>
<script>
const PAGE_SIZE = 25;
const DATA_BASE_URL = "{{ '/data' | relative_url }}";
let archiveIndex = null, currentYear = null, currentMonth = null, currentItems = [], filteredItems = [], currentPage = 1;
const $ = (id) => document.getElementById(id);
function pageItems() {
if (!filteredItems.length) return [];
let p = filteredItems.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
if ($("group-by-source").checked) p = p.slice().sort((a, b) => (a.source || "").localeCompare(b.source || "") || (b.published_ts || 0) - (a.published_ts || 0));
return p;
}
function updateToolBtns() { const has = pageItems().length > 0; $("archive-copy-page-links").disabled = !has; $("archive-export-page-csv").disabled = !has; }
async function copyLinks() { const l = pageItems().map((i) => i.link).filter(Boolean); if (!l.length) return window.S33R_UI?.toast("No links on page"); await window.S33R_UI?.copyText(l.join("\n"), `${l.length} links copied`); }
function exportCsv() {
const items = pageItems(); if (!items.length) return window.S33R_UI?.toast("No items on page");
const e = (v) => `"${String(v ?? "").replace(/"/g, '""')}"`;
const rows = [["title","source","published","link","summary"].map(e).join(","), ...items.map((i) => [i.title || "", i.source || "", fmtDate(i), i.link || "", i.summary || ""].map(e).join(","))];
window.S33R_UI?.downloadText(`s33r_archive_${currentYear}-${currentMonth}_page${currentPage}.csv`, rows.join("\n"), "text/csv;charset=utf-8");
window.S33R_UI?.toast("Archive CSV exported");
}
async function fetchJSON(p) { try { const r = await fetch(p, { cache: "no-store" }); return r.ok ? await r.json() : null; } catch { return null; } }
async function loadIndex() { const a = await fetchJSON(`${DATA_BASE_URL}/archive/archive_index.json`); if (a?.years) return (archiveIndex = a); const b = await fetchJSON(`${DATA_BASE_URL}/archive/index.json`); if (b?.years) return (archiveIndex = b); return (archiveIndex = null); }
function fmtDate(i) { if (typeof i.published_ts === "number") return new Date(i.published_ts * 1000).toLocaleString(); if (i.published) { const d = new Date(i.published); if (!isNaN(d)) return d.toLocaleString(); } return "Unknown date"; }
function computeStats(items) {
const el = $("archive-stats"); if (!items.length) { el.innerHTML = ""; return; }
const sc = {}; let min = Infinity, max = -Infinity;
items.forEach((it) => { const s = it.source || "Unknown"; sc[s] = (sc[s] || 0) + 1; let ts = typeof it.published_ts === "number" ? it.published_ts * 1000 : (it.published ? new Date(it.published).getTime() : null); if (ts && !isNaN(ts)) { if (ts < min) min = ts; if (ts > max) max = ts; } });
const top = Object.entries(sc).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([s, c]) => `${s} (${c})`).join(", ");
el.innerHTML = `<div class="arch-stats-card"><span><b>Total items:</b> ${items.length}</span><span><b>Sources:</b> ${Object.keys(sc).length}</span><span><b>Range:</b> ${isFinite(min) ? new Date(min).toLocaleDateString() : "?"} → ${isFinite(max) ? new Date(max).toLocaleDateString() : "?"}</span><span style="flex-basis:100%"><b>Top sources:</b> ${top || "N/A"}</span></div>`;
}
function applyFilters() {
const q = $("search-input").value.trim().toLowerCase();
filteredItems = !currentItems.length ? [] : !q ? currentItems.slice() : currentItems.filter((it) => [(it.title || ""), (it.summary || ""), (it.source || "")].join(" ").toLowerCase().includes(q));
filteredItems.sort((a, b) => (b.published_ts || 0) - (a.published_ts || 0));
currentPage = 1;
}
function buildItem(it) {
const div = document.createElement("div"); div.className = "archive-item";
div.innerHTML = `<div class="archive-date">${fmtDate(it)}</div><a class="title" href="${it.link}" target="_blank" rel="noopener">${it.title || "Untitled"}</a>${it.source ? `<div class="archive-source">${it.source}</div>` : ""}${it.summary ? `<div class="archive-summary">${it.summary}</div>` : ""}`;
return div;
}
function renderResults() {
const c = $("archive-results"), group = $("group-by-source").checked; c.innerHTML = "";
if (!filteredItems.length) { c.innerHTML = '<div class="arch-empty">No items for this selection.</div>'; $("archive-pagination").innerHTML = ""; computeStats([]); updateToolBtns(); return; }
computeStats(filteredItems);
const totalPages = Math.max(1, Math.ceil(filteredItems.length / PAGE_SIZE)); if (currentPage > totalPages) currentPage = totalPages;
let items = filteredItems.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
if (group) { items = items.slice().sort((a, b) => (a.source || "").localeCompare(b.source || "") || (b.published_ts || 0) - (a.published_ts || 0)); let last = null; items.forEach((it) => { const s = it.source || "Unknown"; if (s !== last) { const h = document.createElement("div"); h.className = "archive-source-header"; h.textContent = s; c.appendChild(h); last = s; } c.appendChild(buildItem(it)); }); }
else items.forEach((it) => c.appendChild(buildItem(it)));
renderPagination(filteredItems.length); updateToolBtns();
}
function renderPagination(total) {
const el = $("archive-pagination"); if (total <= PAGE_SIZE) { el.innerHTML = ""; updateToolBtns(); return; }
const tp = Math.max(1, Math.ceil(total / PAGE_SIZE));
el.innerHTML = `<button class="btn-action" id="page-prev" ${currentPage <= 1 ? "disabled" : ""}>← Prev</button><span>Page ${currentPage} / ${tp} · ${total} items</span><button class="btn-action" id="page-next" ${currentPage >= tp ? "disabled" : ""}>Next →</button>`;
$("page-prev").onclick = () => { if (currentPage > 1) { currentPage--; renderResults(); } };
$("page-next").onclick = () => { if (currentPage < tp) { currentPage++; renderResults(); } };
}
async function loadYears() {
const sel = $("year-select"); const idx = await loadIndex();
if (idx?.years?.length) { idx.years.map((y) => Number(y.year)).filter(Number.isInteger).sort((a, b) => b - a).forEach((y) => { const o = document.createElement("option"); o.value = o.textContent = String(y); sel.appendChild(o); }); }
}
function loadMonths(year) {
const ms = $("month-select"); ms.innerHTML = '<option value="">Select month…</option>';
const meta = archiveIndex?.years?.find((y) => Number(y.year) === Number(year));
let months = meta?.months?.length ? meta.months.map((m) => ({ month: String(m.month).padStart(2, "0"), count: Number(m.item_count || 0) })).sort((a, b) => a.month.localeCompare(b.month)) : [];
months.forEach(({ month, count }) => { const o = document.createElement("option"); o.value = month; o.textContent = Number.isFinite(count) ? `${month} (${count})` : month; ms.appendChild(o); });
ms.disabled = months.length === 0;
}
async function loadArchive(year, month) {
const c = $("archive-results"); c.innerHTML = '<div class="loading"><div class="spinner"></div><span>Loading…</span></div>';
$("crumb-sel").textContent = `${year}-${month}`;
const data = await fetchJSON(`${DATA_BASE_URL}/archive/monthly/${year}/${year}-${month}.json`);
currentYear = year; currentMonth = month; currentItems = Array.isArray(data) ? data : [];
applyFilters(); renderResults();
}
$("year-select").addEventListener("change", (e) => { const y = e.target.value; if (!y) return; loadMonths(y); currentYear = y; currentMonth = null; currentItems = []; filteredItems = []; $("archive-results").innerHTML = ""; $("archive-stats").innerHTML = ""; $("archive-pagination").innerHTML = ""; });
$("month-select").addEventListener("change", (e) => { const m = e.target.value, y = $("year-select").value; if (y && m) loadArchive(y, m); });
$("search-input").addEventListener("input", () => { applyFilters(); renderResults(); });
$("group-by-source").addEventListener("change", renderResults);
$("archive-copy-page-links").addEventListener("click", copyLinks);
$("archive-export-page-csv").addEventListener("click", exportCsv);
$("archive-search-cve").addEventListener("click", () => { const el = $("search-input"); el.value = el.value.trim().toLowerCase() === "cve-" ? "" : "CVE-"; applyFilters(); renderResults(); el.focus(); });
$("archive-clear-search").addEventListener("click", () => { const el = $("search-input"); el.value = ""; applyFilters(); renderResults(); el.focus(); });
document.addEventListener("keydown", (e) => { if (e.key === "/" && !["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)) { e.preventDefault(); $("search-input").focus(); $("search-input").select(); } });
updateToolBtns();
loadYears().then(async () => {
const ys = $("year-select");
if (archiveIndex?.latest?.year && archiveIndex?.latest?.month) {
ys.value = String(archiveIndex.latest.year); loadMonths(String(archiveIndex.latest.year));
const m = String(archiveIndex.latest.month).padStart(2, "0"); $("month-select").value = m;
await loadArchive(String(archiveIndex.latest.year), m);
}
});
</script>
</body>
</html>