diff --git a/index.html b/index.html index 8631eb4..6d9aaf9 100644 --- a/index.html +++ b/index.html @@ -104,6 +104,7 @@

+ Try: @@ -128,41 +129,57 @@

- -
- - + +
-
`, url, []); + + addToHistory(url, 'SAFE', { + title: 'URL is Safe', + desc: `No threats detected. Google Safe Browsing found no issues.

+
+
+ ${isHttps ? '✅' : '⚠️'} HTTPS: ${isHttps ? 'Secure connection' : 'Not secure — use with caution'} +
+
+ ${hasMalware ? '🔴' : '✅'} Malware: ${hasMalware ? 'Detected!' : 'No malware detected'} +
+
+ ${hasPhishing ? '🔴' : '✅'} Phishing: ${hasPhishing ? 'Phishing detected!' : 'No phishing detected'} +
+
+ ${hasUnwanted ? '🔴' : '✅'} Unwanted Software: ${hasUnwanted ? 'Detected!' : 'None detected'} +
+
+ ${hasHarmful ? '🔴' : '✅'} Harmful App: ${hasHarmful ? 'Detected!' : 'None detected'} +
+
`, + threats: [], + resultType: 'safe' + }); + } + } catch (err) { showResult('error', 'Scan Error', `An unexpected error occurred.
Error: ${err.message}`, '', []); + // Store attempted URL only if it passed initial validation + addToHistory(input, 'ERROR', { + title: 'Scan Error', + desc: `An unexpected error occurred.
Error: ${err.message}`, + threats: [], + resultType: 'error' + }); + } finally { + btn.disabled = false; } @@ -657,10 +717,210 @@ async function downloadPDF() { pdf.addImage(imgData, 'PNG', 0, 20, pageWidth, imgHeight); pdf.save('cybershield-report.pdf'); } +// ───────────────────────────── +// HISTORY PANEL (localStorage) +// ───────────────────────────── + +const HISTORY_KEY = 'cybershield_scan_history_v1'; + +function safeParseJson(value, fallback) { + try { + return JSON.parse(value); + } catch { + return fallback; + } +} + +function normalizeUrlForHistory(urlString) { + try { + const input = (urlString || '').trim(); + if (!input) return ''; + const withProto = + input.startsWith('http://') || input.startsWith('https://') + ? input + : 'https://' + input; + const u = new URL(withProto); + // Remove trailing slash to reduce duplicates + const path = (u.pathname && u.pathname !== '/') ? u.pathname : ''; + const query = u.search ? u.search : ''; + const hash = ''; + return `${u.protocol}//${u.hostname}${path}${query}${hash}`; + } catch { + // If normalization fails, use raw trimmed input + return (urlString || '').trim(); + } +} + +function loadHistory() { + const raw = localStorage.getItem(HISTORY_KEY); + const arr = safeParseJson(raw, []); + if (!Array.isArray(arr)) return []; + return arr; +} + +function saveHistory(history) { + localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); +} + +function renderHistory() { + const listEl = document.getElementById('historyList'); + const metaEl = document.getElementById('historyMeta'); + if (!listEl) return; + + const history = loadHistory(); + + if (!history.length) { + if (metaEl) metaEl.textContent = 'No scans yet'; + listEl.innerHTML = ''; + return; + } + + if (metaEl) metaEl.textContent = `${history.length} saved scan${history.length === 1 ? '' : 's'}`; + + // newest first + const view = [...history].sort((a, b) => (b.at || 0) - (a.at || 0)); + listEl.innerHTML = view.map(item => { + const at = item.at ? new Date(item.at).toLocaleString() : ''; + const status = item.status || 'UNKNOWN'; + return ` +
+ + +
${at}
+
+ `; + }).join(''); +} + +function addToHistory(url, status, reportPayload = null) { + if (!url) return; + + const normalized = normalizeUrlForHistory(url); + + if (!normalized) return; + + const history = loadHistory(); + + // De-dup by normalized URL + const existingIndex = history.findIndex(h => normalizeUrlForHistory(h.url) === normalized); + + // Merge existing payload with the newly provided one (if any) + const existing = existingIndex >= 0 ? history[existingIndex] : null; + const entry = { + url: url.trim(), + normalized, + at: Date.now(), + status: (status || '').toUpperCase(), + // Merge existing payload with the newly provided one (if any) + title: reportPayload?.title ?? existing?.title ?? null, + desc: reportPayload?.desc ?? existing?.desc ?? null, + threats: reportPayload?.threats ?? existing?.threats ?? null, + resultType: + reportPayload?.resultType ?? existing?.resultType ?? (status || '').toLowerCase() + }; + + + if (existingIndex >= 0) { + history[existingIndex] = entry; + } else { + history.push(entry); + } + + saveHistory(history); + renderHistory(); +} + +function loadHistoryReport(urlString) { + const history = loadHistory(); + if (!history.length) return; + + const normalized = normalizeUrlForHistory(urlString); + const found = history.find(h => normalizeUrlForHistory(h.url) === normalized); + if (!found) return; + + // Close/hide any previous loading animations quickly + const riskEl = document.getElementById('riskAnalysis'); + if (riskEl) riskEl.classList.remove('hidden'); + + // Make the result update feel instant + const resultEl = document.getElementById('result'); + // 'instant' isn't reliably supported everywhere; use auto for consistent UX. + if (resultEl) resultEl.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + + + + + // If we have stored a full report payload, restore it. + // Otherwise, at minimum fill input + show a friendly message. + if (found.title && found.desc != null) { + document.getElementById('urlInput').value = found.url; + + // Restore result view without altering history list. + showResult( + found.resultType === 'danger' ? 'danger' : 'safe', + found.title, + found.desc, + found.url, + found.threats || [] + ); + + // Ensure risk section becomes visible for safe/danger + document.getElementById('riskAnalysis')?.classList.remove('hidden'); + return; + } + + // Backward compatibility fallback + fillExample(found.url); + showResult( + 'safe', + 'Report loaded', + `Saved scan metadata found, but full details weren\'t available in this history entry.

