306 lines
13 KiB
JavaScript
306 lines
13 KiB
JavaScript
/* ===================================================
|
||
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();
|
||
},
|
||
};
|
||
})();
|