Files
GeoSentinel/wifi-search.html
2026-03-08 21:22:44 +05:30

2163 lines
72 KiB
HTML
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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Surveillance Dashboard</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<title>HayOS - H9 Dashboard</title>
<link rel="icon" href="https://haybnz.web.app/logo.png" type="image/x-icon">
<link href="https://fonts.googleapis.com/css2?family=Orbitron&family=Roboto+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<title>WiFi Search - HayOS</title>
<link rel="icon" href="https://haybnz.web.app/logo.png" type="image/x-icon">
<!-- Leaflet CSS from CDN -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet MarkerCluster CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" />
<script>
document.addEventListener("DOMContentLoaded", function () {
const startTime = Date.now();
const username = document.cookie.split('; ').find(row => row.startsWith('username='))?.split('=')[1] || 'anonymous';
const page = window.location.pathname;
function sendActivity(event, details = '') {
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
fetch('/log-activity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username,
page: page,
event: event,
details: details,
duration: duration
})
});
}
// Log page view
sendActivity('page_view');
// Log clicks
document.addEventListener('click', function (e) {
const target = e.target.closest('button, a, input[type="submit"]');
if (target) {
const text = (target.innerText || target.value || target.href || '').substring(0, 100);
sendActivity('click', text);
}
});
// Log before unload (time spent)
window.addEventListener('beforeunload', function () {
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
navigator.sendBeacon('/log-activity', JSON.stringify({
username: username,
page: page,
event: 'page_leave',
duration: duration
}));
});
});
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
:root {
--primary-color: #00FFD1;
/* Cyber Cyan */
--secondary-color: #00A3FF;
/* Electric Blue */
--accent-color: #00E5FF;
/* Soft Cyan */
--bg-dark: #020a0a;
/* Deep Void */
--glass-bg: rgba(5, 20, 20, 0.85);
--card-bg: rgba(0, 255, 209, 0.03);
--border-color: rgba(0, 255, 209, 0.2);
--text-main: #00FFD1;
--text-dim: rgba(0, 255, 209, 0.6);
--neon-glow: 0 0 15px rgba(0, 255, 209, 0.4);
--danger: #FF1744;
--success: #00E676;
--font-main: 'Share Tech Mono', 'Courier New', monospace;
}
body.sat-mode {
--primary-color: #FF00FF;
--secondary-color: #BC13FE;
--accent-color: #FF0055;
--text-main: #FFB3FF;
--text-dim: rgba(255, 179, 255, 0.6);
--border-color: rgba(255, 0, 255, 0.3);
--glass-bg: rgba(40, 0, 40, 0.85);
--neon-glow: 0 0 15px rgba(255, 0, 255, 0.4);
}
/* ---------- Base / Theme ---------- */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
font-family: var(--font-main);
background: var(--bg-dark);
color: var(--text-main);
overflow-x: hidden;
}
.bg-grid {
position: fixed;
inset: 0;
z-index: 0;
background-image:
repeating-linear-gradient(0deg, transparent, transparent 49px, rgba(0, 255, 209, 0.03) 49px, rgba(0, 255, 209, 0.03) 50px),
repeating-linear-gradient(90deg, transparent, transparent 49px, rgba(0, 255, 209, 0.03) 49px, rgba(0, 255, 209, 0.03) 50px);
animation: gridMove 20s linear infinite;
}
@keyframes gridMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
/* ---------- Top Navigation ---------- */
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: rgba(2, 10, 10, 0.9);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 1100;
}
.left-group {
display: flex;
align-items: center;
gap: 15px;
}
.logo-section {
display: flex;
gap: 12px;
align-items: center;
}
.logo-text h2 {
font-size: 18px;
letter-spacing: 2px;
text-transform: uppercase;
text-shadow: var(--neon-glow);
}
.logo-text p {
font-size: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
.nav-stats {
display: flex;
gap: 15px;
align-items: center;
}
.stat-item {
display: flex;
gap: 8px;
align-items: center;
padding: 6px 12px;
background: rgba(0, 255, 209, 0.05);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 12px;
text-transform: uppercase;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 10px var(--success);
}
/* Cyber Checkboxes */
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin: 15px 0;
justify-content: center;
background: rgba(0, 255, 209, 0.02);
padding: 10px;
border-bottom: 1px solid var(--border-color);
}
.cyber-check {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 11px;
color: var(--text-dim);
transition: 0.3s;
user-select: none;
}
.cyber-check input {
display: none;
}
.cyber-check .box {
width: 14px;
height: 14px;
border: 1px solid var(--border-color);
position: relative;
background: rgba(0, 0, 0, 0.3);
}
.cyber-check input:checked+.box {
background: var(--primary-color);
box-shadow: var(--neon-glow);
}
.cyber-check input:checked+.box::after {
content: "\f00c";
font-family: "Font Awesome 5 Free";
font-weight: 900;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #000;
font-size: 8px;
}
.cyber-check:hover {
color: var(--primary-color);
}
.cyber-check.active {
color: var(--primary-color);
text-shadow: var(--neon-glow);
}
.mode-label {
font-size: 12px;
letter-spacing: 2px;
color: var(--text-dim);
transition: 0.3s;
}
.mode-label.active {
color: var(--primary-color);
text-shadow: var(--neon-glow);
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: .4s;
border: 1px solid var(--border-color);
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 26px;
left: 4px;
bottom: 3px;
background-color: var(--text-dim);
transition: .4s;
}
input:checked+.slider {
background-color: rgba(0, 255, 209, 0.1);
}
input:checked+.slider:before {
transform: translateX(26px);
background-color: var(--primary-color);
box-shadow: var(--neon-glow);
}
/* ---------- Sidebar ---------- */
.sidebar {
position: fixed;
top: 60px;
left: 0;
width: 250px;
height: calc(100vh - 60px);
background: rgba(2, 10, 10, 0.95);
backdrop-filter: blur(15px);
border-right: 1px solid var(--border-color);
padding: 20px 0;
overflow-y: auto;
z-index: 1000;
transition: 0.3s;
}
.menu-title {
padding: 15px 20px 8px;
font-size: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 2px;
border-bottom: 1px solid rgba(0, 255, 209, 0.05);
margin-bottom: 10px;
}
.menu-item {
display: flex;
align-items: center;
gap: 15px;
padding: 12px 20px;
color: var(--text-dim);
transition: 0.3s;
cursor: pointer;
border-left: 2px solid transparent;
text-transform: uppercase;
font-size: 13px;
}
.menu-item:hover,
.menu-item.active {
color: var(--primary-color);
background: rgba(0, 255, 209, 0.05);
border-left-color: var(--primary-color);
text-shadow: var(--neon-glow);
}
.menu-icon {
width: 18px;
height: 18px;
}
/* ---------- Main Content ---------- */
.container {
margin-top: 60px;
margin-left: 250px;
padding: 30px;
position: relative;
z-index: 1;
}
.page-header h1 {
font-size: 24px;
text-transform: uppercase;
letter-spacing: 4px;
margin-bottom: 10px;
text-shadow: var(--neon-glow);
}
.page-header p {
font-size: 12px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 30px;
}
.search-bar {
display: flex;
gap: 15px;
background: var(--glass-bg);
padding: 20px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-bottom: 25px;
backdrop-filter: blur(10px);
justify-content: center;
}
.search-bar select,
.search-bar input {
background: rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
color: #fff;
padding: 12px 15px;
font-family: var(--font-main);
font-size: 14px;
}
.search-bar select:focus,
.search-bar input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(0, 255, 209, 0.2);
}
.search-bar button {
background: rgba(0, 255, 209, 0.1);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 12px 30px;
cursor: pointer;
text-transform: uppercase;
font-weight: bold;
letter-spacing: 2px;
transition: 0.3s;
clip-path: polygon(10% 0, 100% 0, 100% 70%, 90% 100%, 0 100%, 0 30%);
font-family: var(--font-main);
}
#search-type,
#search-input {
background: rgba(0, 0, 0, 0.7);
border: 1px solid var(--border-color);
color: var(--primary-color);
padding: 12px 15px;
font-family: var(--font-main);
font-size: 14px;
outline: none;
transition: 0.3s;
box-shadow: inset 0 0 10px rgba(0, 255, 209, 0.05);
}
#search-type:focus,
#search-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 15px rgba(0, 255, 209, 0.3), inset 0 0 10px rgba(0, 255, 209, 0.1);
}
#search-type option {
background: #020a0a;
color: var(--primary-color);
}
.search-bar button:hover {
background: var(--primary-color);
color: #000;
box-shadow: var(--neon-glow);
transform: translateY(-2px);
}
/* Surveillance Grid Layout */
.surveillance-grid {
display: grid;
grid-template-columns: 350px 1fr;
gap: 20px;
margin-bottom: 25px;
height: 65vh;
}
.results-sidebar {
background: var(--glass-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
overflow-y: auto;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.4);
}
.map-container {
border: 2px solid var(--border-color);
position: relative;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.map-container::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 255, 209, 0.05) 50%),
linear-gradient(90deg, rgba(255, 0, 0, 0.02), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.02));
background-size: 100% 4px, 3px 100%;
pointer-events: none;
z-index: 1000;
}
#map {
height: 100%;
width: 100%;
}
/* Node Item in Sidebar */
.node-item {
background: rgba(0, 255, 209, 0.03);
border: 1px solid rgba(0, 255, 209, 0.1);
padding: 16px;
cursor: pointer;
transition: 0.3s;
position: relative;
overflow: hidden;
}
.node-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
background: var(--primary-color);
opacity: 0;
transition: 0.3s;
}
.node-item:hover {
background: rgba(0, 255, 209, 0.08);
border-color: var(--primary-color);
transform: translateX(5px);
}
.node-item:hover::before {
opacity: 1;
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.node-name {
font-size: 11px;
font-weight: bold;
color: var(--text-main);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-type {
font-size: 9px;
background: rgba(0, 255, 209, 0.15);
padding: 2px 6px;
border-radius: 2px;
color: var(--primary-color);
text-transform: uppercase;
}
.node-meta {
font-size: 9px;
color: var(--text-dim);
}
.signal-meter {
height: 3px;
background: rgba(0, 255, 209, 0.1);
margin-top: 8px;
width: 100%;
border-radius: 2px;
}
.signal-strength {
height: 100%;
background: var(--primary-color);
box-shadow: 0 0 5px var(--primary-color);
}
@media (max-width: 1100px) {
.surveillance-grid {
grid-template-columns: 1fr;
height: auto;
}
.results-sidebar {
height: 300px;
order: 2;
}
.map-container {
height: 50vh;
order: 1;
}
}
.toast {
position: fixed;
bottom: 30px;
right: 30px;
background: var(--danger);
color: #fff;
padding: 15px 30px;
border-radius: 4px;
display: none;
text-transform: uppercase;
font-weight: bold;
font-size: 12px;
z-index: 2000;
box-shadow: 0 0 20px rgba(255, 23, 68, 0.4);
}
/* ---------- Custom Leaflet Popup ---------- */
.leaflet-popup-content-wrapper {
background: var(--glass-bg);
color: var(--text-main);
border: 1px solid var(--border-color);
border-radius: 0;
}
.leaflet-popup-tip {
background: var(--glass-bg);
border: 1px solid var(--border-color);
}
.leaflet-container a.leaflet-popup-close-button {
color: var(--primary-color);
}
/* ---------- Responsive ---------- */
@media (max-width: 968px) {
.container {
margin-left: 0;
padding: 20px;
}
.sidebar {
transform: translateX(-100%);
width: 260px;
}
.sidebar.open {
transform: translateX(0);
}
.hamburger {
display: flex !important;
}
.nav-stats {
display: none;
}
.search-bar {
flex-direction: column;
}
}
.hamburger {
display: none;
width: 40px;
height: 40px;
border: 1px solid var(--border-color);
background: rgba(0, 255, 209, 0.05);
align-items: center;
justify-content: center;
cursor: pointer;
}
.hamburger span {
width: 24px;
height: 2px;
background: var(--primary-color);
position: relative;
transition: 0.3s;
}
.hamburger span::before,
.hamburger span::after {
content: '';
position: absolute;
width: 100%;
height: 2px;
background: var(--primary-color);
left: 0;
transition: 0.3s;
}
.hamburger span::before {
top: -8px;
}
.hamburger span::after {
top: 8px;
}
.hamburger.open span {
background: transparent;
}
.hamburger.open span::before {
transform: rotate(45deg);
top: 0;
}
.hamburger.open span::after {
transform: rotate(-45deg);
top: 0;
}
.sidebar-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 950;
}
.sidebar-backdrop.show {
display: block;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-dark);
}
/* Additional styles for existing elements not explicitly covered by the new CSS, adapted to the theme */
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
padding: 20px 0;
}
.card {
background: var(--glass-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 16px;
color: var(--text-main);
backdrop-filter: blur(10px);
}
.card h2 {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
text-shadow: var(--neon-glow);
}
.card h2 i {
color: var(--primary-color);
}
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px;
}
.app-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12px;
background: rgba(0, 255, 209, 0.05);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.app-item:hover {
background: rgba(0, 255, 209, 0.1);
transform: translateY(-2px);
box-shadow: var(--neon-glow);
}
.app-item i {
font-size: 24px;
margin-bottom: 8px;
color: var(--primary-color);
/* Changed from yellow */
}
.app-item span {
font-size: 12px;
color: var(--text-main);
}
.bot-status p {
font-size: 13px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.bot-status p i {
color: var(--primary-color);
}
#coords {
font-size: 13px;
color: var(--primary-color);
/* Changed from yellow */
margin-bottom: 12px;
}
#history div {
font-size: 12px;
color: var(--text-dim);
margin-bottom: 6px;
}
#results {
margin-top: 12px;
max-height: 200px;
overflow-y: auto;
padding: 10px;
background: rgba(4, 43, 43, 0.8);
border: 2px solid var(--border-color);
border-radius: 4px;
color: var(--primary-color);
/* Changed from yellow */
}
.device {
background: rgba(0, 255, 209, 0.1);
border: 1px solid var(--primary-color);
padding: 8px;
margin-bottom: 8px;
border-radius: 4px;
font-size: 12px;
}
.refresh-btn {
cursor: pointer;
font-size: 18px;
color: var(--primary-color);
/* Changed from yellow */
margin-top: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn i {
color: var(--primary-color);
/* Changed from yellow */
}
/* Spotify Mini Player */
#spotify {
background: transparent;
color: var(--text-main);
}
.track-info {
background: transparent;
border-radius: 4px;
padding: 12px;
max-width: 280px;
}
.track-info>img {
width: 50px;
height: 50px;
border-radius: 4px;
border: 1px solid var(--primary-color);
object-fit: cover;
float: left;
margin-right: 10px;
margin-bottom: 10px;
}
.track-info h3 {
font-size: 13px;
font-weight: 600;
color: var(--text-main);
margin-bottom: 3px;
margin-top: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track-info>p {
font-size: 10px;
color: var(--text-dim);
opacity: 0.7;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin: 10px 0 8px 0;
clear: both;
}
.controls button {
background: transparent;
border: 1.5px solid var(--primary-color);
color: var(--primary-color);
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
transition: all 0.3s;
}
.controls button:hover {
background: rgba(0, 255, 209, 0.1);
transform: scale(1.05);
}
#play-button,
#pause-button {
width: 34px;
height: 34px;
background: var(--primary-color);
color: var(--bg-dark);
font-size: 12px;
border: none;
}
#play-button:hover,
#pause-button:hover {
background: var(--primary-color);
opacity: 0.9;
}
.progress-container {
width: 100%;
margin: 8px 0;
}
.progress-bar {
width: 100%;
height: 3px;
background: rgba(0, 255, 209, 0.2);
border-radius: 2px;
position: relative;
overflow: hidden;
cursor: pointer;
}
.progress-fill {
height: 100%;
background: var(--primary-color);
border-radius: 2px;
transition: width 0.3s ease;
}
.device-selector {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(0, 255, 209, 0.3);
}
.device-selector h4 {
font-size: 11px;
color: var(--text-main);
margin-bottom: 6px;
font-weight: 600;
}
.device-selector ul {
list-style: none;
padding: 0;
margin-bottom: 6px;
max-height: 60px;
overflow-y: auto;
}
.device-selector li {
padding: 5px 8px;
color: var(--text-main);
font-size: 9px;
margin-bottom: 3px;
background: rgba(0, 255, 209, 0.05);
border-radius: 3px;
border: 1px solid var(--border-color);
}
#device-select {
width: 100%;
padding: 6px 8px;
background: transparent;
border: 1.5px solid var(--primary-color);
color: var(--text-main);
border-radius: 4px;
font-size: 10px;
margin-bottom: 6px;
cursor: pointer;
outline: none;
}
#device-select option {
background: var(--bg-dark);
color: var(--text-main);
}
#change-device {
width: 100%;
padding: 6px 10px;
background: transparent;
border: 1.5px solid var(--primary-color);
color: var(--text-main);
border-radius: 4px;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
#change-device:hover {
background: var(--primary-color);
color: var(--bg-dark);
}
#spotify>p {
color: var(--text-main);
text-align: center;
font-size: 12px;
opacity: 0.7;
padding: 20px;
}
/* Responsive Adjustments */
@media (max-width: 1200px) {
.dashboard {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.apps-grid {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
}
}
@media (max-width: 968px) {
.dashboard {
grid-template-columns: 1fr;
}
.apps-grid {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
}
}
@media (max-width: 420px) {
.topnav {
padding: 0 10px;
}
.logo-text h2 {
font-size: 14px;
}
.logo-text p {
display: none;
}
.app-item span {
font-size: 11px;
}
}
/* Main Content specific styles */
.main-content {
padding: 20px;
overflow-y: auto;
min-height: calc(100vh - 60px);
}
/* Controls and Cards */
.controls {
background: var(--glass-bg);
padding: 16px;
border-radius: 4px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
backdrop-filter: blur(10px);
}
.search-input {
flex: 1;
padding: 12px 14px;
border-radius: 4px;
border: 1px solid rgba(0, 255, 209, 0.14);
background: rgba(0, 255, 209, 0.03);
color: var(--text-main);
}
.search-btn {
padding: 12px 18px;
border-radius: 4px;
background: linear-gradient(135deg, var(--primary-color), var(--accent-color));
color: var(--bg-dark);
border: none;
cursor: pointer;
}
.filters {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-chip {
padding: 8px 12px;
border-radius: 20px;
border: 1px solid var(--border-color);
background: rgba(0, 255, 209, 0.03);
color: var(--text-dim);
cursor: pointer;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.device-card {
background: var(--glass-bg);
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(0, 255, 209, 0.08);
transition: all 0.25s;
overflow: hidden;
backdrop-filter: blur(10px);
}
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.device-name {
font-size: 15px;
font-weight: 600;
}
.device-code {
font-size: 11px;
color: var(--text-dim);
font-family: monospace;
}
.card-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 12px;
}
.action-btn {
padding: 10px;
border-radius: 4px;
border: 1px solid var(--border-color);
background: rgba(0, 255, 209, 0.03);
color: var(--text-main);
cursor: pointer;
font-weight: 600;
}
/* Map Popup */
.map-popup {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
z-index: 2000;
}
.map-popup-content {
background: var(--glass-bg);
padding: 14px;
border-radius: 4px;
width: 600px;
max-width: 95%;
}
.sidebar-backdrop {
display: none;
position: fixed;
inset: 60px 0 0 0;
background: rgba(0, 0, 0, 0.5);
z-index: 950;
}
.sidebar-backdrop.show {
display: block;
}
.main-content {
padding: 12px;
}
.cards-grid {
grid-template-columns: 1fr;
}
.card-actions {
grid-template-columns: 1fr;
}
}
@media (max-width: 420px) {
.topnav {
padding: 0 10px;
}
.logo-text h2 {
font-size: 14px;
}
.logo-text p {
display: none;
}
}
/* Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 255, 255, 0.2);
border-radius: 6px;
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<!-- Top Navigation -->
<nav class="topnav">
<div class="left-group">
<button class="hamburger" id="hamburger" aria-label="Open menu" aria-expanded="false">
<span></span>
</button>
<div class="logo-section" onclick="window.location.href='/'" style="cursor: pointer;">
<div class="logo-icon" aria-hidden="true">
<svg width="40" height="40" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(10, 15)">
<path
d="M30 30 C25 15, 35 10, 40 15 C45 10, 55 15, 50 30 C55 45, 45 50, 40 45 C35 50, 25 45, 30 30"
fill="none" stroke="#00FF88" stroke-width="4" />
<path d="M30 30 C25 20, 20 15, 15 10" stroke="#00FF88" stroke-width="3" fill="none" />
<path d="M50 30 C55 20, 60 15, 65 10" stroke="#00FF88" stroke-width="3" fill="none" />
<path d="M30 30 C25 40, 20 45, 15 50" stroke="#00FF88" stroke-width="3" fill="none" />
<path d="M50 30 C55 40, 60 45, 65 50" stroke="#00FF88" stroke-width="3" fill="none" />
<circle cx="30" cy="30" r="5" fill="none" stroke="#00FF88" stroke-width="2" opacity="0.0" />
<circle cx="50" cy="30" r="5" fill="none" stroke="#00FF88" stroke-width="2" opacity="0.0" />
<circle cx="15" cy="10" r="3" fill="none" stroke="#00FF88" stroke-width="1.5"
opacity="0.0" />
<circle cx="65" cy="10" r="3" fill="none" stroke="#00FF88" stroke-width="1.5"
opacity="0.0" />
<circle cx="15" cy="50" r="3" fill="none" stroke="#00FF88" stroke-width="1.5"
opacity="0.0" />
<circle cx="65" cy="50" r="3" fill="none" stroke="#00FF88" stroke-width="1.5"
opacity="0.0" />
</g>
</svg>
</div>
<div class="logo-text">
<h2>HayOS H9 EYE</h2>
<p id="user-info">Created by Hayden |</p>
<script>
(async function () {
try {
const res = await fetch('/api/username', { cache: 'no-store' });
const data = await res.json();
const userInfo = document.getElementById('user-info');
const deviceCount = document.getElementById('device-counts');
const statItem = document.querySelector('.stat-item');
if (data && data.username) {
// ✅ Insert username inside <p id="user-info">
userInfo.innerHTML = `Created by Hayden | ${data.username}`;
// ✅ Update device count
deviceCount.textContent = 'Online';
// ✅ Add logout icon inside stat-item
const logoutBtn = document.createElement('a');
logoutBtn.href = '/logout';
logoutBtn.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
logoutBtn.title = 'Logout';
logoutBtn.style.marginLeft = '10px';
logoutBtn.style.color = '#00FFFF';
logoutBtn.style.cursor = 'pointer';
logoutBtn.style.textDecoration = 'none';
logoutBtn.style.fontSize = '16px';
logoutBtn.addEventListener('mouseenter', () => logoutBtn.style.color = '#FF4444');
logoutBtn.addEventListener('mouseleave', () => logoutBtn.style.color = '#00FFFF');
statItem.appendChild(logoutBtn);
} else {
userInfo.innerHTML = 'Created by Hayden | Guest';
deviceCount.textContent = 'Offline';
}
} catch (err) {
console.warn('Failed to fetch username:', err);
const userInfo = document.getElementById('user-info');
const deviceCount = document.getElementById('device-count');
if (userInfo) userInfo.innerHTML = 'Created by Hayden | Guest';
if (deviceCount) deviceCount.textContent = 'Offline';
}
})();
</script>
</div>
</div>
</div>
<div class="nav-stats">
<div class="stat-item">
<div class="status-dot" aria-hidden="true"></div>
<span id="device-counts">Loading...</span>
</div>
<span id="error-message" style="display:none;margin-left:12px;color:#FF6B6B;font-size:13px;"></span>
</div>
</nav>
<!-- Sidebar backdrop (mobile) -->
<div class="sidebar-backdrop" id="sidebar-backdrop" aria-hidden="true"></div>
<div class="container">
<!-- Vertical Sidebar -->
<aside class="sidebar" id="sidebar" aria-label="Primary navigation">
<div class="menu-group">
<div class="menu-title">Main</div>
<div class="menu-item active" data-href="/crmx"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg><span>Dashboard</span></div>
<div class="menu-item" data-href="/surveillance"><svg class="menu-icon" fill="none"
stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg><span>Surveillance</span></div>
<div class="menu-item" id="tor-chat" data-href="">
<svg class="menu-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
<span>Admin Pannel</span>
</div>
<div class="menu-item" data-href="/profiles"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
</svg><span>Profiles</span></div>
<div class="menu-item" data-href="/profiles"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg><span>Local Tor Chat</span></div>
<div class="menu-item" data-href="/geo"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg><span>Geo Tracking</span></div>
</div>
<div class="menu-group">
<div class="menu-title">Data Sources</div>
<div class="menu-item" data-href="/cctv"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path
d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" />
<circle cx="12" cy="13" r="3" />
</svg><span>CCTV Feeds</span></div>
<div class="menu-item" data-href="/mobile"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg><span>Mobile Tracking</span></div>
<div class="menu-item" data-href="/social"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg><span>Social Media</span></div>
</div>
<div class="menu-group">
<div class="menu-title">System</div>
<div class="menu-item" data-href="/alerts"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
</svg><span>Alerts</span></div>
<div class="menu-item" data-href="fit"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" />
<path d="M8 12 L10 14 L16 8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
stroke-linejoin="round" fill="none" />
</svg><span>Fit</span></div>
<div class="menu-item" data-href="settings"><svg class="menu-icon" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="3" />
<path d="M12 1v6m0 6v6m-9-9h6m6 0h6" />
</svg><span>Settings</span></div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content" id="main">
<!-- Map Layer -->
<div class="map-wrapper">
<div id="map"></div>
</div>
<!-- Absolute Positioning Overlays -->
<div class="page-header">
<h1>HayOS - WiFi Surveillance Tracker</h1>
<p>Coded by Hayden Ø Powered by H9</p>
</div>
<div class="control-panel">
<div class="mode-toggle-container">
<span class="mode-label active" id="label-wifi">WIFI UPLINK</span>
<label class="switch">
<input type="checkbox" id="mode-switch" onchange="toggleMode()">
<span class="slider"></span>
</label>
<span class="mode-label" id="label-bluetooth">BT SCANNERS</span>
</div>
<div class="search-bar">
<div class="search-row">
<select id="search-type">
<option value="location">Location</option>
<option value="bssid">BSSID</option>
<option value="ssid">Wi-Fi SSID</option>
<option value="network">Network/IP</option>
</select>
<input type="text" id="search-input" placeholder="Enter search term">
</div>
<button class="search-btn" onclick="performSearch()">Intercept Targets</button>
</div>
<div class="filter-row">
<div
style="font-size: 10px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 5px; letter-spacing: 1px;">
Hardware Filters:</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<label class="cyber-check"><input type="checkbox" checked class="filter-cb" value="router">
<div class="box"></div> WIFI
</label>
<label class="cyber-check"><input type="checkbox" checked class="filter-cb" value="bluetooth">
<div class="box"></div> BLUETOOTH
</label>
<label class="cyber-check"><input type="checkbox" checked class="filter-cb" value="camera">
<div class="box"></div> CCTV/CAM
</label>
<label class="cyber-check"><input type="checkbox" checked class="filter-cb" value="dashcam">
<div class="box"></div> DASHCAM
</label>
<label class="cyber-check"><input type="checkbox" checked class="filter-cb" value="tv">
<div class="box"></div> SMART_TV
</label>
<label class="cyber-check"><input type="checkbox" checked class="filter-cb" value="car">
<div class="box"></div> VEHICLE
</label>
<label class="cyber-check"><input type="checkbox" checked class="filter-cb" value="headphone">
<div class="box"></div> AUDIO
</label>
</div>
</div>
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(0, 255, 209, 0.1);">
<a href="#" class="search-btn" id="download-btn"
style="display: none; text-align: center; text-decoration: none;">Download Intel Report</a>
</div>
</div>
<div class="results-sidebar" id="results-list">
<div style="text-align: center; color: var(--text-dim); padding: 50px 20px;">
<i class="fas fa-satellite-dish"
style="font-size: 24px; margin-bottom: 15px; display: block; color: var(--primary-color);"></i>
Awaiting Scanner Data...
</div>
</div>
<div class="toast" id="toast">No connections intercepted</div>
</main>
</div>
<!-- Leaflet JS from CDN -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet MarkerCluster JS -->
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script>
let map;
let markers = L.markerClusterGroup();
let results = [];
function initMap() {
map = L.map('map').setView([51.505, -0.09], 13);
const cyberLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
});
const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri, Maxar, Earthstar Geographics'
});
const hybridLayer = L.tileLayer('https://{s}.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', {
maxZoom: 20,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
attribution: '© Google Maps'
});
const baseLayers = {
"CyberMap": cyberLayer,
"SatView": satelliteLayer,
"Hybrid": hybridLayer
};
L.control.layers(baseLayers).addTo(map);
cyberLayer.addTo(map);
document.getElementById('map').classList.add('cyber-map');
map.on('baselayerchange', (e) => {
if (e.name === "SatView") {
document.getElementById('map').classList.remove('cyber-map');
document.body.classList.add('sat-mode');
} else {
document.getElementById('map').classList.add('cyber-map');
document.body.classList.remove('sat-mode');
}
});
map.on('click', async (e) => {
const { lat, lng } = e.latlng;
await fetchNearbyDevices(lat, lng);
});
map.addLayer(markers);
}
// Toggle Mode logic
function toggleMode() {
const isBT = document.getElementById('mode-switch').checked;
document.getElementById('label-wifi').classList.toggle('active', !isBT);
document.getElementById('label-bluetooth').classList.toggle('active', isBT);
markers.clearLayers();
results = [];
showToast(`SENSORS_SWITCHED: ${isBT ? 'BLUETOOTH' : 'WIFI'}`);
}
// Fetch nearby devices from Flask backend
async function fetchNearbyDevices(lat, lon) {
const mode = document.getElementById('mode-switch').checked ? 'bluetooth' : 'wifi';
try {
const response = await fetch(`/nearby?lat=${lat}&lon=${lon}&mode=${mode}`);
const data = await response.json();
console.log('Nearby response:', data);
if (data.error) {
showToast(data.error);
return;
}
updateMap(data);
} catch (err) {
showToast('Error fetching devices');
console.error('Fetch error:', err);
}
}
// Perform search based on input
async function performSearch() {
const searchType = document.getElementById('search-type').value;
const searchInput = document.getElementById('search-input').value;
if (!searchInput) {
showToast('Please enter a search term');
return;
}
try {
const mode = document.getElementById('mode-switch').checked ? 'bluetooth' : 'wifi';
const response = await fetch(`/searchzz?type=${searchType}&query=${encodeURIComponent(searchInput)}&mode=${mode}`);
const data = await response.json();
console.log('Search response:', data);
if (data.error) {
showToast(data.error);
return;
}
updateMap(data);
} catch (err) {
showToast('Error performing search');
console.error('Search error:', err);
}
}
// Update map with device markers
let markerMap = {};
function updateMap(data) {
markers.clearLayers();
markerMap = {};
if (data && data.devices) results = data.devices;
console.log('Processed results for map:', results);
// Get active filters
const activeFilters = Array.from(document.querySelectorAll('.filter-cb:checked')).map(cb => cb.value);
// Apply filtering
const filteredResults = results.filter(device => {
const type = device.type || 'unknown';
if (type === 'router' || type === 'cell_tower' || type === 'iot_device') {
// For WiFi mode, show these if bluetooth isn't the primary focus
// or if they match generic categories.
// Actually, simple logic: if it's in the filters, show it.
return activeFilters.includes(type) || activeFilters.includes('router');
}
return activeFilters.includes(type);
});
if (filteredResults.length === 0) {
showToast('No devices found');
document.getElementById('results-list').innerHTML = `
<div style="text-align: center; color: var(--text-dim); padding: 50px 20px;">
No Matches Found
</div>`;
document.getElementById('download-btn').style.display = 'none';
return;
}
renderSidebar(filteredResults);
filteredResults.forEach((device, index) => {
const { lat, lon, ssid, bssid, cell_id, ip, vendor, signal, accuracy, timestamp, type } = device;
if (lat && lon) {
const icon = getDeviceIcon(type);
const marker = L.marker([lat, lon], { icon })
.bindPopup(`
<div style="font-family: 'Share Tech Mono', monospace; color: var(--primary-color);">
<strong style="font-size: 14px;">${ssid || cell_id || ip || 'Unknown Node'}</strong><br>
<hr style="border: 0.5px solid rgba(0,255,209,0.2); margin: 5px 0;">
<span style="font-size: 10px; color: #fff;">CATEGORY: ${type ? type.toUpperCase() : 'UNKNOWN'}</span><br>
${bssid ? `BSSID: ${bssid}<br>` : ''}
${vendor ? `VENDOR: ${vendor}<br>` : ''}
${signal ? `SIGNAL: ${signal} dBm<br>` : ''}
${accuracy ? `ACCURACY: ${accuracy} m<br>` : ''}
${timestamp ? `LAST_SEEN: ${new Date(timestamp).toLocaleString()}<br>` : ''}
</div>
`);
markers.addLayer(marker);
markerMap[index] = marker;
}
});
map.addLayer(markers);
const validDevices = filteredResults.filter(d => d.lat && d.lon);
if (validDevices.length > 0) {
const group = new L.featureGroup(validDevices.map(d => L.marker([d.lat, d.lon])));
map.fitBounds(group.getBounds().pad(0.2));
document.getElementById('download-btn').style.display = 'block';
setupDownload();
}
}
// Add listeners for real-time filtering
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.filter-cb').forEach(cb => {
cb.addEventListener('change', () => {
updateMap(); // Re-render with existing 'results'
});
});
});
function renderSidebar(nodes) {
const list = document.getElementById('results-list');
list.innerHTML = '';
// Get active filters
const activeFilters = Array.from(document.querySelectorAll('.filter-cb:checked')).map(cb => cb.value);
const filteredNodes = nodes.filter(node => {
const type = node.type || 'unknown';
if (type === 'router' || type === 'cell_tower' || type === 'iot_device') {
return activeFilters.includes(type) || activeFilters.includes('router');
}
return activeFilters.includes(type);
});
filteredNodes.forEach((node) => {
const signal = node.signal ? Math.min(Math.abs(node.signal), 100) : 50;
const percent = 100 - (signal > 100 ? 100 : signal);
const lastSeen = node.timestamp ? formatRelativeTime(node.timestamp) : 'LIVE';
const item = document.createElement('div');
item.className = 'node-item';
item.onclick = () => {
// find marker by coordinates as we might have filtered
const marker = markers.getLayers().find(m => m.getLatLng().lat === node.lat && m.getLatLng().lng === node.lon);
if (marker) {
map.setView(marker.getLatLng(), 17);
marker.openPopup();
}
};
item.innerHTML = `
<div class="node-header">
<span class="node-name">${node.ssid || node.ip || 'Unknown'}</span>
<span class="node-type" style="background: ${getCategoryColor(node.type)}44; color: ${getCategoryColor(node.type)};">${node.type || 'node'}</span>
</div>
<div class="node-meta">
${node.vendor ? `VENDOR: ${node.vendor}` : `ID: ${node.bssid || node.ip || 'ANONYMOUS'}`}<br>
<span style="color: var(--accent-color); font-size: 9px;">LAST_SEEN: ${lastSeen}</span>
</div>
<div class="signal-meter">
<div class="signal-strength" style="width: ${percent}%; background: ${getCategoryColor(node.type)}; box-shadow: 0 0 5px ${getCategoryColor(node.type)}"></div>
</div>
`;
list.appendChild(item);
});
}
function formatRelativeTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return `${diff}s AGO`;
if (diff < 3600) return `${Math.floor(diff / 60)}m AGO`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h AGO`;
return date.toLocaleDateString();
}
function getCategoryColor(type) {
const colors = {
bluetooth: '#4A90E2',
car: '#FFD700',
camera: '#FF4444',
dashcam: '#FF4444',
tv: '#00FF88',
headphone: '#00FF88',
router: 'var(--primary-color)',
cell_tower: '#FF00FF'
};
return colors[type] || 'var(--primary-color)';
}
// Get icon based on device type
function getDeviceIcon(type) {
const icons = {
router: 'fas fa-wifi',
cell_tower: 'fas fa-broadcast-tower',
camera: 'fas fa-camera',
dashcam: 'fas fa-video',
car: 'fas fa-car',
tv: 'fas fa-tv',
headphone: 'fas fa-headphones',
iot_device: 'fas fa-microchip',
iot: 'fas fa-microchip',
bluetooth: 'fab fa-bluetooth',
default: 'fas fa-question-circle'
};
const iconClass = icons[type] || icons.default;
let color = 'var(--primary-color)';
if (type === 'bluetooth') color = '#4A90E2';
if (type === 'car') color = '#FFD700'; // Gold for transport
if (type === 'camera' || type === 'dashcam') color = '#FF4444'; // Red for surveillance
if (type === 'tv' || type === 'headphone') color = '#00FF88'; // Green for entertainment
return L.divIcon({
html: `<i class="${iconClass}" style="color: ${color}; font-size: 20px; text-shadow: 0 0 10px ${color}"></i>`,
className: 'custom-icon',
iconSize: [30, 30],
iconAnchor: [15, 30]
});
}
// Setup download button
function setupDownload() {
const btn = document.getElementById('download-btn');
const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' });
btn.href = URL.createObjectURL(blob);
btn.download = 'wifi-search-results.json';
}
// Show toast notification
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 3000);
}
// Dummy content loader for sidebar (placeholder)
function loadContent(page) {
console.log(`Loading ${page}`);
}
// Initialize map on load
window.onload = initMap;
</script>
<!-- ==== ADD THIS RIGHT BEFORE </body> (still inside <body>) ==== -->
<div class="fab-container">
<button id="chat-fab" class="fab-btn" aria-label="Open AI Chat">
<i class="fas fa-comment-dots"></i>
</button>
</div>
<!-- ==== END FAB ==== -->
<!-- ==== ADD THIS RIGHT AFTER the FAB (still before </body>) ==== -->
<div id="chat-modal" class="chat-modal" aria-hidden="true">
<div class="chat-header">
<span>Hayden's H9-AI Assistant</span>
<button id="close-chat" class="chat-close" aria-label="Close">×</button>
</div>
<div id="chat-messages" class="chat-messages"></div>
<!-- ADD THIS BEFORE <div class="chat-input-area"> -->
<div class="web-search-toggle">
<label>
<input type="checkbox" id="web-search-enabled">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Web Search</span>
</div>
<!-- END ADD -->
<div class="chat-input-area">
<input type="text" id="chat-input" placeholder="Ask me anything…" autocomplete="off">
<button id="chat-send" aria-label="Send">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<!-- ==== END CHAT MODAL ==== -->
<style>
.web-search-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 12px;
color: #00FFFF;
user-select: none;
}
.web-search-toggle input {
display: none;
}
.toggle-slider {
position: relative;
width: 34px;
height: 18px;
background: rgba(0, 255, 255, 0.2);
border: 1px solid #00FFFF;
border-radius: 18px;
cursor: pointer;
transition: 0.3s;
}
.toggle-slider::after {
content: '';
position: absolute;
width: 14px;
height: 14px;
background: #00FFFF;
border-radius: 50%;
top: 1px;
left: 1px;
transition: 0.3s;
}
#web-search-enabled:checked+.toggle-slider {
background: #00FFFF;
}
#web-search-enabled:checked+.toggle-slider::after {
background: #0A1F1F;
transform: translateX(16px);
}
.toggle-label {
font-weight: 600;
}
/* ==== CHAT POPUP STYLES ==== */
.chat-modal {
position: fixed;
bottom: 90px;
right: 24px;
width: 380px;
height: 520px;
max-width: 90vw;
background: rgba(2, 10, 10, 0.95);
backdrop-filter: blur(15px);
border: 1px solid var(--border-color);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
transform: scale(0);
transform-origin: bottom right;
transition: 0.3s;
z-index: 1300;
}
.chat-modal.open {
transform: scale(1);
}
.chat-header {
background: rgba(0, 255, 209, 0.1);
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
color: var(--primary-color);
text-transform: uppercase;
font-size: 12px;
letter-spacing: 2px;
}
.chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 85%;
padding: 10px 15px;
border-radius: 4px;
font-size: 13px;
line-height: 1.5;
}
.message.user {
align-self: flex-end;
background: rgba(0, 255, 209, 0.15);
border: 1px solid var(--primary-color);
color: var(--primary-color);
}
.message.bot {
align-self: flex-start;
background: rgba(0, 163, 255, 0.1);
border: 1px solid var(--secondary-color);
color: var(--secondary-color);
}
.chat-input-area {
display: flex;
border-top: 1px solid var(--border-color);
background: rgba(0, 0, 0, 0.3);
}
#chat-input {
flex: 1;
padding: 15px;
background: transparent;
border: none;
color: #fff;
font-family: var(--font-main);
}
#chat-input:focus {
outline: none;
}
#chat-send {
background: transparent;
border: none;
color: var(--primary-color);
width: 50px;
cursor: pointer;
transition: 0.3s;
}
#chat-send:hover {
transform: scale(1.2);
text-shadow: var(--neon-glow);
}
.fab-btn {
width: 60px;
height: 60px;
border-radius: 50%;
background: var(--bg-dark);
border: 1px solid var(--primary-color);
color: var(--primary-color);
font-size: 24px;
cursor: pointer;
box-shadow: var(--neon-glow);
transition: 0.3s;
}
.fab-btn:hover {
transform: scale(1.1) rotate(15deg);
box-shadow: 0 0 25px var(--primary-color);
}
</style>
<script>
/* ==== CHAT POPUP LOGIC ==== */
const fabBtn = document.getElementById('chat-fab');
const modal = document.getElementById('chat-modal');
const closeBtn = document.getElementById('close-chat');
const input = document.getElementById('chat-input');
const sendBtn = document.getElementById('chat-send');
const messages = document.getElementById('chat-messages');
function openChat() { modal.classList.add('open'); input.focus(); }
function closeChat() { modal.classList.remove('open'); input.value = ''; }
fabBtn.addEventListener('click', openChat);
closeBtn.addEventListener('click', closeChat);
modal.addEventListener('click', e => { if (e.target === modal) closeChat(); });
function addMessage(text, type) {
const div = document.createElement('div');
div.className = `message ${type}`;
if (type === 'bot' && text === 'typing') {
div.innerHTML = `<span class="typing"></span><span class="typing"></span><span class="typing"></span>`;
} else {
div.textContent = text;
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
/* ==== SEND MESSAGE → CALL YOUR FLASK ENDPOINT ==== */
async function sendMessage() {
const userText = input.value.trim();
if (!userText) return;
addMessage(userText, 'user');
input.value = '';
// Show typing
addMessage('typing', 'bot');
// Get web search state
const webSearchEnabled = document.getElementById('web-search-enabled').checked;
try {
const res = await fetch('/chatgpt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userText,
web_search: webSearchEnabled // NEW: send flag
})
});
const data = await res.json();
messages.lastElementChild.remove(); // remove typing
addMessage(data.reply, 'bot');
} catch (err) {
messages.lastElementChild.remove();
addMessage('Error try again', 'bot');
console.error(err);
}
}
sendBtn.addEventListener('click', sendMessage);
input.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
/* optional: close with ESC */
document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.classList.contains('open')) closeChat(); });
</script>
<!-- ==== SYNTAX HIGHLIGHTING ==== -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach(hljs.highlightElement);
});
</script>
<!-- ==== END HIGHLIGHT ==== -->
<script>
function addMessage(text, type) {
const div = document.createElement('div');
div.className = `message ${type}`;
div.innerHTML = text; // Now supports HTML <pre><code>
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
// Re-highlight new code blocks
div.querySelectorAll('pre code').forEach(hljs.highlightElement);
}
</script>
</body>
</html>