mirror of
https://github.com/momenbasel/keyFinder.git
synced 2026-07-25 06:00:49 +02:00
v2.2.0: ops-console UI redesign
- Popup + findings dashboard rebuilt as a dark ops console: radar-sweep brand mark, ARMED/PASSIVE status LEDs, scanline texture, mono chrome, amber (action) / teal (secret) semantic colors - Popup: count-up stats, numbered watchlist, command-prompt input, typed terminal status line with live counts - Dashboard: clickable severity stat filters, LATEST ticker, severity row edges, LED badges, click-to-copy match chips, relative times, skeleton rows, staggered first paint, radar empty state, / and Esc shortcuts, prefers-reduced-motion support - Fix severity sort (critical sorted last) and ticker hidden override - Add preview/ harness (stubbed chrome API) for headless UI screenshots
This commit is contained in:
+55
-4
@@ -1,23 +1,71 @@
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
const REDUCE_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
let lastKeywords = 0;
|
||||
let lastFindings = 0;
|
||||
let termBooted = false;
|
||||
|
||||
async function init() {
|
||||
const versionLabel = document.getElementById("versionLabel");
|
||||
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||||
await renderKeywords();
|
||||
await renderStats();
|
||||
updateTermLine(true);
|
||||
termBooted = true;
|
||||
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
||||
}
|
||||
|
||||
/* eased count-up for stat numerals */
|
||||
function setNumber(el, target) {
|
||||
if (!el) return;
|
||||
const from = parseInt(el.textContent, 10);
|
||||
const start = Number.isFinite(from) ? from : 0;
|
||||
if (REDUCE_MOTION || start === target) {
|
||||
el.textContent = target;
|
||||
return;
|
||||
}
|
||||
const duration = 380;
|
||||
const t0 = performance.now();
|
||||
(function tick(t) {
|
||||
const p = Math.min(1, (t - t0) / duration);
|
||||
const eased = 1 - Math.pow(1 - p, 3);
|
||||
el.textContent = Math.round(start + (target - start) * eased);
|
||||
if (p < 1) requestAnimationFrame(tick);
|
||||
})(t0);
|
||||
}
|
||||
|
||||
/* terminal status line — types once per popup open, instant after */
|
||||
function updateTermLine(typed) {
|
||||
const el = document.getElementById("termText");
|
||||
if (!el) return;
|
||||
const text = `kf> scan armed :: ${lastKeywords} keywords :: ${lastFindings} findings`;
|
||||
if (!typed || REDUCE_MOTION) {
|
||||
el.textContent = text;
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
(function type() {
|
||||
el.textContent = text.slice(0, ++i);
|
||||
if (i < text.length) setTimeout(type, 14);
|
||||
})();
|
||||
}
|
||||
|
||||
async function renderKeywords() {
|
||||
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
|
||||
const keywords = response.keywords || [];
|
||||
const list = document.getElementById("keywordList");
|
||||
list.innerHTML = "";
|
||||
|
||||
document.getElementById("keywordCount").textContent = keywords.length;
|
||||
lastKeywords = keywords.length;
|
||||
setNumber(document.getElementById("keywordCount"), keywords.length);
|
||||
if (termBooted) updateTermLine(false);
|
||||
|
||||
if (keywords.length === 0) {
|
||||
list.innerHTML = '<li class="empty-state">No keywords configured</li>';
|
||||
const empty = document.createElement("li");
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = "no keywords configured";
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,7 +79,7 @@ async function renderKeywords() {
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "keyword-remove";
|
||||
removeBtn.textContent = "\u00D7";
|
||||
removeBtn.textContent = "×";
|
||||
removeBtn.title = `Remove "${kw}"`;
|
||||
removeBtn.addEventListener("click", () => handleRemoveKeyword(kw));
|
||||
|
||||
@@ -44,7 +92,9 @@ async function renderKeywords() {
|
||||
async function renderStats() {
|
||||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||
const findings = response.findings || [];
|
||||
document.getElementById("findingCount").textContent = findings.length;
|
||||
lastFindings = findings.length;
|
||||
setNumber(document.getElementById("findingCount"), findings.length);
|
||||
if (termBooted) updateTermLine(false);
|
||||
}
|
||||
|
||||
async function handleAddKeyword(e) {
|
||||
@@ -68,6 +118,7 @@ async function handleAddKeyword(e) {
|
||||
}
|
||||
|
||||
input.value = "";
|
||||
input.focus();
|
||||
await renderKeywords();
|
||||
}
|
||||
|
||||
|
||||
+191
-22
@@ -1,16 +1,24 @@
|
||||
let allFindings = [];
|
||||
let firstRenderDone = false;
|
||||
|
||||
const REDUCE_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
async function init() {
|
||||
const versionLabel = document.getElementById("versionLabel");
|
||||
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||||
|
||||
renderSkeleton();
|
||||
|
||||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||
allFindings = response.findings || [];
|
||||
|
||||
populateFilters();
|
||||
renderStats();
|
||||
renderFindings();
|
||||
renderTicker();
|
||||
firstRenderDone = true;
|
||||
|
||||
document.getElementById("severityFilter").addEventListener("change", renderFindings);
|
||||
document.getElementById("typeFilter").addEventListener("change", renderFindings);
|
||||
@@ -19,6 +27,57 @@ async function init() {
|
||||
document.getElementById("exportJsonBtn").addEventListener("click", exportJson);
|
||||
document.getElementById("exportCsvBtn").addEventListener("click", exportCsv);
|
||||
document.getElementById("clearBtn").addEventListener("click", clearAll);
|
||||
|
||||
const searchBox = document.getElementById("searchBox");
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const tag = document.activeElement && document.activeElement.tagName;
|
||||
const typing = tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA";
|
||||
if (e.key === "/" && !typing) {
|
||||
e.preventDefault();
|
||||
searchBox.focus();
|
||||
} else if (e.key === "Escape" && document.activeElement === searchBox) {
|
||||
searchBox.value = "";
|
||||
renderFindings();
|
||||
searchBox.blur();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eased count-up for stat numerals */
|
||||
function setNumber(el, target) {
|
||||
if (!el) return;
|
||||
const from = parseInt(el.textContent, 10);
|
||||
const start = Number.isFinite(from) ? from : 0;
|
||||
if (REDUCE_MOTION || start === target) {
|
||||
el.textContent = target;
|
||||
return;
|
||||
}
|
||||
const duration = 400;
|
||||
const t0 = performance.now();
|
||||
(function tick(t) {
|
||||
const p = Math.min(1, (t - t0) / duration);
|
||||
const eased = 1 - Math.pow(1 - p, 3);
|
||||
el.textContent = Math.round(start + (target - start) * eased);
|
||||
if (p < 1) requestAnimationFrame(tick);
|
||||
})(t0);
|
||||
}
|
||||
|
||||
function renderSkeleton() {
|
||||
const tbody = document.getElementById("findingsBody");
|
||||
tbody.classList.remove("fresh");
|
||||
tbody.innerHTML = "";
|
||||
const widths = ["skl-w2", "skl-w1", "skl-w3", "skl-w4", "skl-w2", "skl-w3"];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "skl-row";
|
||||
const td = document.createElement("td");
|
||||
td.colSpan = 10;
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "skl " + widths[i % widths.length];
|
||||
td.appendChild(bar);
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function getFiltered() {
|
||||
@@ -67,54 +126,96 @@ function renderStats() {
|
||||
|
||||
bar.innerHTML = "";
|
||||
const stats = [
|
||||
{ label: "Total", value: allFindings.length, cls: "stat-total" },
|
||||
{ label: "Critical", value: critical, cls: "stat-critical" },
|
||||
{ label: "High", value: high, cls: "stat-high" },
|
||||
{ label: "Medium", value: medium, cls: "stat-medium" },
|
||||
{ label: "Low", value: low, cls: "stat-low" },
|
||||
{ label: "Domains", value: domains, cls: "stat-domains" },
|
||||
{ label: "Total", value: allFindings.length, cls: "stat-total", filter: "all" },
|
||||
{ label: "Critical", value: critical, cls: "stat-critical", filter: "critical" },
|
||||
{ label: "High", value: high, cls: "stat-high", filter: "high" },
|
||||
{ label: "Medium", value: medium, cls: "stat-medium", filter: "medium" },
|
||||
{ label: "Low", value: low, cls: "stat-low", filter: "low" },
|
||||
{ label: "Domains", value: domains, cls: "stat-domains", filter: null },
|
||||
];
|
||||
for (const s of stats) {
|
||||
const el = document.createElement("div");
|
||||
el.className = `stat-item ${s.cls}`;
|
||||
const num = document.createElement("span");
|
||||
num.className = "stat-num";
|
||||
num.textContent = s.value;
|
||||
|
||||
const top = document.createElement("span");
|
||||
top.className = "stat-top";
|
||||
const led = document.createElement("span");
|
||||
led.className = "stat-led";
|
||||
const lbl = document.createElement("span");
|
||||
lbl.className = "stat-lbl";
|
||||
lbl.textContent = s.label;
|
||||
top.appendChild(led);
|
||||
top.appendChild(lbl);
|
||||
|
||||
const num = document.createElement("span");
|
||||
num.className = "stat-num";
|
||||
|
||||
el.appendChild(top);
|
||||
el.appendChild(num);
|
||||
el.appendChild(lbl);
|
||||
setNumber(num, s.value);
|
||||
|
||||
if (s.filter) {
|
||||
el.dataset.filter = s.filter;
|
||||
el.title = s.filter === "all" ? "Show all severities" : `Filter: ${s.label.toLowerCase()} only`;
|
||||
el.addEventListener("click", () => {
|
||||
document.getElementById("severityFilter").value = s.filter;
|
||||
renderFindings();
|
||||
});
|
||||
}
|
||||
bar.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
function syncStatActive() {
|
||||
const current = document.getElementById("severityFilter").value;
|
||||
document.querySelectorAll(".stat-item[data-filter]").forEach((el) => {
|
||||
el.classList.toggle("active", el.dataset.filter === current);
|
||||
});
|
||||
}
|
||||
|
||||
function renderFindings() {
|
||||
const filtered = getFiltered();
|
||||
const tbody = document.getElementById("findingsBody");
|
||||
const empty = document.getElementById("emptyState");
|
||||
|
||||
tbody.innerHTML = "";
|
||||
tbody.classList.toggle("fresh", !firstRenderDone && !REDUCE_MOTION);
|
||||
|
||||
updateMeta(filtered.length);
|
||||
syncStatActive();
|
||||
|
||||
if (filtered.length === 0) {
|
||||
const title = document.getElementById("emptyTitle");
|
||||
const copy = document.getElementById("emptyCopy");
|
||||
if (allFindings.length === 0) {
|
||||
title.textContent = "No findings yet";
|
||||
copy.textContent = "KeyFinder passively scans every page you visit. Browse around — leaked keys, tokens and credentials will surface here.";
|
||||
} else {
|
||||
title.textContent = "No matches in view";
|
||||
copy.textContent = "The current filters hide every finding. Adjust or clear them to bring results back.";
|
||||
}
|
||||
empty.hidden = false;
|
||||
return;
|
||||
}
|
||||
empty.hidden = true;
|
||||
|
||||
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
||||
filtered.sort((a, b) => (severityOrder[a.severity] || 5) - (severityOrder[b.severity] || 5));
|
||||
filtered.sort((a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5));
|
||||
|
||||
filtered.forEach((f, i) => {
|
||||
const sev = f.severity || "medium";
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "sev-" + sev;
|
||||
if (i < 30) tr.style.setProperty("--i", i);
|
||||
|
||||
const tdNum = document.createElement("td");
|
||||
tdNum.textContent = i + 1;
|
||||
tdNum.textContent = String(i + 1).padStart(2, "0");
|
||||
tdNum.className = "td-num";
|
||||
|
||||
const tdSev = document.createElement("td");
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `badge badge-${f.severity || "medium"}`;
|
||||
badge.textContent = (f.severity || "medium").toUpperCase();
|
||||
badge.className = `badge badge-${sev}`;
|
||||
badge.textContent = sev.toUpperCase();
|
||||
tdSev.appendChild(badge);
|
||||
|
||||
const tdProvider = document.createElement("td");
|
||||
@@ -129,7 +230,11 @@ function renderFindings() {
|
||||
const matchCode = document.createElement("code");
|
||||
matchCode.textContent = f.match || "-";
|
||||
matchCode.className = "match-value";
|
||||
matchCode.title = f.match || "";
|
||||
matchCode.title = "click to copy\n" + (f.match || "");
|
||||
matchCode.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(f.match || "");
|
||||
flashCopied(copyBtn);
|
||||
});
|
||||
tdMatch.appendChild(matchCode);
|
||||
|
||||
const tdType = document.createElement("td");
|
||||
@@ -143,42 +248,45 @@ function renderFindings() {
|
||||
tdDomain.className = "td-domain";
|
||||
|
||||
const tdSource = document.createElement("td");
|
||||
tdSource.className = "td-source";
|
||||
if (f.url && f.url.startsWith("http")) {
|
||||
const a = document.createElement("a");
|
||||
a.href = f.url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener";
|
||||
a.textContent = truncateUrl(f.url, 40);
|
||||
a.textContent = truncateUrl(f.url, 30);
|
||||
a.title = f.url;
|
||||
tdSource.appendChild(a);
|
||||
} else {
|
||||
tdSource.textContent = f.url ? truncateUrl(f.url, 40) : "-";
|
||||
tdSource.textContent = f.url ? truncateUrl(f.url, 30) : "-";
|
||||
}
|
||||
|
||||
const tdTime = document.createElement("td");
|
||||
tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-";
|
||||
tdTime.className = "td-time";
|
||||
if (f.timestamp) tdTime.title = new Date(f.timestamp).toLocaleString();
|
||||
|
||||
const tdActions = document.createElement("td");
|
||||
tdActions.className = "td-actions";
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "btn-icon";
|
||||
copyBtn.textContent = "Copy";
|
||||
copyBtn.textContent = "copy";
|
||||
copyBtn.title = "Copy match value";
|
||||
copyBtn.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(f.match || "");
|
||||
copyBtn.textContent = "Done";
|
||||
setTimeout(() => (copyBtn.textContent = "Copy"), 1500);
|
||||
flashCopied(copyBtn);
|
||||
});
|
||||
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "btn-icon btn-icon-danger";
|
||||
delBtn.textContent = "Del";
|
||||
delBtn.textContent = "del";
|
||||
delBtn.title = "Remove finding";
|
||||
delBtn.addEventListener("click", async () => {
|
||||
await chrome.runtime.sendMessage({ type: "removeFinding", findingId: f.id });
|
||||
allFindings = allFindings.filter((x) => x.id !== f.id);
|
||||
renderStats();
|
||||
renderFindings();
|
||||
renderTicker();
|
||||
});
|
||||
|
||||
tdActions.appendChild(copyBtn);
|
||||
@@ -198,6 +306,58 @@ function renderFindings() {
|
||||
});
|
||||
}
|
||||
|
||||
function flashCopied(btn) {
|
||||
if (!btn) return;
|
||||
btn.textContent = "copied";
|
||||
btn.classList.add("copied");
|
||||
setTimeout(() => {
|
||||
btn.textContent = "copy";
|
||||
btn.classList.remove("copied");
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
function updateMeta(shown) {
|
||||
const metaLine = document.getElementById("metaLine");
|
||||
if (metaLine) {
|
||||
metaLine.innerHTML = "";
|
||||
metaLine.append(
|
||||
"showing ",
|
||||
strong(shown),
|
||||
" of ",
|
||||
strong(allFindings.length)
|
||||
);
|
||||
}
|
||||
const shellMeta = document.getElementById("shellMeta");
|
||||
if (shellMeta) shellMeta.textContent = shown + (shown === 1 ? " row" : " rows");
|
||||
}
|
||||
|
||||
function strong(n) {
|
||||
const el = document.createElement("strong");
|
||||
el.textContent = n;
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderTicker() {
|
||||
const ticker = document.getElementById("ticker");
|
||||
const tickerText = document.getElementById("tickerText");
|
||||
if (!ticker || !tickerText) return;
|
||||
if (!allFindings.length) {
|
||||
ticker.hidden = true;
|
||||
return;
|
||||
}
|
||||
const latest = [...allFindings].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
|
||||
tickerText.innerHTML = "";
|
||||
const head = document.createElement("span");
|
||||
head.textContent = `${latest.provider || "unknown"} · ${latest.patternName || "match"} @ ${latest.domain || "—"} `;
|
||||
tickerText.appendChild(head);
|
||||
if (latest.match) {
|
||||
const code = document.createElement("code");
|
||||
code.textContent = truncateUrl(latest.match, 28);
|
||||
tickerText.appendChild(code);
|
||||
}
|
||||
ticker.hidden = false;
|
||||
}
|
||||
|
||||
function truncateUrl(url, max) {
|
||||
if (url.length <= max) return url;
|
||||
return url.substring(0, max - 3) + "...";
|
||||
@@ -205,7 +365,15 @@ function truncateUrl(url, max) {
|
||||
|
||||
function formatTime(ts) {
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
const diff = Date.now() - d.getTime();
|
||||
const m = Math.floor(diff / 60000);
|
||||
if (m < 1) return "just now";
|
||||
if (m < 60) return m + "m ago";
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return h + "h ago";
|
||||
const days = Math.floor(h / 24);
|
||||
if (days < 7) return days + "d ago";
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
function exportJson() {
|
||||
@@ -255,4 +423,5 @@ async function clearAll() {
|
||||
allFindings = [];
|
||||
renderStats();
|
||||
renderFindings();
|
||||
renderTicker();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user