jaeswift-website/js/scan-visitor.js

306 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ===================================================
JAESWIFT.XYZ — Scan The Visitor
Typewriter-animated visitor recon overlay.
=================================================== */
(function () {
'use strict';
const DISMISS_KEY = 'scanDismissed';
const DISMISS_DAYS = 7;
const BADGE_HIDE_KEY = 'scanBadgeHidden';
const TYPE_SPEED = 22; // ms per char
const LINE_PAUSE = 120;
// ─── Check if dismissed recently ────────────────
function isDismissed() {
try {
const raw = localStorage.getItem(DISMISS_KEY);
if (!raw) return false;
const ts = parseInt(raw, 10);
if (!ts) return false;
const ageDays = (Date.now() - ts) / (1000 * 60 * 60 * 24);
return ageDays < DISMISS_DAYS;
} catch (e) { return false; }
}
function setDismissed() {
try { localStorage.setItem(DISMISS_KEY, Date.now().toString()); } catch (e) {}
}
function isBadgeHidden() {
try { return localStorage.getItem(BADGE_HIDE_KEY) === '1'; } catch (e) { return false; }
}
// ─── Gather local client info ───────────────────
function localInfo() {
const scr = `${screen.width}x${screen.height}`;
const dpr = (window.devicePixelRatio || 1);
let conn = 'Unknown';
try {
const c = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (c) {
const t = c.effectiveType || c.type || '';
conn = t ? t.toUpperCase() : 'Unknown';
if (c.downlink) conn += `, ${c.downlink}Mbps`;
if (c.rtt) conn += `, ${c.rtt}ms`;
}
} catch (e) {}
return {
screen: `${scr} @ ${dpr}x`,
connection: conn,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
lang: navigator.language || 'en',
};
}
// ─── Build scan lines list ──────────────────────
function buildLines(data, local) {
const locStr = data.city ? `${data.city}, ${data.country}` : data.country;
const browser = data.browser + (data.browser_version ? ' ' + data.browser_version : '');
const osStr = data.os + (data.os_version ? ' ' + data.os_version : '');
const flag = data.country_flag || '';
const threatColor = data.threat_level === 'GREEN' ? 'green'
: data.threat_level === 'AMBER' ? 'amber'
: 'red';
const bar = (() => {
if (data.threat_level === 'GREEN') return '[█░░░░] GREEN — TRUSTED';
if (data.threat_level === 'AMBER') return '[███░░] AMBER — MONITORED';
if (data.threat_level === 'RED') return '[█████] RED — BLOCKED';
return '[░░░░░] UNKNOWN';
})();
return [
{ cls: 'green', text: '>>> ESTABLISHING SECURE CONNECTION...' },
{ cls: 'green', text: '>>> HANDSHAKE OK · TLS 1.3 · AES-256-GCM' },
{ cls: 'green', text: '>>> SCANNING OPERATOR...' },
{ cls: 'dim', text: '' },
{ cls: 'green', text: `>>> IP ADDRESS....... ${data.ip_masked}` },
{ cls: 'green', text: `>>> GEOLOCATION...... ${locStr} ${flag}` },
{ cls: 'green', text: `>>> NETWORK.......... ${data.isp}` },
data.hostname ? { cls: 'dim', text: `>>> HOSTNAME......... ${data.hostname}` } : null,
{ cls: 'green', text: `>>> BROWSER.......... ${browser}` },
{ cls: 'green', text: `>>> OS............... ${osStr}` },
{ cls: 'green', text: `>>> DEVICE........... ${data.device}` },
{ cls: 'green', text: `>>> SCREEN........... ${local.screen}` },
{ cls: 'green', text: `>>> LANGUAGE......... ${local.lang}` },
{ cls: 'green', text: `>>> TIMEZONE......... ${local.tz}` },
{ cls: 'green', text: `>>> CONNECTION....... ${local.connection}` },
{ cls: 'dim', text: '' },
{ cls: threatColor, text: `>>> THREAT LEVEL..... ${bar}` },
{ cls: threatColor, text: `>>> STATUS........... ${data.threat_reason}` },
{ cls: 'dim', text: '' },
{ cls: 'green', text: '>>> ACCESS GRANTED — WELCOME, OPERATOR' },
].filter(Boolean);
}
// ─── Typewriter: types text into element ─────────
function typeInto(el, text, speed) {
return new Promise(function (resolve) {
let i = 0;
function tick() {
if (i < text.length) {
el.textContent += text.charAt(i);
i++;
setTimeout(tick, speed);
} else {
resolve();
}
}
if (!text.length) { resolve(); return; }
tick();
});
}
// ─── Build overlay DOM ───────────────────────────
function buildOverlay() {
const overlay = document.createElement('div');
overlay.className = 'scan-overlay';
overlay.id = 'scanOverlay';
overlay.innerHTML = `
<div class="scan-panel" role="dialog" aria-label="Visitor Scan">
<div class="scan-panel-header">
<span class="scan-panel-title">◉ VISITOR RECONNAISSANCE // SCAN IN PROGRESS</span>
<div style="display:flex;align-items:center;gap:8px;">
<span class="scan-panel-status" id="scanStatus">● ACTIVE</span>
<button class="scan-close" id="scanClose" aria-label="Close">X CLOSE</button>
</div>
</div>
<div class="scan-body" id="scanBody"></div>
<div class="scan-footer">
<span id="scanFooterInfo">JAESWIFT // COMMAND OPS · SCAN v1.0</span>
<div style="display:flex;gap:8px;">
<button class="scan-footer-action scan-footer-action-dismiss" id="scanDismissBtn">DISMISS 7D</button>
<button class="scan-footer-action" id="scanAcceptBtn" disabled style="opacity:0.5;cursor:wait;">ACKNOWLEDGE</button>
</div>
</div>
</div>
`;
return overlay;
}
// ─── Build badge (collapsed state) ───────────────
function buildBadge(data) {
const flag = data.country_flag || '🌐';
const cc = data.country_code || 'XX';
const locShort = data.city ? `${data.city}, ${cc}` : (data.country || cc);
const badge = document.createElement('div');
badge.className = 'scan-badge';
badge.id = 'scanBadge';
badge.title = 'Click to re-run scan';
badge.innerHTML = `
<span class="scan-badge-flag">${flag}</span>
<div style="display:flex;flex-direction:column;line-height:1.2;">
<span class="scan-badge-label">OPERATOR</span>
<span class="scan-badge-val">${locShort}</span>
</div>
<button class="scan-badge-close" id="scanBadgeClose" aria-label="Hide badge" title="Hide">×</button>
`;
badge.addEventListener('click', function (e) {
if (e.target && e.target.id === 'scanBadgeClose') return;
// Re-run scan on click
try { localStorage.removeItem(DISMISS_KEY); } catch (err) {}
badge.remove();
runScan();
});
badge.querySelector('#scanBadgeClose').addEventListener('click', function (e) {
e.stopPropagation();
try { localStorage.setItem(BADGE_HIDE_KEY, '1'); } catch (err) {}
badge.classList.add('scan-closing');
setTimeout(() => badge.remove(), 220);
});
document.body.appendChild(badge);
}
// ─── Render scan lines one-by-one ────────────────
async function renderLines(body, lines) {
for (let i = 0; i < lines.length; i++) {
const item = lines[i];
const span = document.createElement('span');
span.className = `scan-line scan-line-${item.cls}`;
body.appendChild(span);
if (item.text) {
await typeInto(span, item.text, TYPE_SPEED);
body.scrollTop = body.scrollHeight;
} else {
span.textContent = '\u00A0';
}
await new Promise(r => setTimeout(r, LINE_PAUSE));
}
}
// ─── Run full scan sequence ──────────────────────
async function runScan() {
// fade in overlay first with 'CONNECTING...'
const overlay = buildOverlay();
document.body.appendChild(overlay);
const body = overlay.querySelector('#scanBody');
const statusEl = overlay.querySelector('#scanStatus');
const closeBtn = overlay.querySelector('#scanClose');
const dismissBtn = overlay.querySelector('#scanDismissBtn');
const ackBtn = overlay.querySelector('#scanAcceptBtn');
let scanData = null;
let apiFailed = false;
// Seed initial connecting line before fetch completes
const initLine = document.createElement('span');
initLine.className = 'scan-line scan-line-green';
body.appendChild(initLine);
typeInto(initLine, '>>> INITIALISING RECON PROTOCOL...', TYPE_SPEED);
function closeAll(dismiss) {
overlay.classList.add('scan-closing');
if (dismiss) setDismissed();
setTimeout(() => {
overlay.remove();
if (scanData && !isBadgeHidden()) {
buildBadge(scanData);
}
}, 260);
}
closeBtn.addEventListener('click', () => closeAll(false));
dismissBtn.addEventListener('click', () => closeAll(true));
ackBtn.addEventListener('click', () => closeAll(false));
// allow esc to close
function escHandler(e) {
if (e.key === 'Escape') {
closeAll(false);
document.removeEventListener('keydown', escHandler);
}
}
document.addEventListener('keydown', escHandler);
try {
const r = await fetch('/api/visitor/scan', { credentials: 'same-origin' });
if (r.ok) {
scanData = await r.json();
} else if (r.status === 429) {
apiFailed = true;
} else {
apiFailed = true;
}
} catch (e) {
apiFailed = true;
}
// clear init line
body.innerHTML = '';
if (apiFailed || !scanData) {
const errSpan = document.createElement('span');
errSpan.className = 'scan-line scan-line-red';
body.appendChild(errSpan);
await typeInto(errSpan, '>>> RECON SERVICE UNAVAILABLE — STANDBY', TYPE_SPEED);
statusEl.textContent = '● OFFLINE';
statusEl.style.color = 'var(--danger)';
ackBtn.disabled = false; ackBtn.style.opacity = '1'; ackBtn.style.cursor = 'pointer';
return;
}
const local = localInfo();
const lines = buildLines(scanData, local);
await renderLines(body, lines);
statusEl.textContent = '● SCAN COMPLETE';
ackBtn.disabled = false; ackBtn.style.opacity = '1'; ackBtn.style.cursor = 'pointer';
ackBtn.focus();
}
// ─── Boot ────────────────────────────────────────
function boot() {
if (isDismissed()) {
// still show compact badge if not hidden — fetch silently
if (!isBadgeHidden()) {
fetch('/api/visitor/scan', { credentials: 'same-origin' })
.then(r => r.ok ? r.json() : null)
.then(d => { if (d && d.country_code) buildBadge(d); })
.catch(() => {});
}
return;
}
// Slight delay so page renders first
setTimeout(runScan, 700);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
// Expose for debug / re-trigger
window.__jaeScan = {
run: runScan,
reset: function () {
try {
localStorage.removeItem(DISMISS_KEY);
localStorage.removeItem(BADGE_HIDE_KEY);
} catch (e) {}
const b = document.getElementById('scanBadge'); if (b) b.remove();
const o = document.getElementById('scanOverlay'); if (o) o.remove();
runScan();
},
};
})();