Please rescan to regenerate full report details for this URL.`, + found.url, + [] + ); +} + + +document.addEventListener('DOMContentLoaded', () => { + renderHistory(); + + // Real-time-ish update for history timestamps (every 5s) + const startTsUpdater = () => { + const listEl = document.getElementById('historyList'); + if (!listEl) return; + + const tick = () => { + const history = loadHistory(); + if (!history.length) return; + + const view = [...history].sort((a, b) => (b.at || 0) - (a.at || 0)); + const items = listEl.querySelectorAll('.history-item'); + + items.forEach((el, idx) => { + const item = view[idx]; + if (!item) return; + const at = item.at ? new Date(item.at).toLocaleString() : ''; + const atEl = el.querySelector('.history-at'); + if (atEl) atEl.textContent = at; + }); + }; + + tick(); + window.setInterval(tick, 5000); + }; + + startTsUpdater(); +}); + + // ───────────────────────────── // THEME TOGGLE // ───────────────────────────── + (function initTheme() { const btn = document.getElementById('themeToggle'); diff --git a/style.css b/style.css index bf46d56..15d6d12 100644 --- a/style.css +++ b/style.css @@ -1285,4 +1285,149 @@ input[type="text"].input-error { @media (max-width: 380px) { .team-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} + +/* ── Scanner 2-column layout (main + history) ── */ +.scanner-layout { + display: grid; + grid-template-columns: 1fr; + gap: 20px; +} + +@media (min-width: 900px) { + /* Two columns: full-width main (left) + fixed history (right) */ + .scanner-layout { + grid-template-columns: minmax(0, 1fr) 280px; + align-items: start; + } + + /* Force deterministic placement */ + .scanner-main { + grid-column: 1 !important; + justify-self: stretch; + } + + aside.history-panel { + grid-column: 2 !important; + justify-self: stretch; + width: 100%; + } + + /* Ensure result card fills the available left column width */ + .scanner-main #result, + .scanner-main #riskAnalysis { + width: 100%; + } + + .scanner-main #result .result-card { + width: 100%; + } +} + + + + +/* ── History Panel ── */ +.history-panel { + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 16px; + padding: 16px; + min-width: 0; + + /* Keep buttons clickable and avoid being covered */ + position: relative; + z-index: 10; +} + +.history-list { + max-height: 320px; + overflow: auto; + padding-right: 6px; + display: flex; + flex-direction: column; + gap: 10px; +} + +@media (max-width: 600px) { + .history-list { + max-height: 220px; + } +} + +.history-header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + margin-bottom: 12px; +} + +.history-panel h3 { + font-size: 14px; + color: var(--accent-1); + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.history-meta { + font-size: 12px; + color: var(--subtitle-color); +} + +.history-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-radius: 12px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); +} + +.history-item:hover { + border-color: var(--accent-1); + box-shadow: 0 10px 24px rgba(0,0,0,0.08); +} + +.history-item button { + background: none; + border: 0; + padding: 0; + margin: 0; + cursor: pointer; + color: inherit; + text-align: left; + flex: 1; +} + +.history-item button:focus-visible { + outline: 2px solid rgba(var(--accent-1-rgb), 0.65); + outline-offset: 2px; + border-radius: 8px; +} + +.history-url { + font-family: 'Courier New', monospace; + font-size: 12px; + color: var(--text-color); + word-break: break-word; + line-height: 1.4; +} + +.history-at { + font-size: 11px; + color: var(--subtitle-color); + white-space: nowrap; +} + +/* ── Ensure the long URL report spans wide (until Scan button width) ── */ +#result .result-url { + display: block; + width: 100%; + box-sizing: border-box; + max-width: 100%; +} + +