'use strict';
const Theme = (() => {
const KEY = 'pw_theme';
function apply() {
document.documentElement.classList.toggle('dark', localStorage.getItem(KEY) === 'dark');
}
function toggle() {
localStorage.setItem(KEY, Theme.current() === 'dark' ? 'light' : 'dark');
apply();
}
function current() { return document.documentElement.classList.contains('dark') ? 'dark' : 'light'; }
return { apply, toggle, current, KEY };
})();
const App = (() => {
const cfg = window.PAGER_CONFIG || {};
const API_BASE = cfg.apiBase || '';
const WS_BASE = (cfg.wsBase || location.origin).replace(/^http/, 'ws');
const TERMINAL_WS = cfg.terminalWs || ('ws://' + location.hostname + ':1471/api/terminal/openWs');
const PAGER_SCREEN_WS = cfg.pagerScreenWs || (WS_BASE + '/api/pager/display/screen.ws');
const PAGER_KEYS_WS = cfg.pagerKeysWs || (WS_BASE + '/api/pager/input/keys.ws');
const els = {};
let currentView = null;
let notifications = [];
let unreadNotifications = 0;
const NOTIFICATIONS_KEY = 'markviii_notifications';
const railItems = [
{ key: 'dashboard', label: 'Dashboard', hash: '#/dashboard', icon: 'dashboard' },
{ key: 'attacks', label: 'Attacks', hash: '#/attacks', icon: 'attack' },
{ key: 'pineap', label: 'PineAP', hash: '#/pineap', icon: 'wifi' },
{ key: 'recon', label: 'Recon', hash: '#/recon', icon: 'recon' },
{ key: 'logging', label: 'Logging', hash: '#/logging', icon: 'logging' },
{ key: 'modules', label: 'Payloads', hash: '#/modules', icon: 'modules' },
{ key: 'harness', label: 'Harness', hash: '#/harness', icon: 'extension' },
{ key: 'settings', label: 'Settings', hash: '#/settings', icon: 'settings' }
];
const railDividers = new Set(['logging']);
function keyOf(hash) {
const seg = (hash || '#/dashboard').replace(/^#\//, '').split('/')[0];
return seg || 'dashboard';
}
function buildRail() {
const rail = els.rail;
rail.innerHTML = '';
function railEntry(it) {
const a = document.createElement('a');
a.className = 'entry';
a.href = it.hash;
a.title = it.label;
a.innerHTML = '' + PineappleIcons[it.icon] + '' + it.label + '';
a.addEventListener('click', (e) => { e.preventDefault(); location.hash = it.hash; });
return a;
}
railItems.filter((it) => it.key !== 'settings').forEach((it) => {
if (railDividers.has(it.key)) {
const d = document.createElement('div');
d.className = 'entry divider';
rail.appendChild(d);
}
rail.appendChild(railEntry(it));
});
const foot = document.createElement('div');
foot.className = 'rail-footer';
const settings = railItems.find((it) => it.key === 'settings');
if (settings) foot.appendChild(railEntry(settings));
const open = document.createElement('a');
open.className = 'entry';
open.title = 'Open Menu';
open.innerHTML = '' + PineappleIcons.chevron_right + 'Open Menu';
open.addEventListener('click', (e) => {
e.preventDefault();
const openState = localStorage.getItem('pw_rail') === 'open';
localStorage.setItem('pw_rail', openState ? 'closed' : 'open');
els.rail.classList.toggle('open', !openState);
});
foot.appendChild(open);
rail.appendChild(foot);
rail.classList.toggle('open', localStorage.getItem('pw_rail') === 'open');
}
function route() {
closeToolbarMenus();
const hash = (location.hash || '#/dashboard').replace(/\/+$/, '');
if (hash === '#/recon/survey') {
location.hash = '#/recon';
return;
}
const name = routes[hash];
if (currentView && currentView.destroy) currentView.destroy();
els.content.innerHTML = '';
if (!name || !views[name]) {
const ph = document.createElement('div');
ph.className = 'section empty';
ph.textContent = 'View not available.';
els.content.appendChild(ph);
currentView = null;
} else {
currentView = views[name](els.content);
}
const key = keyOf(hash);
Array.prototype.forEach.call(els.rail.querySelectorAll('.entry'), (a) => {
const href = a.getAttribute('href');
a.classList.toggle('active', !!href && (href === hash || keyOf(href) === key));
});
}
function loadNotifications() {
try {
const stored = JSON.parse(sessionStorage.getItem(NOTIFICATIONS_KEY) || '{}');
notifications = Array.isArray(stored.items) ? stored.items.slice(0, 30) : [];
unreadNotifications = Math.max(0, Number(stored.unread) || 0);
} catch (e) {
notifications = [];
unreadNotifications = 0;
}
}
function saveNotifications() {
try {
sessionStorage.setItem(NOTIFICATIONS_KEY, JSON.stringify({
items: notifications.slice(0, 30), unread: unreadNotifications
}));
} catch (e) {}
}
function notificationTime(timestamp) {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return '';
const today = new Date();
if (date.toDateString() === today.toDateString()) {
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
}
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
function renderNotifications() {
if (!els.notificationsList) return;
els.notificationsList.innerHTML = '';
els.notificationBadge.classList.toggle('hidden', unreadNotifications === 0);
els.notificationBadge.textContent = unreadNotifications > 99 ? '99+' : String(unreadNotifications);
els.notificationsClear.disabled = notifications.length === 0;
if (!notifications.length) {
const empty = document.createElement('div');
empty.className = 'notification-empty';
empty.textContent = 'No Notifications';
els.notificationsList.appendChild(empty);
return;
}
notifications.forEach((notice) => {
const item = document.createElement('div');
item.className = 'notification-item';
const marker = document.createElement('span');
marker.className = 'notification-kind ' + (notice.kind || 'info');
const message = document.createElement('span');
message.className = 'notification-message';
message.textContent = notice.message;
const time = document.createElement('time');
time.className = 'notification-time';
time.dateTime = new Date(notice.timestamp).toISOString();
time.textContent = notificationTime(notice.timestamp);
item.append(marker, message, time);
els.notificationsList.appendChild(item);
});
}
function addNotification(message, kind) {
const normalizedKind = ['error', 'success'].indexOf(kind) === -1 ? 'info' : kind;
notifications.unshift({
message: String(message).slice(0, 300), kind: normalizedKind, timestamp: Date.now()
});
notifications = notifications.slice(0, 30);
unreadNotifications += 1;
saveNotifications();
renderNotifications();
}
function toast(msg, kind, options) {
if (!options || options.notify !== false) addNotification(msg, kind);
const d = document.createElement('div');
d.className = 'toast ' + (kind || 'info');
d.textContent = msg;
els.toasts.appendChild(d);
setTimeout(() => d.remove(), 4000);
}
function closeToolbarMenus() {
if (!els.notificationsMenu) return;
els.notificationsMenu.classList.add('hidden');
els.overflowMenu.classList.add('hidden');
els.notificationsButton.setAttribute('aria-expanded', 'false');
els.overflowButton.setAttribute('aria-expanded', 'false');
}
function toggleToolbarMenu(name) {
const menu = name === 'notifications' ? els.notificationsMenu : els.overflowMenu;
const button = name === 'notifications' ? els.notificationsButton : els.overflowButton;
const opening = menu.classList.contains('hidden');
closeToolbarMenus();
if (!opening) return;
menu.classList.remove('hidden');
button.setAttribute('aria-expanded', 'true');
if (name === 'notifications') {
unreadNotifications = 0;
saveNotifications();
renderNotifications();
}
}
function setInternetStatus(state) {
const labels = { checking: 'Checking\u2026', online: 'Online', offline: 'Offline' };
els.internetStatus.textContent = labels[state];
els.internetStatusDot.className = 'connection-dot ' + state;
}
function checkInternet(announce) {
setInternetStatus('checking');
return PagerAPI.get('/api/settings/internet')
.then((response) => {
const online = !!response.data.online;
setInternetStatus(online ? 'online' : 'offline');
if (announce) toast(online ? 'Internet connection is online' : 'Internet connection is offline', online ? 'success' : 'error');
})
.catch(() => {
setInternetStatus('offline');
if (announce) toast('Unable to check the internet connection', 'error');
});
}
function handleMenuAction(action) {
closeToolbarMenus();
if (action === 'help') {
location.hash = '#/settings/help';
} else if (action === 'updates') {
location.hash = '#/settings/advanced';
toast('Update controls are available in Advanced settings');
} else if (action === 'internet') {
if (typeof views.openClientModeModal === 'function') views.openClientModeModal();
else checkInternet(true);
} else if (action === 'logout') {
PagerAPI.post('/api/logout')
.then(() => showLogin())
.catch((error) => toast(error.message || 'Logout failed', 'error'));
} else if (action === 'reboot') {
if (!window.confirm('Reboot Mark VIII now?')) return;
PagerAPI.post('/api/settings/reboot')
.then(() => toast('Reboot requested. Mark VIII will disconnect shortly.'))
.catch((error) => toast(error.message || 'Reboot failed', 'error'));
}
}
function showDock(name) {
if (name === 'pager') {
if (typeof Term !== 'undefined' && Term.isOpen()) Term.close();
} else {
if (typeof Pager !== 'undefined' && Pager.isOpen()) Pager.close();
}
}
function showApp() {
els.login.classList.add('hidden');
els.app.classList.remove('hidden');
Theme.apply();
Live.start();
route();
checkInternet(false);
}
function showLogin() {
closeToolbarMenus();
if (typeof Live !== 'undefined') Live.stop();
if (typeof Term !== 'undefined' && Term.isOpen()) Term.close();
if (typeof Pager !== 'undefined' && Pager.isOpen()) Pager.close();
els.app.classList.add('hidden');
els.login.classList.remove('hidden');
}
function init() {
els.login = document.getElementById('login-screen');
els.app = document.getElementById('app');
els.rail = document.getElementById('rail');
els.content = document.getElementById('content');
els.toasts = document.getElementById('toast-container');
els.notificationsButton = document.getElementById('notifications-btn');
els.notificationsMenu = document.getElementById('notifications-menu');
els.notificationsList = document.getElementById('notifications-list');
els.notificationsClear = document.getElementById('notifications-clear');
els.notificationBadge = document.getElementById('notification-badge');
els.overflowButton = document.getElementById('overflow-btn');
els.overflowMenu = document.getElementById('overflow-menu');
els.internetStatus = document.getElementById('internet-status');
els.internetStatusDot = document.getElementById('internet-status-dot');
els.notificationsButton.innerHTML = PineappleIcons.notifications;
document.getElementById('terminal-btn').innerHTML = PineappleIcons.terminal;
document.getElementById('pager-btn').innerHTML = PineappleIcons.pager;
els.overflowButton.innerHTML = PineappleIcons.more_vert;
Array.prototype.forEach.call(document.querySelectorAll('[data-icon]'), (icon) => {
icon.innerHTML = PineappleIcons[icon.getAttribute('data-icon')] || '';
});
loadNotifications();
renderNotifications();
els.notificationsButton.addEventListener('click', (event) => {
event.stopPropagation();
toggleToolbarMenu('notifications');
});
els.overflowButton.addEventListener('click', (event) => {
event.stopPropagation();
toggleToolbarMenu('overflow');
});
els.notificationsMenu.addEventListener('click', (event) => event.stopPropagation());
els.overflowMenu.addEventListener('click', (event) => event.stopPropagation());
els.notificationsClear.addEventListener('click', () => {
notifications = [];
unreadNotifications = 0;
saveNotifications();
renderNotifications();
});
Array.prototype.forEach.call(els.overflowMenu.querySelectorAll('[data-menu-action]'), (item) => {
item.addEventListener('click', () => handleMenuAction(item.getAttribute('data-menu-action')));
});
document.addEventListener('click', closeToolbarMenus);
document.getElementById('login-form').addEventListener('submit', (e) => {
e.preventDefault();
const btn = document.getElementById('login-button');
const pw = document.getElementById('login-password').value;
document.getElementById('login-error').textContent = '';
btn.disabled = true;
PagerAPI.login('root', pw)
.then(() => { document.getElementById('login-password').value = ''; showApp(); toast('Logged in'); })
.catch((err) => {
document.getElementById('login-error').textContent = (err && err.message && err.message !== 'unauthorized')
? 'Login failed.' : 'Invalid credentials.';
})
.finally(() => { btn.disabled = false; });
});
document.getElementById('terminal-btn').addEventListener('click', () => {
if (typeof Term === 'undefined') { toast('Terminal not available yet'); return; }
showDock('terminal');
Term.toggle();
});
document.getElementById('terminal-close').addEventListener('click', () => {
if (typeof Term === 'undefined') { toast('Terminal not available yet'); return; }
showDock('terminal');
Term.toggle();
});
document.getElementById('pager-btn').addEventListener('click', () => {
if (typeof Pager === 'undefined') { toast('Virtual Pager not available yet'); return; }
showDock('pager');
Pager.toggle();
});
document.getElementById('pager-close').addEventListener('click', () => {
if (typeof Pager === 'undefined') { toast('Virtual Pager not available yet'); return; }
showDock('pager');
Pager.toggle();
});
window.addEventListener('hashchange', route);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeToolbarMenus();
return;
}
if (localStorage.getItem('pw_hotkeys') === 'false') return;
const t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
const map = { d: '#/dashboard', p: '#/pineap', r: '#/recon', l: '#/logging', m: '#/modules' };
if (map[e.key.toLowerCase()]) { location.hash = map[e.key.toLowerCase()]; }
else if (e.key === '`') {
if (typeof Term === 'undefined') return;
if (typeof Pager !== 'undefined' && Pager.isOpen()) Pager.close();
Term.toggle();
}
});
PagerAPI.setBase(API_BASE);
PagerAPI.on401(() => showLogin());
buildRail();
Theme.apply();
checkSession();
}
function checkSession() {
PagerAPI.get('/api/api_ping')
.then(() => showApp())
.catch(() => showLogin());
}
const routes = {
'#/dashboard': 'dashboard',
'#/attacks': 'attacks',
'#/attacks/wpa': 'attacks_wpa',
'#/attacks/open': 'attacks_open',
'#/attacks/enterprise': 'attacks_enterprise',
'#/pineap': 'pineap',
'#/pineap/open': 'pineap_open',
'#/pineap/evilwpa': 'pineap_evilwpa',
'#/pineap/impersonation': 'pineap_impersonation',
'#/pineap/clients': 'pineap_clients',
'#/pineap/filtering': 'pineap_filtering',
'#/recon': 'recon',
'#/recon/reports': 'recon_reports',
'#/recon/handshakes': 'recon_handshakes',
'#/logging': 'logging',
'#/logging/system': 'logging_system',
'#/modules': 'modules',
'#/modules/online': 'modules_online',
'#/modules/running': 'modules_running',
'#/modules/develop': 'modules_develop',
'#/settings': 'settings',
'#/settings/networking': 'settings_networking',
'#/settings/wifi': 'settings_wifi',
'#/settings/led': 'settings_led',
'#/settings/advanced': 'settings_advanced',
'#/settings/help': 'settings_help',
'#/harness': 'harness'
};
return { init, route, toast, showLogin, checkInternet, wsUrl: (p) => WS_BASE + p,
terminalWs: TERMINAL_WS, pagerScreenWs: PAGER_SCREEN_WS,
pagerKeysWs: PAGER_KEYS_WS, apiBase: API_BASE,
keyOf, railItems, go: (h) => { location.hash = h; },
get key() { return keyOf(location.hash); } };
})();
const Live = (() => {
let ws = null;
let ever = false;
let poll = null;
const subs = [];
let timer = null;
function stopPoll() {
if (poll) { clearInterval(poll); poll = null; }
}
function start() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
stopPoll();
try { ws = new WebSocket(App.wsUrl('/api/ws')); }
catch (e) { fallback(); return; }
ws.onopen = () => { ever = true; };
ws.onmessage = (ev) => {
let msg;
try { msg = JSON.parse(ev.data); } catch (e) { return; }
subs.forEach((fn) => fn(msg));
updateBar(msg);
};
ws.onclose = () => {
ws = null;
clearTimeout(timer);
if (ever) timer = setTimeout(start, 5000);
else fallback();
};
ws.onerror = () => { try { ws.close(); } catch (e) {} };
}
function stop() {
ever = false;
clearTimeout(timer);
timer = null;
stopPoll();
if (ws) {
const active = ws;
ws = null;
active.onclose = null;
try { active.close(); } catch (e) {}
}
}
function fallback() {
stopPoll();
const seconds = Math.max(2, Math.min(120, parseInt(localStorage.getItem('pw-poll') || '5', 10) || 5));
poll = setInterval(async () => {
try {
const r = await PagerAPI.get('/api/status');
const msg = { type: 'tick', status: r.data, clients: r.data.clients };
subs.forEach((fn) => fn(msg));
updateBar(msg);
} catch (e) {}
}, seconds * 1000);
}
function updateBar(msg) {
const b = (msg.status || {}).battery || {};
const n = (msg.clients || []).length;
const el = document.getElementById('live-status');
if (el) el.textContent = 'BAT ' + (b.level == null ? '--' : b.level + '%' + (b.charging ? '+' : '')) + ' CLIENTS ' + n;
}
function onTick(fn) {
subs.push(fn);
return () => {
const i = subs.indexOf(fn);
if (i !== -1) subs.splice(i, 1);
};
}
return { start, stop, onTick };
})();
document.addEventListener('DOMContentLoaded', () => App.init());