Dash
Items
Custom JavaScript
Save
Cancel
/* ============================================================ HEIMDALL — COMMAND CENTER THEME | custom.js Drop in: storage/app/public/custom.js ============================================================ */ (function () { 'use strict'; /* ─── UTILITY ──────────────────────────────────────────────── */ const qs = (sel, ctx = document) => ctx.querySelector(sel); const qsa = (sel, ctx = document) => [...ctx.querySelectorAll(sel)]; const on = (el, ev, fn, opts) => el && el.addEventListener(ev, fn, opts); /* ===================================================== 1. PARTICLE CANVAS — deep-space starfield with teal constellation lines ===================================================== */ function initParticles() { const canvas = document.createElement('canvas'); canvas.id = 'heimdall-canvas'; document.body.prepend(canvas); const ctx = canvas.getContext('2d'); const CONFIG = { count: 120, maxDist: 140, speed: 0.22, teal: '0, 245, 200', amber: '255, 184, 0', mouseDist: 130, mouseRepel: 0.012, }; let W, H, mouse = { x: -9999, y: -9999 }; function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } resize(); on(window, 'resize', resize); on(window, 'mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; }); class Particle { constructor() { this.reset(true); } reset(init = false) { this.x = Math.random() * W; this.y = init ? Math.random() * H : -4; this.vx = (Math.random() - 0.5) * CONFIG.speed; this.vy = (Math.random() - 0.5) * CONFIG.speed; this.r = Math.random() * 1.4 + 0.3; this.alpha = Math.random() * 0.6 + 0.2; // ~5% amber stars this.color = Math.random() < 0.05 ? CONFIG.amber : CONFIG.teal; this.pulse = Math.random() * Math.PI * 2; this.pulseSpeed = 0.015 + Math.random() * 0.02; } update() { // mouse repel const dx = this.x - mouse.x; const dy = this.y - mouse.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < CONFIG.mouseDist) { const force = (CONFIG.mouseDist - dist) / CONFIG.mouseDist; this.vx += dx / dist * force * CONFIG.mouseRepel; this.vy += dy / dist * force * CONFIG.mouseRepel; } // dampen velocity this.vx *= 0.994; this.vy *= 0.994; this.x += this.vx; this.y += this.vy; this.pulse += this.pulseSpeed; // wrap edges if (this.x < -10) this.x = W + 10; if (this.x > W + 10) this.x = -10; if (this.y < -10) this.y = H + 10; if (this.y > H + 10) this.y = -10; } draw() { const a = this.alpha * (0.75 + 0.25 * Math.sin(this.pulse)); ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); ctx.fillStyle = `rgba(${this.color}, ${a})`; ctx.fill(); } } const particles = Array.from({ length: CONFIG.count }, () => new Particle()); function drawLines() { for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const a = particles[i], b = particles[j]; const dx = a.x - b.x, dy = a.y - b.y; const d2 = dx * dx + dy * dy; if (d2 < CONFIG.maxDist * CONFIG.maxDist) { const alpha = (1 - Math.sqrt(d2) / CONFIG.maxDist) * 0.18; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.strokeStyle = `rgba(${CONFIG.teal}, ${alpha})`; ctx.lineWidth = 0.6; ctx.stroke(); } } } } function loop() { ctx.clearRect(0, 0, W, H); particles.forEach(p => { p.update(); p.draw(); }); drawLines(); requestAnimationFrame(loop); } loop(); } /* ===================================================== 2. CUSTOM CURSOR ===================================================== */ function initCursor() { const dot = document.createElement('div'); const ring = document.createElement('div'); dot.id = 'cursor-dot'; ring.id = 'cursor-ring'; document.body.append(dot, ring); let rx = 0, ry = 0, mx = 0, my = 0; let isHovering = false; on(document, 'mousemove', e => { mx = e.clientX; my = e.clientY; dot.style.left = mx + 'px'; dot.style.top = my + 'px'; // Hide custom cursor inside xterm so it doesn't interfere const inXterm = e.target.closest('#wt-xterm'); dot.style.opacity = inXterm ? '0' : '1'; ring.style.opacity = inXterm ? '0' : '1'; }); // Lag ring for smooth trail (function ringLoop() { rx += (mx - rx) * 0.14; ry += (my - ry) * 0.14; ring.style.left = (rx - 14) + 'px'; ring.style.top = (ry - 14) + 'px'; requestAnimationFrame(ringLoop); })(); // Expand ring on interactive elements const hotSelectors = 'a, button, .btn, .app-card, .card, .application-card, input, select, textarea, [role="button"]'; on(document, 'mouseover', e => { if (e.target.closest(hotSelectors)) { ring.style.width = '44px'; ring.style.height = '44px'; ring.style.borderColor = 'rgba(0, 245, 200, 0.9)'; dot.style.transform = 'scale(2)'; isHovering = true; } }); on(document, 'mouseout', e => { if (isHovering && !e.target.closest(hotSelectors)) { ring.style.width = '28px'; ring.style.height = '28px'; ring.style.borderColor = 'rgba(0, 245, 200, 0.6)'; dot.style.transform = 'scale(1)'; isHovering = false; } }); // Click burst on(document, 'click', e => { const burst = document.createElement('div'); burst.style.cssText = ` position:fixed; left:${e.clientX - 20}px; top:${e.clientY - 20}px; width:40px; height:40px; border-radius:50%; border:2px solid rgba(0,245,200,0.8); pointer-events:none; z-index:99997; animation:cursorBurst 0.5s ease-out forwards; `; document.body.append(burst); setTimeout(() => burst.remove(), 520); }); } /* ===================================================== 3. HUD STATUS BAR — 3-column large metrics ===================================================== */ function initHudBar() { const SESSION_ID = Math.random().toString(36).slice(2,9).toUpperCase(); const bar = document.createElement('div'); bar.className = 'heimdall-hud-bar'; bar.innerHTML = ` <!-- LEFT: uptime + session --> <div class="hud-left"> <span class="hud-uptime-label">Uptime</span> <span class="hud-uptime-value" id="hud-uptime">00:00:00</span> <div class="hud-session-row"> <div class="hud-ping-dot"></div> <span class="hud-session-label">Online</span> <span class="hud-session-id">#${SESSION_ID}</span> </div> </div> <!-- CENTER: clock + date --> <div class="hud-center"> <span class="hud-clock" id="hud-clock">--:--:--</span> <span class="hud-date" id="hud-date">--- -- ----</span> <span class="hud-sys-tag">WAWTOR</span> </div> <!-- RIGHT: stats + terminal button --> <div class="hud-right"> <div class="hud-stat-row"> <span class="hud-stat-label">Containers</span> <span class="hud-stat-value">6</span> </div> <div class="hud-stat-row"> <span class="hud-stat-label">Tailscale</span> <span class="hud-stat-value">UP</span> </div> <div class="hud-stat-row"> <span class="hud-stat-label">Temp</span> <span class="hud-stat-value amber">38°C</span> </div> <button class="hud-terminal-btn" id="hud-term-btn">⌨ Terminal</button> </div> `; const nav = qs('header, nav.navbar, .navbar, #topnav'); if (nav && nav.parentNode) { nav.parentNode.insertBefore(bar, nav.nextSibling); } else { document.body.prepend(bar); } // Live clock function tick() { const now = new Date(); const hh = String(now.getHours()).padStart(2,'0'); const mm = String(now.getMinutes()).padStart(2,'0'); const ss = String(now.getSeconds()).padStart(2,'0'); const clockEl = qs('#hud-clock'); if (clockEl) clockEl.textContent = `${hh}:${mm}:${ss}`; const dateEl = qs('#hud-date'); if (dateEl) dateEl.textContent = now.toLocaleDateString('en-US', { weekday:'short', month:'short', day:'numeric', year:'numeric' }).toUpperCase(); } tick(); setInterval(tick, 1000); // Session uptime counter const start = Date.now(); setInterval(() => { const el = qs('#hud-uptime'); if (!el) return; const s = Math.floor((Date.now() - start) / 1000); const H = String(Math.floor(s / 3600)).padStart(2,'0'); const M = String(Math.floor((s % 3600) / 60)).padStart(2,'0'); const S = String(s % 60).padStart(2,'0'); el.textContent = `${H}:${M}:${S}`; }, 1000); // Terminal button wires to the terminal widget on(qs('#hud-term-btn'), 'click', () => { const win = qs('#wt-window'); const tog = qs('#wt-toggle'); if (!win) return; if (win.classList.contains('hidden')) { win.classList.remove('hidden'); if (tog) tog.style.display = 'none'; } else { win.classList.add('hidden'); if (tog) tog.style.display = 'block'; } }); } /* ===================================================== 3b. TERMINAL WIDGET ===================================================== */ function initTerminalWidget() { /* ── Default macros ────────────────────────────────── */ const DEFAULT_MACROS = [ { id: 'm1', name: 'Restart Nginx', cmd: 'docker restart nginx-proxy' }, { id: 'm2', name: 'Restart Portainer', cmd: 'docker restart portainer' }, { id: 'm3', name: 'Restart Nextcloud', cmd: 'docker restart nextcloud' }, { id: 'm4', name: 'Restart MariaDB', cmd: 'docker restart mariadb' }, { id: 'm5', name: 'Restart Redis', cmd: 'docker restart redis' }, { id: 'm6', name: 'Docker PS', cmd: 'docker ps --format "table {{.Names}}\\t{{.Status}}"' }, { id: 'm7', name: 'System Uptime', cmd: 'uptime' }, { id: 'm8', name: 'Disk Usage', cmd: 'df -h /' }, ]; function loadMacros() { try { const raw = localStorage.getItem('wt_macros'); return raw ? JSON.parse(raw) : DEFAULT_MACROS; } catch { return DEFAULT_MACROS; } } function saveMacros(list) { localStorage.setItem('wt_macros', JSON.stringify(list)); } function loadWsUrl() { return localStorage.getItem('wt_ws_url') || 'ws://localhost:7682/ws'; } function saveWsUrl(url) { localStorage.setItem('wt_ws_url', url); } /* ── Load xterm.js + fit addon from CDN ─────────────── */ function loadScript(src) { return new Promise((res, rej) => { if (document.querySelector(`script[src="${src}"]`)) { res(); return; } const s = document.createElement('script'); s.src = src; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); } function loadLink(href) { if (document.querySelector(`link[href="${href}"]`)) return; const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = href; document.head.appendChild(l); } /* ── Build DOM ─────────────────────────────────────── */ const win = document.createElement('div'); win.id = 'wt-window'; win.innerHTML = ` <div id="wt-titlebar"> <div class="wt-traffic"> <div class="wt-dot close" id="wt-close"></div> <div class="wt-dot min" id="wt-min"></div> <div class="wt-dot max" id="wt-max"></div> </div> <span class="wt-title">dmw@wawtor — terminal</span> <span id="wt-conn-status" class="disconnected">DISCONNECTED</span> <button class="wt-btn" id="wt-macro-toggle">⚡ Macros</button> <button class="wt-btn" id="wt-reconnect-btn">Reconnect</button> </div> <div id="wt-body"> <!-- xterm mount --> <div id="wt-xterm"></div> <!-- connect screen --> <div id="wt-connect-screen"> <div class="wt-connect-title">Connect to Backend Terminal</div> <div class="wt-connect-desc"> This widget connects to a WebSocket terminal server.<br> Recommended: <code>ttyd</code> running on your wawtor server.<br><br> Install: <code>apt install ttyd</code><br> Run: <code>ttyd -p 7681 bash</code> </div> <div class="wt-connect-row"> <input type="text" id="wt-ws-input" placeholder="ws://wawtor:7682/ws" /> <button class="wt-connect-btn" id="wt-do-connect">Connect</button> </div> </div> <!-- macro panel --> <div id="wt-macro-panel"> <div class="wt-macro-header"> <span class="wt-macro-title">⚡ Service Macros</span> <button class="wt-macro-edit-btn" id="wt-edit-mode-btn">Edit</button> </div> <div class="wt-macro-list" id="wt-macro-list"></div> <button class="wt-macro-add-btn" id="wt-add-macro">+ Add Macro</button> <div class="wt-macro-settings"> <div class="wt-settings-label">WebSocket URL</div> <input class="wt-settings-input" id="wt-ws-setting" type="text" placeholder="ws://wawtor:7681/ws" /> </div> </div> </div> `; document.body.appendChild(win); // Toggle button (shows when window is hidden) const toggleBtn = document.createElement('button'); toggleBtn.id = 'wt-toggle'; toggleBtn.textContent = '⌨ Terminal'; document.body.appendChild(toggleBtn); on(toggleBtn, 'click', () => { win.classList.remove('hidden'); toggleBtn.style.display = 'none'; }); /* ── State ──────────────────────────────────────────── */ let term = null; let fitAddon = null; let ws = null; let macros = loadMacros(); let editMode = false; let isMinimized = false; const connScreen = qs('#wt-connect-screen'); const xtermMount = qs('#wt-xterm'); const macroPanel = qs('#wt-macro-panel'); const macroList = qs('#wt-macro-list'); const connStatus = qs('#wt-conn-status'); const wsInput = qs('#wt-ws-input'); const wsSetting = qs('#wt-ws-setting'); // Pre-fill stored URL const stored = loadWsUrl(); if (wsInput) wsInput.value = stored; if (wsSetting) wsSetting.value = stored; /* ── Load xterm then init ──────────────────────────── */ loadLink('https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css'); Promise.all([ loadScript('https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js'), loadScript('https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js'), ]).then(() => { term = new Terminal({ theme: { background: '#0b0d0f', foreground: '#e8f4f8', cursor: '#00f5c8', cursorAccent: '#0b0d0f', selectionBackground: 'rgba(0,245,200,0.25)', black: '#0b0d0f', brightBlack: '#3d6070', red: '#ff3a5c', brightRed: '#ff6b84', green: '#00f5c8', brightGreen: '#00f5c8', yellow: '#ffb800', brightYellow: '#ffca44', blue: '#4a9eff', brightBlue: '#7ab8ff', magenta: '#cc44ff', brightMagenta: '#dd88ff', cyan: '#00c49e', brightCyan: '#00f5c8', white: '#7aaabb', brightWhite: '#e8f4f8', }, fontFamily: "'Share Tech Mono', 'Courier New', monospace", fontSize: 13, lineHeight: 1.4, cursorBlink: true, cursorStyle: 'block', scrollback: 2000, allowTransparency: true, }); fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); term.open(xtermMount); // Register input handler ONCE — references outer ws variable term.onData(data => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send('0' + data); } }); // Register resize handler here where term is defined term.onResize(() => sendResize()); // Click anywhere in terminal to focus it on(xtermMount, 'click', () => term.focus()); // Try auto-connect attemptConnect(loadWsUrl()); // Resize observer new ResizeObserver(() => { if (fitAddon && !isMinimized) { try { fitAddon.fit(); } catch(e){} } }).observe(win); }); /* ── WebSocket connection ───────────────────────────── */ function setStatus(state) { connStatus.className = state; const labels = { connected:'CONNECTED', disconnected:'DISCONNECTED', connecting:'CONNECTING…' }; connStatus.textContent = labels[state] || state.toUpperCase(); } function attemptConnect(url) { if (!url || !url.startsWith('ws')) { showConnectScreen(true); return; } if (ws) { try { ws.close(); } catch(e){} } setStatus('connecting'); ws = new WebSocket(url, ['tty']); // 'tty' subprotocol is REQUIRED by ttyd ws.binaryType = 'arraybuffer'; ws.onopen = () => { setStatus('connected'); showConnectScreen(false); saveWsUrl(url); if (wsSetting) wsSetting.value = url; if (wsInput) wsInput.value = url; // ttyd auth — must include cols/rows in initial message const cols = (term && term.cols) || 80; const rows = (term && term.rows) || 24; ws.send(JSON.stringify({ AuthToken: '', columns: cols, rows: rows })); setTimeout(() => { if (fitAddon) { try { fitAddon.fit(); } catch(e){} } sendResize(); term && term.focus(); }, 200); }; ws.onmessage = (e) => { if (!term) return; if (typeof e.data === 'string') { const type = e.data[0]; const payload = e.data.slice(1); if (type === '0') { // OUTPUT — ttyd uses '0' for server→client output term.write(payload); } else if (type === '1') { // Also handle '1' just in case build differs term.write(payload); } else if (type === '2') { try { const t = JSON.parse(payload); qs('.wt-title').textContent = t.title || 'dmw@wawtor — terminal'; } catch(e) {} } } else if (e.data instanceof ArrayBuffer) { // Binary frame — first byte is opcode, rest is data const bytes = new Uint8Array(e.data); if (bytes.length > 1) { term.write(bytes.slice(1)); // strip the opcode byte } } }; ws.onerror = () => { setStatus('disconnected'); showConnectScreen(true); }; ws.onclose = () => { setStatus('disconnected'); }; } function sendResize() { if (!ws || ws.readyState !== WebSocket.OPEN || !term) return; // ttyd resize opcode is '4', NOT '1' ws.send('4' + JSON.stringify({ columns: term.cols, rows: term.rows })); } function showConnectScreen(show) { connScreen.classList.toggle('visible', show); xtermMount.style.display = show ? 'none' : 'block'; } /* ── Macro panel ────────────────────────────────────── */ function renderMacros() { macroList.innerHTML = ''; macros.forEach((m, idx) => { const item = document.createElement('div'); item.className = 'wt-macro-item'; item.dataset.id = m.id; item.innerHTML = ` <div class="wt-macro-run" title="Run macro">▶</div> <div class="wt-macro-info"> <div class="wt-macro-name">${escHtml(m.name)}</div> <div class="wt-macro-cmd">${escHtml(m.cmd)}</div> </div> <div class="wt-macro-del" title="Delete">✕</div> `; // Run qs('.wt-macro-run', item).onclick = () => runMacro(m, qs('.wt-macro-run', item)); // Double-click name/cmd to edit qs('.wt-macro-info', item).ondblclick = () => enterEditItem(item, m); // Delete qs('.wt-macro-del', item).onclick = () => { macros = macros.filter(x => x.id !== m.id); saveMacros(macros); renderMacros(); }; macroList.appendChild(item); }); } function enterEditItem(item, m) { item.classList.add('editing'); item.innerHTML = ` <input class="wt-macro-input name-input" value="${escHtml(m.name)}" placeholder="Macro name" /> <input class="wt-macro-input cmd-input" value="${escHtml(m.cmd)}" placeholder="Command…" /> <div class="wt-macro-save-row"> <button class="wt-macro-cancel">Cancel</button> <button class="wt-macro-save">Save</button> </div> `; qs('.wt-macro-save', item).onclick = () => { const newName = qs('.name-input', item).value.trim(); const newCmd = qs('.cmd-input', item).value.trim(); if (!newName || !newCmd) return; const idx = macros.findIndex(x => x.id === m.id); if (idx !== -1) { macros[idx].name = newName; macros[idx].cmd = newCmd; } saveMacros(macros); renderMacros(); }; qs('.wt-macro-cancel', item).onclick = () => renderMacros(); qs('.name-input', item).focus(); } function runMacro(m, btn) { if (ws && ws.readyState === WebSocket.OPEN && term) { // ttyd text protocol: '0' + data ws.send('0' + m.cmd + '\r'); term.focus(); // Visual feedback btn.classList.add('running'); btn.textContent = '⟳'; setTimeout(() => { btn.classList.remove('running'); btn.textContent = '▶'; }, 800); } else { // Fallback: copy to clipboard navigator.clipboard.writeText(m.cmd).catch(() => {}); btn.textContent = '✓'; setTimeout(() => { btn.textContent = '▶'; }, 1200); } } // Edit mode toggle on(qs('#wt-edit-mode-btn'), 'click', () => { editMode = !editMode; macroPanel.classList.toggle('edit-mode', editMode); qs('#wt-edit-mode-btn').classList.toggle('active', editMode); qs('#wt-edit-mode-btn').textContent = editMode ? 'Done' : 'Edit'; }); // Add new macro on(qs('#wt-add-macro'), 'click', () => { const newM = { id: 'm' + Date.now(), name: 'New Macro', cmd: 'echo hello' }; macros.push(newM); saveMacros(macros); renderMacros(); // Auto-enter edit on the new item const lastItem = macroList.lastElementChild; if (lastItem) enterEditItem(lastItem, newM); }); // WS URL setting on(wsSetting, 'change', () => { const url = wsSetting.value.trim(); saveWsUrl(url); if (wsInput) wsInput.value = url; }); // Connect screen button on(qs('#wt-do-connect'), 'click', () => { const url = wsInput.value.trim(); if (url) attemptConnect(url); }); on(wsInput, 'keydown', e => { if (e.key === 'Enter') qs('#wt-do-connect').click(); }); // Reconnect button on(qs('#wt-reconnect-btn'), 'click', () => attemptConnect(loadWsUrl())); // Macro panel toggle on(qs('#wt-macro-toggle'), 'click', () => { macroPanel.classList.toggle('open'); qs('#wt-macro-toggle').classList.toggle('active', macroPanel.classList.contains('open')); setTimeout(() => { if (fitAddon) try { fitAddon.fit(); } catch(e){} }, 300); }); renderMacros(); /* ── Window controls ────────────────────────────────── */ on(qs('#wt-close'), 'click', () => { win.classList.add('hidden'); toggleBtn.style.display = 'block'; }); on(qs('#wt-min'), 'click', () => { isMinimized = !isMinimized; win.classList.toggle('minimized', isMinimized); }); on(qs('#wt-max'), 'click', () => { win.classList.toggle('maximized'); setTimeout(() => { if (fitAddon) try { fitAddon.fit(); } catch(e){} }, 100); }); /* ── Drag ───────────────────────────────────────────── */ const titlebar = qs('#wt-titlebar'); let dragOffX = 0, dragOffY = 0, dragging = false; function savePos() { const rect = win.getBoundingClientRect(); localStorage.setItem('wt_pos', JSON.stringify({ left: rect.left, top: rect.top })); } function restorePos() { try { const saved = JSON.parse(localStorage.getItem('wt_pos')); if (saved) { // Clamp to viewport in case screen size changed const maxL = window.innerWidth - 200; const maxT = window.innerHeight - 60; const left = Math.max(0, Math.min(saved.left, maxL)); const top = Math.max(0, Math.min(saved.top, maxT)); win.style.right = 'auto'; win.style.bottom = 'auto'; win.style.left = left + 'px'; win.style.top = top + 'px'; } } catch(e) {} } on(titlebar, 'mousedown', e => { if (e.target.closest('.wt-dot, .wt-btn')) return; if (win.classList.contains('maximized')) return; dragging = true; const rect = win.getBoundingClientRect(); dragOffX = e.clientX - rect.left; dragOffY = e.clientY - rect.top; win.style.transition = 'none'; win.style.right = 'auto'; win.style.bottom = 'auto'; e.preventDefault(); }); on(document, 'mousemove', e => { if (!dragging) return; win.style.left = (e.clientX - dragOffX) + 'px'; win.style.top = (e.clientY - dragOffY) + 'px'; }); on(document, 'mouseup', () => { if (dragging) { dragging = false; savePos(); } }); // Restore saved position, start hidden restorePos(); win.classList.add('hidden'); toggleBtn.style.display = 'block'; } function escHtml(s) { return String(s) .replace(/&/g,'&') .replace(/</g,'<') .replace(/>/g,'>') .replace(/"/g,'"'); } /* ===================================================== 4. HUD CORNER BRACKETS ON CARDS ===================================================== */ function initCornerBrackets() { const cards = qsa('.item'); cards.forEach(card => { if (!card.querySelector('.corner-bracket')) { const positions = [ { top:'5px', left:'5px', borderWidth:'2px 0 0 2px' }, { top:'5px', right:'5px', borderWidth:'2px 2px 0 0' }, { bottom:'5px', left:'5px', borderWidth:'0 0 2px 2px' }, { bottom:'5px', right:'68px', borderWidth:'0 2px 2px 0' }, ]; positions.forEach(pos => { const b = document.createElement('div'); b.className = 'corner-bracket'; b.style.cssText = Object.entries({ ...pos, position:'absolute', width:'10px', height:'10px', borderColor:'rgba(0,245,200,0.7)', borderStyle:'solid', opacity:'0', transition:'opacity 0.3s ease', pointerEvents:'none', zIndex:'3', }).map(([k,v]) => `${k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase())}:${v}`).join(';'); card.appendChild(b); }); card.addEventListener('mouseenter', () => { card.querySelectorAll('.corner-bracket').forEach(b => b.style.opacity = '1'); }); card.addEventListener('mouseleave', () => { card.querySelectorAll('.corner-bracket').forEach(b => b.style.opacity = '0'); }); } }); } function initCardTilt() { const cards = qsa('.item'); cards.forEach(card => { on(card, 'mousemove', e => { const rect = card.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const rx = ((e.clientY - cy) / (rect.height / 2)) * -5; const ry = ((e.clientX - cx) / (rect.width / 2)) * 5; card.style.transform = `perspective(600px) rotateX(${rx}deg) rotateY(${ry}deg) translateY(-4px) scale(1.02)`; }); on(card, 'mouseleave', () => { card.style.transform = ''; card.style.transition = 'transform 0.45s cubic-bezier(0.23,1,0.32,1)'; setTimeout(() => { card.style.transition = ''; }, 460); }); }); } /* ===================================================== 6. RIPPLE EFFECT on interactive elements ===================================================== */ function initRipple() { on(document, 'click', e => { const target = e.target.closest('.btn, button, .item'); if (!target) return; const rect = target.getBoundingClientRect(); const ripple = document.createElement('span'); const size = Math.max(rect.width, rect.height) * 2; ripple.style.cssText = ` position:absolute; width:${size}px; height:${size}px; left:${e.clientX - rect.left - size/2}px; top:${e.clientY - rect.top - size/2}px; background:rgba(0,245,200,0.12); border-radius:50%; transform:scale(0); animation:rippleAnim 0.6s ease-out forwards; pointer-events:none; z-index:10; `; target.style.overflow = 'hidden'; target.appendChild(ripple); setTimeout(() => ripple.remove(), 620); }); } /* ===================================================== 7. BOOT SEQUENCE — wawtor server terminal shell ===================================================== */ function initBootSequence() { if (sessionStorage.getItem('hd_boot_v2')) return; sessionStorage.setItem('hd_boot_v2', '1'); const HOST = 'wawtor'; const USER = 'dmw'; const KERNEL = '6.8.0-wawtor-lts'; const now = new Date(); const bootTs = now.toISOString().replace('T', ' ').slice(0, 19); const upSeed = (Math.random() * 4 + 0.5).toFixed(2); /* Each entry: { cmd: string | null, out: [ { text, color } ] } cmd=null → output-only block (kernel banner, etc.) */ const SCRIPT = [ // ── kernel boot banner (no prompt) ────────────────────── { cmd: null, out: [ { text: `[ 0.000000] Linux version ${KERNEL} (gcc 13.2.0)`, color: '#4a7c6e' }, { text: `[ 0.000000] BIOS-provided memory map:`, color: '#4a7c6e' }, { text: `[ 0.000000] ACPI: IRQ0 used by override.`, color: '#4a7c6e' }, { text: `[ 0.418312] pci 0000:00:01.0: [8086:1237] type 00 class 0x060000`, color: '#4a7c6e' }, { text: `[ 1.203441] EXT4-fs (nvme0n1p2): mounted filesystem with ordered data mode`, color: '#4a7c6e' }, { text: `[ 1.887604] systemd[1]: Detected architecture x86-64.`, color: '#4a7c6e' }, { text: `[ 1.887660] systemd[1]: Hostname set to <${HOST}>.`, color: '#00f5c8' }, { text: ``, color: '' }, { text: `Ubuntu 24.04.2 LTS ${HOST} ${KERNEL} #1 SMP`, color: '#7aaabb' }, { text: ``, color: '' }, ]}, // ── systemctl check ───────────────────────────────────── { cmd: `systemctl is-active --all 2>/dev/null | head -12`, out: [ { text: ` docker active`, color: '#00f5c8' }, { text: ` portainer active`, color: '#00f5c8' }, { text: ` nextcloud active`, color: '#00f5c8' }, { text: ` tailscaled active`, color: '#00f5c8' }, { text: ` nginx active`, color: '#00f5c8' }, { text: ` samba active`, color: '#00f5c8' }, { text: ` ssh active`, color: '#00f5c8' }, { text: ` cron active`, color: '#00f5c8' }, { text: ` ufw active`, color: '#00f5c8' }, ]}, // ── disk usage ────────────────────────────────────────── { cmd: `df -h --output=target,used,avail,pcent | column -t`, out: [ { text: `Mounted on Used Avail Use%`, color: '#7aaabb' }, { text: `/ 14G 198G 7%`, color: '#e8f4f8' }, { text: `/mnt/storage 2.1T 5.7T 27%`, color: '#e8f4f8' }, { text: `/mnt/media 890G 3.1T 22%`, color: '#e8f4f8' }, { text: `/boot/efi 11M 511M 3%`, color: '#e8f4f8' }, ]}, // ── docker containers ─────────────────────────────────── { cmd: `docker ps --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}" 2>/dev/null`, out: [ { text: `NAMES STATUS PORTS`, color: '#7aaabb' }, { text: `portainer Up 14 days 0.0.0.0:9443->9443/tcp`, color: '#e8f4f8' }, { text: `nextcloud Up 14 days 0.0.0.0:8080->80/tcp`, color: '#e8f4f8' }, { text: `watchtower Up 14 days 8080/tcp`, color: '#e8f4f8' }, { text: `nginx-proxy Up 14 days 0.0.0.0:80->80/tcp, 443->443/tcp`, color: '#e8f4f8' }, { text: `mariadb Up 14 days 3306/tcp`, color: '#e8f4f8' }, { text: `redis Up 14 days 6379/tcp`, color: '#e8f4f8' }, ]}, // ── tailscale status ───────────────────────────────────── { cmd: `tailscale status 2>/dev/null`, out: [ { text: `100.x.x.x ${HOST} tagged-devices active; relay "mia", tx 1.4MiB rx 892KiB`, color: '#00f5c8' }, ]}, // ── temp / uptime ──────────────────────────────────────── { cmd: `uptime && sensors 2>/dev/null | grep -E 'Core|temp'`, out: [ { text: ` ${bootTs} up ${upSeed} days, 1 user, load average: 0.12, 0.18, 0.21`, color: '#e8f4f8' }, { text: `Core 0: +38.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' }, { text: `Core 1: +37.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' }, { text: `Core 2: +39.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' }, { text: `Core 3: +38.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' }, ]}, // ── final ──────────────────────────────────────────────── { cmd: `echo "all systems nominal — welcome back, ${USER}"`, out: [ { text: ``, color: '' }, { text: ` ██╗ ██╗ █████╗ ██╗ ██╗████████╗ ██████╗ ██████╗ `, color: '#00f5c8' }, { text: ` ██║ ██║██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗`, color: '#00f5c8' }, { text: ` ██║ █╗ ██║███████║██║ █╗ ██║ ██║ ██║ ██║██████╔╝`, color: '#00c49e' }, { text: ` ██║███╗██║██╔══██║██║███╗██║ ██║ ██║ ██║██╔══██╗`, color: '#00c49e' }, { text: ` ╚███╔███╔╝██║ ██║╚███╔███╔╝ ██║ ╚██████╔╝██║ ██║`, color: '#009e7e' }, { text: ` ╚══╝╚══╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝`, color: '#009e7e' }, { text: ``, color: '' }, { text: ` all systems nominal — welcome back, ${USER}`, color: '#ffb800' }, { text: ``, color: '' }, ]}, ]; /* ── Build overlay ──────────────────────────────────────── */ const overlay = document.createElement('div'); overlay.id = 'wawtor-boot'; overlay.style.cssText = ` position:fixed; inset:0; z-index:99999; background:#0b0d0f; display:flex; flex-direction:column; font-family:'Share Tech Mono',monospace; font-size:0.8rem; line-height:1.7; overflow:hidden; `; /* terminal title bar */ const titleBar = document.createElement('div'); titleBar.style.cssText = ` flex-shrink:0; display:flex; align-items:center; gap:8px; padding:8px 16px; background:#161a1e; border-bottom:1px solid rgba(0,245,200,0.12); `; titleBar.innerHTML = ` <span style="width:10px;height:10px;border-radius:50%;background:#ff5f57;display:inline-block"></span> <span style="width:10px;height:10px;border-radius:50%;background:#ffbd2e;display:inline-block"></span> <span style="width:10px;height:10px;border-radius:50%;background:#28c840;display:inline-block"></span> <span style="margin-left:12px;color:#3d6070;font-size:0.72rem;letter-spacing:0.14em"> ${USER}@${HOST}: ~ </span> `; overlay.appendChild(titleBar); /* scrollable terminal body */ const body = document.createElement('div'); body.style.cssText = ` flex:1; overflow-y:auto; padding:18px 24px 24px; scroll-behavior:smooth; `; /* scanlines inside terminal */ body.style.backgroundImage = `repeating-linear-gradient( to bottom, transparent, transparent 2px, rgba(0,0,0,0.15) 2px, rgba(0,0,0,0.15) 4px )`; overlay.appendChild(body); document.body.appendChild(overlay); /* ── Rendering helpers ──────────────────────────────────── */ function appendLine(text, color, extraStyle = '') { const el = document.createElement('div'); el.style.cssText = `color:${color || '#e8f4f8'};white-space:pre;${extraStyle}`; el.textContent = text; body.appendChild(el); body.scrollTop = body.scrollHeight; return el; } function appendPrompt(cmd) { const row = document.createElement('div'); row.style.cssText = `display:flex; align-items:baseline; white-space:pre;`; row.innerHTML = ` <span style="color:#00f5c8">${USER}@${HOST}</span> <span style="color:#7aaabb">:</span> <span style="color:#4a9eff">~</span> <span style="color:#7aaabb">$ </span> <span id="cmd-target" style="color:#e8f4f8"></span> <span class="blink-cursor" style=" display:inline-block;width:8px;height:1em; background:#00f5c8;margin-left:1px; animation:blinkCursor 0.75s step-end infinite; vertical-align:text-bottom; "></span> `; body.appendChild(row); body.scrollTop = body.scrollHeight; return { target: row.querySelector('#cmd-target'), cursor: row.querySelector('.blink-cursor') }; } /* ── Sequencer ──────────────────────────────────────────── */ let scriptIdx = 0; // Generate a plausible-looking MAC address seeded to this browser function genMac() { const seed = navigator.userAgent + screen.width + screen.height + navigator.language; let hash = 0; for (let i = 0; i < seed.length; i++) { hash = ((hash << 5) - hash) + seed.charCodeAt(i); hash |= 0; } const hex = Math.abs(hash).toString(16).padStart(12, '0'); // Set locally administered bit on first octet for realism const octs = []; for (let i = 0; i < 12; i += 2) octs.push(hex.slice(i, i + 2)); octs[0] = (parseInt(octs[0], 16) | 0x02 & ~0x01).toString(16).padStart(2,'0'); return octs.join(':').toUpperCase(); } const CLIENT_MAC = genMac(); function runNext() { if (scriptIdx >= SCRIPT.length) { // Show MAC warning prompt instead of auto-fading showMacPrompt(); return; } const block = SCRIPT[scriptIdx++]; if (block.cmd === null) { typeOutputLines(block.out, 0, runNext); } else { const { target, cursor } = appendPrompt(''); typeCommand(block.cmd, target, cursor, () => { cursor.remove(); setTimeout(() => typeOutputLines(block.out, 0, runNext), 40); }); } } function showMacPrompt() { appendLine('', ''); // Warning box const warn = document.createElement('div'); warn.style.cssText = ` margin:12px 0; border:1px solid rgba(255,184,0,0.5); border-left:3px solid #ffb800; border-radius:4px; padding:14px 18px; background:rgba(255,184,0,0.05); color:#ffb800; font-family:'Share Tech Mono',monospace; font-size:0.78rem; line-height:1.9; `; warn.innerHTML = ` <div style="color:#ffb800;font-size:0.7rem;letter-spacing:0.22em;margin-bottom:8px;"> ⚠ SECURITY NOTICE — WAWTOR ACCESS CONTROL </div> <div style="color:#e8f4f8;">This terminal session is monitored and logged.</div> <div style="color:#e8f4f8;">By continuing you acknowledge that the following</div> <div style="color:#e8f4f8;">client identifier has been recorded:</div> <div style="margin-top:10px;color:#00f5c8;letter-spacing:0.12em;"> MAC ADDRESS <span style="color:#ffb800;font-size:0.9rem;">${CLIENT_MAC}</span> </div> <div style="margin-top:2px;color:#3d6070;font-size:0.65rem;"> Timestamp: ${new Date().toISOString()} </div> `; body.appendChild(warn); body.scrollTop = body.scrollHeight; // Prompt row appendLine('', ''); const promptRow = document.createElement('div'); promptRow.style.cssText = ` display:flex; align-items:center; gap:14px; font-family:'Share Tech Mono',monospace; font-size:0.78rem; `; promptRow.innerHTML = ` <span style="color:#7aaabb;">Continue to dashboard?</span> <button id="boot-accept" style=" font-family:'Orbitron',monospace; font-size:0.62rem; letter-spacing:0.18em; text-transform:uppercase; padding:6px 20px; border:1px solid rgba(0,245,200,0.5); border-radius:3px; background:rgba(0,245,200,0.08); color:#00f5c8; cursor:pointer; transition:all 0.2s ease; ">[ ACCEPT & CONTINUE ]</button> <button id="boot-decline" style=" font-family:'Orbitron',monospace; font-size:0.62rem; letter-spacing:0.18em; text-transform:uppercase; padding:6px 20px; border:1px solid rgba(255,58,92,0.3); border-radius:3px; background:rgba(255,58,92,0.05); color:#ff3a5c; cursor:pointer; transition:all 0.2s ease; ">[ DISCONNECT ]</button> `; body.appendChild(promptRow); body.scrollTop = body.scrollHeight; const acceptBtn = qs('#boot-accept'); const declineBtn = qs('#boot-decline'); acceptBtn.onmouseenter = () => { acceptBtn.style.background = 'rgba(0,245,200,0.18)'; acceptBtn.style.boxShadow = '0 0 16px rgba(0,245,200,0.2)'; }; acceptBtn.onmouseleave = () => { acceptBtn.style.background = 'rgba(0,245,200,0.08)'; acceptBtn.style.boxShadow = 'none'; }; declineBtn.onmouseenter = () => { declineBtn.style.background = 'rgba(255,58,92,0.12)'; }; declineBtn.onmouseleave = () => { declineBtn.style.background = 'rgba(255,58,92,0.05)'; }; acceptBtn.onclick = () => { promptRow.innerHTML = `<span style="color:#00f5c8;">Access granted. Loading dashboard...</span>`; body.scrollTop = body.scrollHeight; setTimeout(() => { overlay.style.transition = 'opacity 0.8s ease'; overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 820); }, 600); }; declineBtn.onclick = () => { promptRow.innerHTML = `<span style="color:#ff3a5c;">Session terminated. Goodbye.</span>`; body.scrollTop = body.scrollHeight; setTimeout(() => { document.body.style.transition = 'opacity 0.5s ease'; document.body.style.opacity = '0'; }, 800); }; } function typeCommand(cmd, target, cursor, done) { let i = 0; function step() { if (i >= cmd.length) { done(); return; } target.textContent += cmd[i++]; body.scrollTop = body.scrollHeight; // Faster: 8-18ms per character instead of 28-66ms setTimeout(step, 8 + Math.random() * 10); } // Shorter initial pause: 60-120ms instead of 180-300ms setTimeout(step, 60 + Math.random() * 60); } function typeOutputLines(lines, idx, done) { if (idx >= lines.length) { done(); return; } const { text, color } = lines[idx]; appendLine(text, color); // Faster: 8-14ms per line instead of 22-40ms const delay = text.trim() === '' ? 8 : 8 + Math.random() * 6; setTimeout(() => typeOutputLines(lines, idx + 1, done), delay); } /* inject blinkCursor keyframe if not present */ if (!document.querySelector('#wawtor-boot-kf')) { const s = document.createElement('style'); s.id = 'wawtor-boot-kf'; s.textContent = ` @keyframes blinkCursor { 0%,100% { opacity:1; } 50% { opacity:0; } } `; document.head.appendChild(s); } /* kick it off */ setTimeout(runNext, 120); } /* ===================================================== 8. AMBIENT GLOW TRAIL ===================================================== */ function initGlowTrail() { const trail = []; const N = 10; for (let i = 0; i < N; i++) { const el = document.createElement('div'); el.style.cssText = ` position:fixed; width:${6 - i*0.4}px; height:${6 - i*0.4}px; background:rgba(0,245,200,${0.28 - i*0.024}); border-radius:50%; pointer-events:none; z-index:99990; transition:left ${0.04 + i*0.04}s ease, top ${0.04 + i*0.04}s ease; transform:translate(-50%,-50%); `; document.body.appendChild(el); trail.push(el); } on(document, 'mousemove', e => { trail.forEach(el => { el.style.left = e.clientX + 'px'; el.style.top = e.clientY + 'px'; }); }); } /* ===================================================== 9. INJECT REQUIRED KEYFRAMES ===================================================== */ function injectKeyframes() { const style = document.createElement('style'); style.textContent = ` @keyframes rippleAnim { to { transform:scale(1); opacity:0; } } @keyframes cursorBurst { 0% { transform:scale(0.5); opacity:1; } 100% { transform:scale(2.5); opacity:0; } } `; document.head.appendChild(style); } /* ===================================================== 10. OBSERVER: re-init on dynamic card injection ===================================================== */ function initObserver() { const ob = new MutationObserver(() => { initCornerBrackets(); initCardTilt(); }); ob.observe(document.body, { childList: true, subtree: true }); } /* ===================================================== 11. KEYBOARD SHORTCUT OVERLAY (press ?) ===================================================== */ function initShortcutHelp() { const shortcuts = [ ['?', 'Toggle this overlay'], ['Ctrl+K', 'Focus search'], ['Escape', 'Close modal / search'], ]; const overlay = document.createElement('div'); overlay.id = 'hd-shortcuts'; overlay.style.cssText = ` display:none; position:fixed; inset:0; background:rgba(3,5,8,0.92); z-index:99000; justify-content:center; align-items:center; backdrop-filter:blur(12px); `; const box = document.createElement('div'); box.style.cssText = ` background:#0a1520; border:1px solid rgba(0,245,200,0.3); border-radius:8px; padding:36px 48px; min-width:380px; box-shadow:0 0 60px rgba(0,245,200,0.15); `; box.innerHTML = ` <div style="font-family:'Orbitron',monospace;font-size:0.7rem; letter-spacing:0.24em;color:#00f5c8;margin-bottom:24px; text-transform:uppercase;text-shadow:0 0 10px #00f5c8"> ⌨ KEY BINDINGS </div> ${shortcuts.map(([k,v]) => ` <div style="display:flex;justify-content:space-between; margin-bottom:14px;font-family:'Share Tech Mono',monospace; font-size:0.78rem;"> <span style="background:rgba(0,245,200,0.1);border:1px solid rgba(0,245,200,0.25); border-radius:3px;padding:3px 10px;color:#00f5c8">${k}</span> <span style="color:#7aaabb;align-self:center">${v}</span> </div> `).join('')} <div style="margin-top:22px;font-family:'Share Tech Mono',monospace; font-size:0.65rem;color:#3d6070;text-transform:uppercase; letter-spacing:0.1em">Press ESC or ? to close</div> `; overlay.appendChild(box); document.body.appendChild(overlay); let open = false; function toggle() { open = !open; overlay.style.display = open ? 'flex' : 'none'; } on(document, 'keydown', e => { if (e.key === '?' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName)) toggle(); if (e.key === 'Escape' && open) toggle(); }); on(overlay, 'click', e => { if (e.target === overlay) toggle(); }); } /* ─── BOOT ORDER ─────────────────────────────────────────────── */ function init() { injectKeyframes(); initBootSequence(); initParticles(); initCursor(); initHudBar(); initTerminalWidget(); initCornerBrackets(); initCardTilt(); initRipple(); initGlowTrail(); initObserver(); initShortcutHelp(); } if (document.readyState === 'loading') { on(document, 'DOMContentLoaded', init); } else { init(); } })();
Home dashboard
Users
Application list
Tags list
Settings