chore: remove legacy simulator pages and mobile app scaffold

This commit is contained in:
2026-04-16 17:10:00 +01:00
parent 3c1099efdf
commit 69ec7108a5
46 changed files with 0 additions and 23269 deletions

View File

@@ -175,26 +175,6 @@ OpenAPI docs are generated from Zod/OpenAPI definitions:
| `GET /openapi.json` | OpenAPI 3 spec (JSON) |
| `GET /docs` | Swagger UI |
### Web Mobile Simulator
Use `GET /sim/mobile-sim.html` to run the full single-page browser simulator that behaves like the mobile app.
Split-page entrypoints are also available:
- `GET /sim/mobile-sim-auth.html`
- `GET /sim/mobile-sim-onboarding.html`
- `GET /sim/mobile-sim-camera.html`
- `GET /sim/mobile-sim-client.html`
- `GET /sim/mobile-sim-activity.html`
- `GET /sim/mobile-sim-settings.html`
Architecture reference page:
- `GET /sim/backend-architecture.html`
All simulator pages support the same flow:
- Register as `camera` or `client`
- Connect Socket.IO with bearer device token
- Camera: process incoming stream requests, negotiate WebRTC, start/end motion events
- Client: create links, request streams, and negotiate WebRTC viewing
### Admin Dashboard
Access `/admin` with Basic auth to:

View File

@@ -95,7 +95,6 @@ app.all('/api/auth/*splat', corsMiddleware, toNodeHandler(auth));
app.use(rateLimit({ keyPrefix: 'global', windowMs: 60_000, max: 400 }));
app.use(requestContext);
app.use(express.json());
app.use('/sim', express.static('public'));
app.use('/videos', videosRoutes);
app.use('/admin', adminRoutes);
app.use('/devices', rateLimit({ keyPrefix: 'devices', windowMs: 60_000, max: 120 }), devicesRoutes);

View File

@@ -1,934 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Backend Architecture Deep Dive</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #071015;
--panel: #0e1b22;
--panel-2: #132731;
--text: #dbe7ef;
--muted: #94adbb;
--line: #255061;
--accent: #22c55e;
--accent-2: #06b6d4;
--warn: #f59e0b;
--danger: #f97316;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at 0% 0%, #113141 0%, transparent 45%),
radial-gradient(circle at 100% 0%, #17303a 0%, transparent 45%),
var(--bg);
line-height: 1.5;
}
.wrap {
max-width: 1320px;
margin: 0 auto;
padding: 24px;
}
.hero {
border: 1px solid var(--line);
background: linear-gradient(165deg, #122734 0%, #0b171e 60%);
border-radius: 18px;
padding: 24px;
margin-bottom: 18px;
}
h1, h2, h3 {
margin: 0 0 10px 0;
line-height: 1.25;
}
h1 {
font-size: clamp(1.5rem, 2vw, 2.2rem);
}
h2 {
font-size: clamp(1.2rem, 1.6vw, 1.6rem);
margin-top: 26px;
margin-bottom: 12px;
}
h3 {
font-size: 1rem;
color: #d2effb;
margin-top: 12px;
}
p, li {
color: var(--muted);
}
strong, b {
color: #eaf7ff;
}
code, .mono {
font-family: "IBM Plex Mono", monospace;
font-size: 0.9em;
background: #0a151b;
border: 1px solid #1b3744;
padding: 2px 6px;
border-radius: 6px;
color: #b7f5ff;
}
.small {
font-size: 0.88rem;
color: #8aa1af;
}
.grid {
display: grid;
gap: 12px;
}
.g2 {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.g3 {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.card {
background: linear-gradient(180deg, var(--panel-2), var(--panel));
border: 1px solid var(--line);
border-radius: 14px;
padding: 14px;
}
.chip {
display: inline-block;
border: 1px solid #2e6074;
color: #9ed4ea;
border-radius: 999px;
padding: 2px 10px;
font-size: 0.8rem;
margin: 0 4px 6px 0;
background: #10232d;
}
.chip.warn {
border-color: #7a5b1c;
color: #ffd58a;
background: #20190c;
}
.chip.ok {
border-color: #1f6a42;
color: #8df0bc;
background: #0b1d15;
}
.chip.alt {
border-color: #23617b;
color: #8cd7f8;
background: #0d1f29;
}
.svg-box {
border: 1px solid var(--line);
border-radius: 14px;
background: #0a161d;
padding: 10px;
overflow: auto;
}
svg {
width: 100%;
height: auto;
min-width: 840px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
background: #0b171e;
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
}
th, td {
padding: 9px 10px;
border-bottom: 1px solid #1f3c4b;
vertical-align: top;
text-align: left;
}
th {
background: #132833;
color: #d9f4ff;
font-weight: 600;
}
tr:last-child td {
border-bottom: none;
}
details {
border: 1px solid var(--line);
border-radius: 12px;
padding: 10px 12px;
background: #0b171e;
margin-bottom: 10px;
}
summary {
cursor: pointer;
color: #cde9f8;
font-weight: 600;
}
.seq {
white-space: pre;
overflow: auto;
border: 1px solid var(--line);
border-radius: 12px;
padding: 14px;
background: #081117;
color: #b4d4e2;
font-family: "IBM Plex Mono", monospace;
font-size: 0.82rem;
line-height: 1.45;
}
.toc {
position: sticky;
top: 10px;
border: 1px solid var(--line);
border-radius: 12px;
background: #0a161d;
padding: 10px;
}
.toc a {
color: #9dc9dd;
text-decoration: none;
display: block;
padding: 3px 2px;
font-size: 0.9rem;
}
.toc a:hover {
color: #d4f3ff;
}
.layout {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 16px;
align-items: start;
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
}
.toc {
position: static;
}
svg {
min-width: 760px;
}
}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<div class="small">SecureCam Backend Architecture</div>
<h1>Backend Architecture Deep Dive (Single-Page Reference)</h1>
<p>
This document explains how the backend is built, how requests and realtime events move through the system,
how data is stored, and where each concern lives in code.
It is based on the current implementation in this repository (<span class="mono">index.ts</span>,
<span class="mono">routes/*</span>, <span class="mono">realtime/gateway.ts</span>,
<span class="mono">services/*</span>, <span class="mono">workers/*</span>, <span class="mono">db/schema.ts</span>).
</p>
<div>
<span class="chip ok">Express 5 API</span>
<span class="chip alt">Socket.IO Realtime</span>
<span class="chip">Better Auth + Drizzle</span>
<span class="chip">PostgreSQL</span>
<span class="chip">MinIO / S3-compatible</span>
<span class="chip warn">Mock Media Provider</span>
</div>
</section>
<div class="layout">
<aside class="toc">
<strong>Contents</strong>
<a href="#system">1. System Context</a>
<a href="#startup">2. Startup Sequence</a>
<a href="#pipeline">3. HTTP Pipeline</a>
<a href="#auth">4. Auth + Identity</a>
<a href="#realtime">5. Realtime Gateway</a>
<a href="#stream">6. Stream Lifecycle</a>
<a href="#data">7. Data Model</a>
<a href="#routes">8. Route Surface</a>
<a href="#workers">9. Workers + Reliability</a>
<a href="#security">10. Security Controls</a>
<a href="#config">11. Configuration</a>
<a href="#code-map">12. Code Ownership Map</a>
<a href="#constraints">13. Current Constraints</a>
</aside>
<main>
<section id="system">
<h2>1) System Context</h2>
<div class="svg-box">
<svg viewBox="0 0 1220 560" role="img" aria-label="System context architecture diagram">
<defs>
<marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#6ec9e8" />
</marker>
</defs>
<rect x="20" y="20" width="280" height="170" rx="12" fill="#132833" stroke="#2d5d71"/>
<text x="36" y="52" fill="#d7f2ff" font-size="18" font-weight="600">Clients</text>
<text x="36" y="84" fill="#95bacb" font-size="15">- Browser simulator (camera/client)</text>
<text x="36" y="109" fill="#95bacb" font-size="15">- Future mobile apps</text>
<text x="36" y="134" fill="#95bacb" font-size="15">- Admin browser</text>
<text x="36" y="159" fill="#95bacb" font-size="15">- Swagger/OpenAPI consumers</text>
<rect x="20" y="230" width="280" height="140" rx="12" fill="#1f2a20" stroke="#3d7249"/>
<text x="36" y="262" fill="#dcffe5" font-size="18" font-weight="600">Identity</text>
<text x="36" y="292" fill="#a8d8b6" font-size="15">Better Auth session cookies</text>
<text x="36" y="317" fill="#a8d8b6" font-size="15">Custom HMAC device bearer tokens</text>
<text x="36" y="342" fill="#a8d8b6" font-size="15">Role-aware device auth</text>
<rect x="360" y="20" width="530" height="390" rx="14" fill="#0f1e27" stroke="#2f6176"/>
<text x="380" y="52" fill="#def6ff" font-size="20" font-weight="700">Backend Process (Bun + Express + Socket.IO)</text>
<rect x="380" y="70" width="235" height="120" rx="10" fill="#13303d" stroke="#2e647a"/>
<text x="396" y="98" fill="#d7f2ff" font-size="16" font-weight="600">HTTP Layer</text>
<text x="396" y="122" fill="#9fc8da" font-size="14">Helmet + CORS + rate limits</text>
<text x="396" y="143" fill="#9fc8da" font-size="14">requestContext metrics + logs</text>
<text x="396" y="164" fill="#9fc8da" font-size="14">REST routes + OpenAPI docs</text>
<rect x="640" y="70" width="230" height="120" rx="10" fill="#13303d" stroke="#2e647a"/>
<text x="658" y="98" fill="#d7f2ff" font-size="16" font-weight="600">Realtime Gateway</text>
<text x="658" y="122" fill="#9fc8da" font-size="14">Socket.IO device rooms</text>
<text x="658" y="143" fill="#9fc8da" font-size="14">command / stream / webrtc signals</text>
<text x="658" y="164" fill="#9fc8da" font-size="14">presence + retry loop</text>
<rect x="380" y="210" width="235" height="180" rx="10" fill="#1d2e23" stroke="#447551"/>
<text x="396" y="238" fill="#e3ffe8" font-size="16" font-weight="600">Service / Worker Layer</text>
<text x="396" y="262" fill="#b8dfc3" font-size="14">push queue worker</text>
<text x="396" y="282" fill="#b8dfc3" font-size="14">recording timeout reconciler</text>
<text x="396" y="302" fill="#b8dfc3" font-size="14">audit logging</text>
<text x="396" y="322" fill="#b8dfc3" font-size="14">health + metrics endpoints</text>
<text x="396" y="342" fill="#b8dfc3" font-size="14">media provider adapter</text>
<rect x="640" y="210" width="230" height="180" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="658" y="238" fill="#fff1da" font-size="16" font-weight="600">Media Control Plane</text>
<text x="658" y="262" fill="#f2d7aa" font-size="14">stream session orchestration</text>
<text x="658" y="282" fill="#f2d7aa" font-size="14">mock credential issuance</text>
<text x="658" y="302" fill="#f2d7aa" font-size="14">optional SFU scaffold (noop)</text>
<text x="658" y="322" fill="#f2d7aa" font-size="14">recording row lifecycle</text>
<rect x="940" y="40" width="260" height="160" rx="12" fill="#192b35" stroke="#39677d"/>
<text x="958" y="70" fill="#daf4ff" font-size="18" font-weight="600">PostgreSQL</text>
<text x="958" y="98" fill="#9fc5d8" font-size="14">users, devices, links, commands</text>
<text x="958" y="118" fill="#9fc5d8" font-size="14">streams, recordings, events</text>
<text x="958" y="138" fill="#9fc5d8" font-size="14">videos, notifications, audit</text>
<text x="958" y="158" fill="#9fc5d8" font-size="14">Better Auth tables</text>
<rect x="940" y="240" width="260" height="160" rx="12" fill="#2b2318" stroke="#7c6039"/>
<text x="958" y="270" fill="#fff0d4" font-size="18" font-weight="600">MinIO / Object Storage</text>
<text x="958" y="298" fill="#ebd3aa" font-size="14">presigned PUT / GET URLs</text>
<text x="958" y="318" fill="#ebd3aa" font-size="14">video objects + recordings</text>
<text x="958" y="338" fill="#ebd3aa" font-size="14">bucket bootstrap on startup</text>
<line x1="300" y1="108" x2="360" y2="108" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
<line x1="300" y1="298" x2="360" y2="138" stroke="#7ee2aa" stroke-width="2" marker-end="url(#arr)"/>
<line x1="890" y1="120" x2="940" y2="120" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
<line x1="890" y1="290" x2="940" y2="300" stroke="#f1bf73" stroke-width="2" marker-end="url(#arr)"/>
<line x1="700" y1="190" x2="700" y2="210" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
<line x1="500" y1="190" x2="500" y2="210" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
</svg>
</div>
<div class="grid g3" style="margin-top:10px;">
<div class="card"><h3>Core Role</h3><p>Acts primarily as a <strong>control plane</strong> for auth, command routing, stream state, credential issuance, and recording metadata. It is not yet a full production media plane.</p></div>
<div class="card"><h3>Transport Split</h3><p><strong>HTTP</strong> handles CRUD/state endpoints; <strong>Socket.IO</strong> handles realtime command delivery, acknowledgements, and WebRTC signaling relay.</p></div>
<div class="card"><h3>Persistence Split</h3><p><strong>Postgres</strong> stores state + metadata. <strong>MinIO</strong> stores binary objects. Routes often coordinate both.</p></div>
</div>
</section>
<section id="startup">
<h2>2) Startup Sequence</h2>
<div class="seq">Process start
-> load module graph (auth, db, minio, routes)
-> create Express app + OpenAPI doc
-> mount middleware and routes
-> create HTTP server
-> start() called
-> ensureMinioBucket()
- checks bucket existence
- creates bucket if missing
- exits process on failure
-> setupRealtimeGateway(server)
- Socket.IO auth middleware
- event handlers
- command retry interval init (if required tables exist)
-> startRecordingsWorker()
- interval scans stale awaiting_upload rows -> failed
-> startPushWorker()
- interval dispatches queued push rows
-> server.listen(PORT)
</div>
<p class="small">If MinIO initialization fails, process exits with code 1 by design (<span class="mono">index.ts</span>).</p>
</section>
<section id="pipeline">
<h2>3) HTTP Request Pipeline (Express)</h2>
<div class="svg-box">
<svg viewBox="0 0 1220 360" role="img" aria-label="HTTP middleware pipeline diagram">
<defs>
<marker id="arr2" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#90ddff" />
</marker>
</defs>
<rect x="20" y="128" width="130" height="80" rx="10" fill="#122733" stroke="#2b5e74"/>
<text x="42" y="173" fill="#d6f2ff" font-size="16">Client</text>
<rect x="190" y="70" width="160" height="190" rx="10" fill="#132a35" stroke="#2f6074"/>
<text x="205" y="100" fill="#ddf6ff" font-size="15" font-weight="600">helmet()</text>
<text x="205" y="124" fill="#9ec5d6" font-size="13">CSP, headers</text>
<text x="205" y="154" fill="#ddf6ff" font-size="15" font-weight="600">cors()</text>
<text x="205" y="178" fill="#9ec5d6" font-size="13">trusted origins</text>
<text x="205" y="208" fill="#ddf6ff" font-size="15" font-weight="600">global rate limit</text>
<text x="205" y="232" fill="#9ec5d6" font-size="13">memory buckets</text>
<rect x="390" y="70" width="200" height="190" rx="10" fill="#132a35" stroke="#2f6074"/>
<text x="408" y="100" fill="#ddf6ff" font-size="15" font-weight="600">requestContext</text>
<text x="408" y="124" fill="#9ec5d6" font-size="13">x-request-id</text>
<text x="408" y="144" fill="#9ec5d6" font-size="13">counter increment</text>
<text x="408" y="164" fill="#9ec5d6" font-size="13">JSON finish log</text>
<text x="408" y="198" fill="#ddf6ff" font-size="15" font-weight="600">express.json()</text>
<text x="408" y="222" fill="#9ec5d6" font-size="13">body parsing</text>
<rect x="630" y="52" width="270" height="226" rx="10" fill="#1d2e23" stroke="#447551"/>
<text x="648" y="82" fill="#e6ffea" font-size="15" font-weight="600">Route layer</text>
<text x="648" y="106" fill="#badfc5" font-size="13">/api/auth/* (Better Auth)</text>
<text x="648" y="126" fill="#badfc5" font-size="13">/videos /devices /commands ...</text>
<text x="648" y="146" fill="#badfc5" font-size="13">route-level auth + zod validation</text>
<text x="648" y="166" fill="#badfc5" font-size="13">DB + MinIO + realtime side effects</text>
<text x="648" y="200" fill="#e6ffea" font-size="15" font-weight="600">Static + docs</text>
<text x="648" y="224" fill="#badfc5" font-size="13">/sim, /docs, /openapi.json</text>
<rect x="940" y="90" width="260" height="150" rx="10" fill="#2a2118" stroke="#7d613a"/>
<text x="958" y="120" fill="#fff2d9" font-size="15" font-weight="600">Response + Error Handler</text>
<text x="958" y="146" fill="#edd6ad" font-size="13">successful JSON / HTML / static file</text>
<text x="958" y="168" fill="#edd6ad" font-size="13">or fallback 500 JSON</text>
<text x="958" y="190" fill="#edd6ad" font-size="13">{ message: "Internal server error" }</text>
<line x1="150" y1="168" x2="190" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="350" y1="168" x2="390" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="590" y1="168" x2="630" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="900" y1="168" x2="940" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
</svg>
</div>
</section>
<section id="auth">
<h2>4) Authentication and Identity Model</h2>
<div class="grid g2">
<div class="card">
<h3>A) Session auth (<span class="mono">requireAuth</span>)</h3>
<ul>
<li>Used by user-facing REST routes like <span class="mono">/videos</span>, <span class="mono">/devices/register</span>, <span class="mono">/device-links</span>.</li>
<li>Reads Better Auth session from request headers/cookies via <span class="mono">auth.api.getSession()</span>.</li>
<li>Attaches session object to <span class="mono">req.auth</span>.</li>
<li>Backed by Better Auth tables: <span class="mono">users</span>, <span class="mono">account</span>, <span class="mono">session</span>, <span class="mono">verification</span>.</li>
</ul>
</div>
<div class="card">
<h3>B) Device auth (<span class="mono">requireDeviceAuth</span>)</h3>
<ul>
<li>Used by device-to-backend routes and Socket.IO auth.</li>
<li>Bearer token format: <span class="mono">base64url(payload).hmac</span>.</li>
<li>Payload fields: <span class="mono">userId</span>, <span class="mono">deviceId</span>, <span class="mono">role</span>, <span class="mono">exp</span>.</li>
<li>Signed with HMAC-SHA256 using <span class="mono">BETTER_AUTH_SECRET</span>.</li>
<li>Token role is verified against device role in realtime handshake.</li>
</ul>
</div>
</div>
<p class="small">This dual model separates user session identity from per-device identity and permissions.</p>
</section>
<section id="realtime">
<h2>5) Realtime Gateway (Socket.IO)</h2>
<div class="grid g2">
<div class="card">
<h3>Connection model</h3>
<ul>
<li>Devices authenticate with token in <span class="mono">handshake.auth.token</span> or <span class="mono">Authorization</span> header.</li>
<li>Each device joins room <span class="mono">device:{deviceId}</span>.</li>
<li>Presence updates <span class="mono">devices.status</span> + <span class="mono">lastSeenAt</span>.</li>
<li>Disconnect applies a 500ms delay to reduce status flapping on fast reconnect.</li>
</ul>
</div>
<div class="card">
<h3>Gateway responsibilities</h3>
<ul>
<li><span class="mono">command:received</span> delivery to target room.</li>
<li><span class="mono">command:ack</span> validation + DB update + source notification.</li>
<li><span class="mono">webrtc:signal</span> relay with stream-participant validation.</li>
<li><span class="mono">stream:requested</span>, <span class="mono">stream:started</span>, and <span class="mono">stream:ended</span> lifecycle fan-out.</li>
<li>Legacy command retries remain only for non-stream commands while <span class="mono">SIMPLE_STREAMING</span> is enabled.</li>
</ul>
</div>
</div>
<h3>Command dispatch and ack sequence</h3>
<div class="seq">Client Device API /commands DB Socket.IO Gateway Camera Device
| | | | |
1) POST /commands -------->| validate/link ---->| insert queued command | |
| | dispatchCommandById() | |
| |-----------------------------------------------> emit command:received --->|
| | | status sent/queued | |
|<--------------------| command payload | | |
2) camera emits command:ack --------------------------------------------------------------->|
| | | | validate + update DB |
| | | command status + ack time | emit command:status -->| (to source room)
</div>
</section>
<section id="stream">
<h2>6) Stream Lifecycle and Media Control</h2>
<div class="card">
<h3>State machine (stream session)</h3>
<div class="seq">requested --> streaming --> completed|cancelled|failed
| | |
| | +-- create recording placeholder row on end
| +-- media session created (provider + endpoints)
+-- command start_stream queued/dispatched to camera
</div>
</div>
<h3>On-demand stream end-to-end sequence</h3>
<div class="seq">Client Device /streams/request Camera Device /streams/:id/accept Media Provider
| | | | |
1) request stream -------------->| validate roles/link -> | | |
| | create stream_session | | |
| | create start_stream cmd | | |
| | dispatch via socket ----+-------------------------> | command:received |
|<--------------------------| stream:requested event | | |
2) accept command (camera side)
| | | POST /accept -----------> | createSession() ------>|
| | | | status=streaming |
|<--------------------------| stream:started event ---+ | |
3) credential issuance
camera -> GET /publish-credentials -> mediaProvider.issuePublishCredentials()
viewer -> GET /subscribe-credentials -> mediaProvider.issueSubscribeCredentials()
4) stream end
camera/requester -> POST /end -> mark ended + optional sfuService.endSession() + createRecordingForStream()
-> emit stream:ended to camera and requester (push fallback if offline)
</div>
<div class="grid g2">
<div class="card">
<h3>Media provider abstraction</h3>
<ul>
<li>Current provider: <span class="mono">mock</span> (<span class="mono">media/providers/mock.ts</span>).</li>
<li>Creates deterministic mock media session IDs.</li>
<li>Issues signed publish/subscribe tokens with TTL.</li>
<li>Uses <span class="mono">BETTER_AUTH_SECRET</span> for HMAC signing.</li>
</ul>
</div>
<div class="card">
<h3>SFU mode status</h3>
<ul>
<li><span class="mono">MEDIA_MODE=single_server_sfu</span> enables SFU endpoints.</li>
<li>Current implementation is a <strong>noop scaffold</strong> with in-memory session registry + synthetic transport IDs.</li>
<li>No full server-side RTP forwarding pipeline implemented yet.</li>
</ul>
</div>
</div>
</section>
<section id="data">
<h2>7) Data Model (Core Tables and Relationships)</h2>
<div class="svg-box">
<svg viewBox="0 0 1280 880" role="img" aria-label="ER-style architecture data diagram">
<defs>
<marker id="arr3" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#8bcbe3" />
</marker>
</defs>
<rect x="30" y="30" width="260" height="170" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="48" y="58" fill="#ddf6ff" font-size="17" font-weight="700">users</text>
<text x="48" y="82" fill="#a5c9da" font-size="13">id (PK), email, name</text>
<text x="48" y="102" fill="#a5c9da" font-size="13">passwordHash, emailVerified</text>
<text x="48" y="122" fill="#a5c9da" font-size="13">createdAt, updatedAt</text>
<rect x="340" y="30" width="290" height="210" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="358" y="58" fill="#ddf6ff" font-size="17" font-weight="700">devices</text>
<text x="358" y="82" fill="#a5c9da" font-size="13">id (PK), userId (FK -> users)</text>
<text x="358" y="102" fill="#a5c9da" font-size="13">role, status, isCamera</text>
<text x="358" y="122" fill="#a5c9da" font-size="13">platform, appVersion, pushToken</text>
<text x="358" y="142" fill="#a5c9da" font-size="13">lastSeenAt, timestamps</text>
<rect x="680" y="30" width="300" height="190" rx="10" fill="#153329" stroke="#3f7351"/>
<text x="698" y="58" fill="#e4ffea" font-size="17" font-weight="700">device_links</text>
<text x="698" y="82" fill="#b8dfc4" font-size="13">ownerUserId -> users</text>
<text x="698" y="102" fill="#b8dfc4" font-size="13">cameraDeviceId -> devices</text>
<text x="698" y="122" fill="#b8dfc4" font-size="13">clientDeviceId -> devices</text>
<text x="698" y="142" fill="#b8dfc4" font-size="13">unique(cameraDeviceId, clientDeviceId)</text>
<rect x="1020" y="30" width="230" height="210" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="1038" y="58" fill="#fff1da" font-size="17" font-weight="700">device_commands</text>
<text x="1038" y="82" fill="#f0d7ac" font-size="13">sourceDeviceId -> devices</text>
<text x="1038" y="102" fill="#f0d7ac" font-size="13">targetDeviceId -> devices</text>
<text x="1038" y="122" fill="#f0d7ac" font-size="13">commandType, payload</text>
<text x="1038" y="142" fill="#f0d7ac" font-size="13">status, retryCount, ackAt</text>
<rect x="340" y="300" width="300" height="210" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="358" y="328" fill="#ddf6ff" font-size="17" font-weight="700">stream_sessions</text>
<text x="358" y="352" fill="#a5c9da" font-size="13">ownerUserId -> users</text>
<text x="358" y="372" fill="#a5c9da" font-size="13">cameraDeviceId -> devices</text>
<text x="358" y="392" fill="#a5c9da" font-size="13">requesterDeviceId -> devices</text>
<text x="358" y="412" fill="#a5c9da" font-size="13">status, reason, mediaProvider</text>
<text x="358" y="432" fill="#a5c9da" font-size="13">mediaSessionId, streamKey</text>
<rect x="680" y="300" width="300" height="210" rx="10" fill="#153329" stroke="#3f7351"/>
<text x="698" y="328" fill="#e4ffea" font-size="17" font-weight="700">recordings</text>
<text x="698" y="352" fill="#b8dfc4" font-size="13">streamSessionId -> stream_sessions</text>
<text x="698" y="372" fill="#b8dfc4" font-size="13">cameraDeviceId, requesterDeviceId</text>
<text x="698" y="392" fill="#b8dfc4" font-size="13">status awaiting_upload/ready/failed</text>
<text x="698" y="412" fill="#b8dfc4" font-size="13">objectKey, bucket, duration, size</text>
<rect x="1020" y="300" width="230" height="190" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="1038" y="328" fill="#fff1da" font-size="17" font-weight="700">events</text>
<text x="1038" y="352" fill="#f0d7ac" font-size="13">userId -> users</text>
<text x="1038" y="372" fill="#f0d7ac" font-size="13">deviceId -> devices</text>
<text x="1038" y="392" fill="#f0d7ac" font-size="13">startedAt, endedAt, status</text>
<text x="1038" y="412" fill="#f0d7ac" font-size="13">triggeredBy, videoUrl</text>
<rect x="30" y="560" width="300" height="220" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="48" y="588" fill="#ddf6ff" font-size="17" font-weight="700">videos (legacy upload metadata)</text>
<text x="48" y="612" fill="#a5c9da" font-size="13">userId, deviceId, eventId</text>
<text x="48" y="632" fill="#a5c9da" font-size="13">objectKey, bucket, uploadUrl</text>
<text x="48" y="652" fill="#a5c9da" font-size="13">downloadUrl, status, expiresAt</text>
<rect x="370" y="560" width="300" height="220" rx="10" fill="#153329" stroke="#3f7351"/>
<text x="388" y="588" fill="#e4ffea" font-size="17" font-weight="700">push_notifications</text>
<text x="388" y="612" fill="#b8dfc4" font-size="13">ownerUserId -> users</text>
<text x="388" y="632" fill="#b8dfc4" font-size="13">recipientDeviceId -> devices</text>
<text x="388" y="652" fill="#b8dfc4" font-size="13">type, payload, status</text>
<text x="388" y="672" fill="#b8dfc4" font-size="13">attempts, nextAttemptAt, sentAt</text>
<rect x="710" y="560" width="270" height="220" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="728" y="588" fill="#fff1da" font-size="17" font-weight="700">audit_logs</text>
<text x="728" y="612" fill="#f0d7ac" font-size="13">ownerUserId -> users</text>
<text x="728" y="632" fill="#f0d7ac" font-size="13">actorDeviceId -> devices</text>
<text x="728" y="652" fill="#f0d7ac" font-size="13">action, targetType, targetId</text>
<text x="728" y="672" fill="#f0d7ac" font-size="13">metadata, ipAddress, createdAt</text>
<rect x="1020" y="560" width="230" height="220" rx="10" fill="#1f2d36" stroke="#4a6f80"/>
<text x="1038" y="588" fill="#daf4ff" font-size="17" font-weight="700">Better Auth tables</text>
<text x="1038" y="612" fill="#a9cfdf" font-size="13">account</text>
<text x="1038" y="632" fill="#a9cfdf" font-size="13">session</text>
<text x="1038" y="652" fill="#a9cfdf" font-size="13">verification</text>
<line x1="290" y1="96" x2="340" y2="96" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="290" y1="130" x2="680" y2="130" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="630" y1="140" x2="1020" y2="140" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="490" y1="240" x2="490" y2="300" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="640" y1="404" x2="680" y2="404" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="1135" y1="240" x2="1135" y2="300" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="190" y1="200" x2="190" y2="560" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="490" y1="510" x2="520" y2="560" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="490" y1="510" x2="845" y2="560" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
</svg>
</div>
<p class="small">Note: <span class="mono">notifications</span> table exists for event notification tracking; push delivery queue is modeled separately by <span class="mono">push_notifications</span>.</p>
</section>
<section id="routes">
<h2>8) Route Surface and Responsibilities</h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>Auth</th>
<th>Main tables/resources</th>
<th>Side effects</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="mono">/api/auth/*</span></td>
<td>Better Auth</td>
<td>users, account, session, verification</td>
<td>session cookie lifecycle</td>
</tr>
<tr>
<td><span class="mono">/devices</span></td>
<td>session + device token for heartbeat</td>
<td>devices, device_links</td>
<td>auto-link opposite-role devices on register; stale-status projection on list</td>
</tr>
<tr>
<td><span class="mono">/device-links</span></td>
<td>session</td>
<td>device_links, devices</td>
<td>enforces camera/client role pairing</td>
</tr>
<tr>
<td><span class="mono">/commands</span></td>
<td>session + device token ack fallback</td>
<td>device_commands, devices, device_links</td>
<td>dispatch to Socket.IO, ack/reject propagation</td>
</tr>
<tr>
<td><span class="mono">/events</span></td>
<td>device token (start/end), session (list)</td>
<td>events, device_links, notifications</td>
<td>realtime motion fanout, push fallback, audit log</td>
</tr>
<tr>
<td><span class="mono">/streams</span></td>
<td>device token</td>
<td>stream_sessions, device_commands, recordings</td>
<td>stream command dispatch, media credentials, stream realtime events, push fallback, optional SFU calls</td>
</tr>
<tr>
<td><span class="mono">/recordings</span></td>
<td>device token</td>
<td>recordings</td>
<td>storage object validation, presigned download URL, audit log</td>
</tr>
<tr>
<td><span class="mono">/videos</span></td>
<td>session</td>
<td>videos, devices, MinIO</td>
<td>presigned PUT/GET generation, object listing/deletion</td>
</tr>
<tr>
<td><span class="mono">/push-notifications</span></td>
<td>device token</td>
<td>push_notifications</td>
<td>manual worker dispatch trigger</td>
</tr>
<tr>
<td><span class="mono">/audit</span></td>
<td>device token</td>
<td>audit_logs</td>
<td>none (read only)</td>
</tr>
<tr>
<td><span class="mono">/ops</span></td>
<td>none</td>
<td>DB, MinIO, in-memory metrics, SFU service</td>
<td>readiness checks, metric export</td>
</tr>
<tr>
<td><span class="mono">/admin</span></td>
<td>HTTP Basic auth</td>
<td>MinIO</td>
<td>embedded admin UI + object operations</td>
</tr>
</tbody>
</table>
<h3>Detailed endpoint groups</h3>
<details>
<summary>Devices and Links</summary>
<ul>
<li><span class="mono">POST /devices/register</span>: creates device, sets initial online status, auto-creates links with existing opposite-role devices, returns device token.</li>
<li><span class="mono">GET /devices</span>: lists user devices with computed effective presence status using <span class="mono">DEVICE_ONLINE_STALE_SECONDS</span>.</li>
<li><span class="mono">PATCH /devices/:id</span>: updates mutable metadata and role.</li>
<li><span class="mono">POST /devices/:id/heartbeat</span>: token-authenticated presence update for exact device token/device match.</li>
<li><span class="mono">/device-links</span>: ensures one active camera-client pair and ownership checks.</li>
</ul>
</details>
<details>
<summary>Commands, Events, Streams</summary>
<ul>
<li><span class="mono">POST /commands</span>: only client -> camera, only for active links.</li>
<li><span class="mono">POST /events/motion/start</span>: camera-only; sends realtime to linked clients, queues push if offline.</li>
<li><span class="mono">POST /streams/request</span>: creates stream session + start_stream command + realtime notification.</li>
<li><span class="mono">POST /streams/:id/accept</span>: camera transitions stream to streaming; creates media session and optional SFU bootstrap.</li>
<li><span class="mono">GET /streams/:id/publish-credentials</span>: camera-only credential issuance.</li>
<li><span class="mono">GET /streams/:id/subscribe-credentials</span>: participant credential issuance.</li>
<li><span class="mono">POST /streams/:id/end</span>: closes session, ends SFU (if enabled), creates recording placeholder, notifies both parties.</li>
</ul>
</details>
<details>
<summary>Storage and Recordings</summary>
<ul>
<li><span class="mono">POST /videos/upload-url</span>: session route to mint presigned PUT + metadata row.</li>
<li><span class="mono">POST /recordings/:id/finalize</span>: camera marks recording ready once object exists, or creates simulator placeholder if object key starts with <span class="mono">sim/</span>.</li>
<li><span class="mono">GET /recordings/:id/download-url</span>: requester/camera only, ready-only, verifies object exists before presigning.</li>
</ul>
</details>
</section>
<section id="workers">
<h2>9) Workers and Reliability Mechanisms</h2>
<div class="grid g3">
<div class="card">
<h3>Command retry loop</h3>
<p>Inside realtime gateway. Scans <span class="mono">device_commands</span> where status is <span class="mono">sent</span> and stale by >10s. Re-dispatches every 5s. Fails after 3 retries.</p>
</div>
<div class="card">
<h3>Push worker</h3>
<p>Interval (default 10s) dispatches queued notifications with <span class="mono">nextAttemptAt &lt;= now</span>. Missing push token triggers retry backoff; max attempts configurable.</p>
</div>
<div class="card">
<h3>Recording worker</h3>
<p>Interval (default 30s) marks stale <span class="mono">awaiting_upload</span> recordings as <span class="mono">failed</span> after timeout window (default 30 min).</p>
</div>
</div>
<p class="small">Workers perform startup guards using <span class="mono">hasRequiredTables()</span> so they do not run before migrations are applied.</p>
</section>
<section id="security">
<h2>10) Security Controls and Guardrails</h2>
<div class="grid g2">
<div class="card">
<h3>Implemented</h3>
<ul>
<li><strong>Helmet CSP</strong> with explicit script/style/font/connect/media/image directives.</li>
<li><strong>CORS</strong> tied to <span class="mono">BETTER_AUTH_TRUSTED_ORIGINS</span> (or permissive fallback).</li>
<li><strong>Rate limiting</strong> globally and on high-traffic route groups.</li>
<li><strong>Ownership checks</strong> on almost all queries (user-scoped data access).</li>
<li><strong>Role constraints</strong> (for example client->camera command direction).</li>
<li><strong>Token integrity</strong> via timing-safe HMAC verification.</li>
</ul>
</div>
<div class="card">
<h3>Important caveats</h3>
<ul>
<li>Rate limits are in-memory; not shared across replicas.</li>
<li>Metrics are in-memory counters only (no persistence/export protocol).</li>
<li>Mock push provider treats presence of push token as delivery success.</li>
<li>Mock media provider + SFU scaffold are not production media infrastructure.</li>
</ul>
</div>
</div>
</section>
<section id="config">
<h2>11) Configuration Map (Key Env Variables)</h2>
<table>
<thead>
<tr>
<th>Domain</th>
<th>Variables</th>
<th>Architectural effect</th>
</tr>
</thead>
<tbody>
<tr>
<td>Core server</td>
<td><span class="mono">PORT</span>, <span class="mono">DATABASE_URL</span></td>
<td>listener + DB connectivity</td>
</tr>
<tr>
<td>Auth</td>
<td><span class="mono">BETTER_AUTH_SECRET</span>, <span class="mono">BETTER_AUTH_BASE_URL</span>, <span class="mono">BETTER_AUTH_TRUSTED_ORIGINS</span></td>
<td>session signing, base URL, trusted origins, device token signing</td>
</tr>
<tr>
<td>Presence</td>
<td><span class="mono">DEVICE_ONLINE_STALE_SECONDS</span></td>
<td>effective online/offline projection in device listings</td>
</tr>
<tr>
<td>Storage</td>
<td><span class="mono">MINIO_ENDPOINT</span>, <span class="mono">MINIO_PORT</span>, <span class="mono">MINIO_USE_SSL</span>, <span class="mono">MINIO_ACCESS_KEY</span>, <span class="mono">MINIO_SECRET_KEY</span>, <span class="mono">MINIO_BUCKET</span>, <span class="mono">MINIO_PRESIGNED_EXPIRY_SECONDS</span></td>
<td>object I/O, presign TTL, startup bucket bootstrap</td>
</tr>
<tr>
<td>Media</td>
<td><span class="mono">MEDIA_MODE</span>, <span class="mono">MEDIA_PROVIDER</span>, <span class="mono">TURN_URLS</span>, <span class="mono">TURN_USERNAME</span>, <span class="mono">TURN_CREDENTIAL</span></td>
<td>control plane mode and transport descriptor generation</td>
</tr>
<tr>
<td>Workers</td>
<td><span class="mono">PUSH_WORKER_INTERVAL_MS</span>, <span class="mono">PUSH_MAX_ATTEMPTS</span>, <span class="mono">RECORDING_WORKER_INTERVAL_MS</span>, <span class="mono">RECORDING_STALE_SECONDS</span></td>
<td>queue throughput, retry/failure timing</td>
</tr>
<tr>
<td>Admin</td>
<td><span class="mono">ADMIN_USERNAME</span>, <span class="mono">ADMIN_PASSWORD</span></td>
<td>required to mount admin dashboard route logic</td>
</tr>
</tbody>
</table>
</section>
<section id="code-map">
<h2>12) Code Ownership Map (Where to Modify What)</h2>
<div class="grid g2">
<div class="card">
<h3>Server composition</h3>
<p><span class="mono">index.ts</span>: middleware stack, route mounting, startup ordering, workers, realtime setup.</p>
<h3>Identity</h3>
<p><span class="mono">auth.ts</span>, <span class="mono">middleware/auth.ts</span>, <span class="mono">middleware/device-auth.ts</span>, <span class="mono">utils/device-token.ts</span>.</p>
<h3>Persistence schema</h3>
<p><span class="mono">db/schema.ts</span> + <span class="mono">drizzle/*</span> migrations.</p>
</div>
<div class="card">
<h3>Realtime + command delivery</h3>
<p><span class="mono">realtime/gateway.ts</span> and <span class="mono">routes/commands.ts</span>.</p>
<h3>Streaming control</h3>
<p><span class="mono">routes/streams.ts</span>, <span class="mono">media/*</span>, <span class="mono">routes/recordings.ts</span>.</p>
<h3>Operational views</h3>
<p><span class="mono">routes/ops.ts</span>, <span class="mono">observability/metrics.ts</span>, <span class="mono">routes/admin.ts</span>.</p>
</div>
</div>
</section>
<section id="constraints">
<h2>13) Current Constraints and Scaling Boundaries</h2>
<div class="grid g3">
<div class="card">
<h3>State locality</h3>
<p>Presence, rate-limit counters, metrics counters, and SFU registry are process-local. Horizontal scaling requires external shared state.</p>
</div>
<div class="card">
<h3>Media realism</h3>
<p>Media provider is mock; SFU service is scaffold/noop. Production deployment needs real media infrastructure for reliability and scale.</p>
</div>
<div class="card">
<h3>Queue semantics</h3>
<p>Push and command retries are interval-based polling workers. Throughput, ordering guarantees, and dead-letter handling are minimal.</p>
</div>
</div>
<p class="small">For load-bearing evolution, the natural next architecture step is extracting shared state (Redis/queue), production media plane, and distributed rate/metrics telemetry.</p>
</section>
</main>
</div>
</div>
</body>
</html>

View File

@@ -1,268 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCam Web Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.6.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.glass-panel {
background: rgba(15, 15, 20, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(25, 25, 30, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
transition: all 0.2s ease;
}
.btn-premium:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.status-offline {
background-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.toast-enter {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
</head>
<body data-page="activity" class="h-screen bg-[#0a0a0c] text-gray-200 overflow-hidden flex">
<!-- Toast Container -->
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
<!-- MAIN APP SHELL -->
<div class="flex h-full w-full">
<!-- SIDEBAR NAVIGATION -->
<aside id="bottomNav"
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full">
<!-- Logo Area -->
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5">
</div>
<!-- Nav Links -->
<nav class="flex-1 py-6 px-3 space-y-2">
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="home">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="font-medium hidden lg:block text-sm">Dashboard</span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all relative data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="activity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="font-medium hidden lg:block text-sm">Activity Feed</span>
<span id="notificationDot"
class="absolute lg:relative lg:top-auto lg:right-auto top-3 right-3 lg:ml-auto w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="font-medium hidden lg:block text-sm">Settings</span>
</button>
</nav>
</aside>
<!-- CONTENT AREA -->
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
<!-- Top Bar -->
<header
class="h-16 shrink-0 border-b border-white/5 flex items-center justify-end px-6 relative z-10 glass-panel">
<div class="flex items-center gap-6">
<div id="connectionStatus" class="flex items-center gap-2">
<span class="status-dot status-offline transition-colors duration-300"></span>
<span class="text-xs text-gray-400 font-medium tracking-wide uppercase">OFFLINE</span>
</div>
<div class="h-6 w-px bg-white/10"></div>
<div id="authStatusBadge" class="flex items-center gap-2 text-sm text-gray-400">
<div
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold border border-white/10">
?</div>
<span class="hidden sm:inline">Signed Out</span>
</div>
</div>
</header>
<!-- Pages -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
<!-- ACTIVITY SCREEN -->
<section id="screen-activity" class="hidden flex-col gap-6 max-w-4xl mx-auto py-4">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold text-white tracking-tight">Activity History</h2>
<button id="clearActivityBtn"
class="btn btn-ghost text-gray-400 hover:bg-white/5 hover:text-white rounded-xl border border-transparent">Clear
Read</button>
</div>
<div class="glass-card rounded-3xl border border-white/5 p-2 overflow-hidden">
<div id="activityFeedList" class="divide-y divide-white/5">
<!-- Empty State -->
<div class="text-center py-16 opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-gray-600" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<p class="text-sm font-medium text-gray-400">All quiet. No notifications yet.</p>
</div>
</div>
</div>
</section>
</div>
</main>
</div> <!-- End Main App Shell -->
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<!-- Recording Modal -->
<div id="recordingModal"
class="fixed inset-0 bg-[#0a0a0c]/90 backdrop-blur z-[100] hidden items-center justify-center p-4 lg:p-10">
<div
class="w-full max-w-4xl glass-card rounded-3xl p-6 space-y-4 shadow-2xl border border-white/10 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between shrink-0">
<h3 id="recordingModalTitle" class="text-lg font-semibold text-white tracking-wide">Recording Playback</h3>
<button id="recordingModalCloseBtn"
class="btn btn-square btn-ghost text-gray-400 hover:text-white rounded-xl hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 min-h-0 bg-black rounded-2xl overflow-hidden relative border border-white/5 shadow-inner">
<video id="recordingModalVideo" class="w-full h-full object-contain" controls playsinline></video>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,286 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCam Web Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.6.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.glass-panel {
background: rgba(15, 15, 20, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(25, 25, 30, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
transition: all 0.2s ease;
}
.btn-premium:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.status-offline {
background-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.toast-enter {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
</head>
<body data-page="auth" class="h-screen bg-[#0a0a0c] text-gray-200 overflow-hidden flex">
<!-- Toast Container -->
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
<!-- MAIN APP SHELL -->
<div class="flex h-full w-full">
<!-- SIDEBAR NAVIGATION -->
<aside id="bottomNav"
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full">
<!-- Logo Area -->
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5">
</div>
<!-- Nav Links -->
<nav class="flex-1 py-6 px-3 space-y-2">
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="home">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="font-medium hidden lg:block text-sm">Dashboard</span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all relative data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="activity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="font-medium hidden lg:block text-sm">Activity Feed</span>
<span id="notificationDot"
class="absolute lg:relative lg:top-auto lg:right-auto top-3 right-3 lg:ml-auto w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="font-medium hidden lg:block text-sm">Settings</span>
</button>
</nav>
</aside>
<!-- CONTENT AREA -->
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
<!-- Top Bar -->
<header
class="h-16 shrink-0 border-b border-white/5 flex items-center justify-end px-6 relative z-10 glass-panel">
<div class="flex items-center gap-6">
<div id="connectionStatus" class="flex items-center gap-2">
<span class="status-dot status-offline transition-colors duration-300"></span>
<span class="text-xs text-gray-400 font-medium tracking-wide uppercase">OFFLINE</span>
</div>
<div class="h-6 w-px bg-white/10"></div>
<div id="authStatusBadge" class="flex items-center gap-2 text-sm text-gray-400">
<div
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold border border-white/10">
?</div>
<span class="hidden sm:inline">Signed Out</span>
</div>
</div>
</header>
<!-- Pages -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
<!-- AUTH SCREEN -->
<section id="screen-auth"
class="flex flex-col items-center justify-center min-h-[70vh] animate-fade-in max-w-sm mx-auto">
<div class="text-center space-y-3 mb-8">
<div
class="w-20 h-20 bg-gradient-to-tr from-blue-600 to-indigo-600 rounded-3xl mx-auto flex items-center justify-center shadow-lg shadow-blue-900/20">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h2 class="text-3xl font-bold text-white tracking-tight">SecureCam Web</h2>
<p class="text-gray-400 text-sm">Sign in to manage visual security from your browser.</p>
</div>
<div class="w-full glass-card p-6 md:p-8 rounded-3xl space-y-4 shadow-2xl">
<div class="form-control">
<label class="label hidden"><span class="label-text text-gray-400">Email</span></label>
<input id="authEmail" type="email" placeholder="Email address"
class="input bg-black/40 border-white/10 text-sm focus:border-blue-500 focus:outline-none transition-colors w-full h-12 rounded-xl" />
</div>
<div class="form-control">
<input id="authPassword" type="password" placeholder="Password"
class="input bg-black/40 border-white/10 text-sm focus:border-blue-500 focus:outline-none transition-colors w-full h-12 rounded-xl" />
</div>
<div id="authNameField" class="form-control hidden">
<input id="authName" type="text" placeholder="Your Name"
class="input bg-black/40 border-white/10 text-sm focus:border-blue-500 focus:outline-none transition-colors w-full h-12 rounded-xl" />
</div>
<div class="pt-4 space-y-4">
<button id="signInBtn"
class="btn btn-premium w-full h-12 rounded-xl shadow-lg shadow-blue-900/20 text-base">Sign In</button>
<div class="divider text-xs text-gray-600">OR</div>
<button id="toggleAuthModeBtn"
class="btn btn-ghost w-full text-gray-400 hover:text-white hover:bg-white/5 rounded-xl border border-white/5">Create
an account</button>
</div>
</div>
</section>
</div>
</main>
</div> <!-- End Main App Shell -->
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<!-- Recording Modal -->
<div id="recordingModal"
class="fixed inset-0 bg-[#0a0a0c]/90 backdrop-blur z-[100] hidden items-center justify-center p-4 lg:p-10">
<div
class="w-full max-w-4xl glass-card rounded-3xl p-6 space-y-4 shadow-2xl border border-white/10 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between shrink-0">
<h3 id="recordingModalTitle" class="text-lg font-semibold text-white tracking-wide">Recording Playback</h3>
<button id="recordingModalCloseBtn"
class="btn btn-square btn-ghost text-gray-400 hover:text-white rounded-xl hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 min-h-0 bg-black rounded-2xl overflow-hidden relative border border-white/5 shadow-inner">
<video id="recordingModalVideo" class="w-full h-full object-contain" controls playsinline></video>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,320 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCam Web Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.6.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.glass-panel {
background: rgba(15, 15, 20, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(25, 25, 30, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
transition: all 0.2s ease;
}
.btn-premium:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.status-offline {
background-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.toast-enter {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
</head>
<body data-page="camera" class="h-screen bg-[#0a0a0c] text-gray-200 overflow-hidden flex">
<!-- Toast Container -->
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
<!-- MAIN APP SHELL -->
<div class="flex h-full w-full">
<!-- SIDEBAR NAVIGATION -->
<aside id="bottomNav"
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full">
<!-- Logo Area -->
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5">
</div>
<!-- Nav Links -->
<nav class="flex-1 py-6 px-3 space-y-2">
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="home">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="font-medium hidden lg:block text-sm">Dashboard</span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all relative data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="activity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="font-medium hidden lg:block text-sm">Activity Feed</span>
<span id="notificationDot"
class="absolute lg:relative lg:top-auto lg:right-auto top-3 right-3 lg:ml-auto w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="font-medium hidden lg:block text-sm">Settings</span>
</button>
</nav>
</aside>
<!-- CONTENT AREA -->
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
<!-- Top Bar -->
<header
class="h-16 shrink-0 border-b border-white/5 flex items-center justify-end px-6 relative z-10 glass-panel">
<div class="flex items-center gap-6">
<div id="connectionStatus" class="flex items-center gap-2">
<span class="status-dot status-offline transition-colors duration-300"></span>
<span class="text-xs text-gray-400 font-medium tracking-wide uppercase">OFFLINE</span>
</div>
<div class="h-6 w-px bg-white/10"></div>
<div id="authStatusBadge" class="flex items-center gap-2 text-sm text-gray-400">
<div
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold border border-white/10">
?</div>
<span class="hidden sm:inline">Signed Out</span>
</div>
</div>
</header>
<!-- Pages -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
<!-- CAMERA DASHBOARD -->
<section id="screen-home-camera" class="hidden flex-col gap-10 max-w-7xl mx-auto h-full">
<div class="flex justify-between items-center shrink-0 mb-4">
<h2 class="text-2xl font-bold text-white tracking-tight">Camera Feed (Broadcasting)</h2>
</div>
<div class="flex-1 flex flex-col lg:flex-row gap-8 min-h-0">
<!-- Main Player -->
<div class="flex-1 glass-card rounded-3xl overflow-hidden relative flex flex-col border border-white/10">
<div id="cameraPreview" class="flex-1 bg-black relative flex items-center justify-center">
<video id="cameraVideo" class="absolute inset-0 w-full h-full object-cover hidden" autoplay playsinline
muted></video>
<!-- Recording indicator -->
<div
class="absolute top-4 left-4 z-20 flex items-center gap-2 px-3 py-1.5 rounded-full bg-black/50 backdrop-blur border border-white/10">
<span
class="w-2.5 h-2.5 bg-red-500 rounded-full shadow-[0_0_8px_rgba(239,68,68,0.8)] animate-pulse"></span>
<span class="text-xs text-white font-medium tracking-wide">REC</span>
</div>
</div>
<div id="cameraOfflineOverlay"
class="absolute inset-0 bg-[#0a0a0c]/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
<p class="text-gray-400 font-medium tracking-wide">Camera Offline</p>
<button id="cameraGoOnlineBtn"
class="btn btn-outline btn-success rounded-xl border-green-500/50 text-green-400 hover:bg-green-500/10 hover:border-green-400">
Go Online
</button>
</div>
</div>
<!-- Controls Sidebar -->
<div class="lg:w-80 shrink-0 flex flex-col gap-6">
<div class="glass-card p-6 rounded-3xl border border-white/5 space-y-4">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider">Manual Controls</h3>
<div class="space-y-3">
<button id="startMotionBtn"
class="btn w-full justify-start h-14 rounded-xl bg-white/5 border-white/5 hover:bg-red-500/10 hover:border-red-500/30 hover:text-red-400 transition-all font-medium group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-red-400 mr-2"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Simulate Motion Event
</button>
<button id="endMotionBtn"
class="btn w-full justify-start h-14 rounded-xl bg-white/5 border-white/5 hover:bg-white/10 font-medium disabled:opacity-30"
disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 mr-2" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg>
Stop Recording
</button>
</div>
</div>
<div class="glass-card p-6 rounded-3xl border border-white/5 h-80 flex flex-col min-h-0 overflow-hidden">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4 shrink-0">System Logs</h3>
<div id="cameraLogs"
class="flex-1 min-h-0 bg-black/40 rounded-xl p-4 text-xs font-mono text-gray-400 overflow-y-auto border border-white/5">
<div class="text-gray-600 italic">Awaiting connection...</div>
</div>
</div>
</div>
</div>
</section>
</div>
</main>
</div> <!-- End Main App Shell -->
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<!-- Recording Modal -->
<div id="recordingModal"
class="fixed inset-0 bg-[#0a0a0c]/90 backdrop-blur z-[100] hidden items-center justify-center p-4 lg:p-10">
<div
class="w-full max-w-4xl glass-card rounded-3xl p-6 space-y-4 shadow-2xl border border-white/10 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between shrink-0">
<h3 id="recordingModalTitle" class="text-lg font-semibold text-white tracking-wide">Recording Playback</h3>
<button id="recordingModalCloseBtn"
class="btn btn-square btn-ghost text-gray-400 hover:text-white rounded-xl hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 min-h-0 bg-black rounded-2xl overflow-hidden relative border border-white/5 shadow-inner">
<video id="recordingModalVideo" class="w-full h-full object-contain" controls playsinline></video>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,335 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCam Web Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.6.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.glass-panel {
background: rgba(15, 15, 20, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(25, 25, 30, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
transition: all 0.2s ease;
}
.btn-premium:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.status-offline {
background-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.toast-enter {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
</head>
<body data-page="client" class="h-screen bg-[#0a0a0c] text-gray-200 overflow-hidden flex">
<!-- Toast Container -->
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
<!-- MAIN APP SHELL -->
<div class="flex h-full w-full">
<!-- SIDEBAR NAVIGATION -->
<aside id="bottomNav"
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full">
<!-- Logo Area -->
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5">
</div>
<!-- Nav Links -->
<nav class="flex-1 py-6 px-3 space-y-2">
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="home">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="font-medium hidden lg:block text-sm">Dashboard</span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all relative data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="activity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="font-medium hidden lg:block text-sm">Activity Feed</span>
<span id="notificationDot"
class="absolute lg:relative lg:top-auto lg:right-auto top-3 right-3 lg:ml-auto w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="font-medium hidden lg:block text-sm">Settings</span>
</button>
</nav>
</aside>
<!-- CONTENT AREA -->
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
<!-- Top Bar -->
<header
class="h-16 shrink-0 border-b border-white/5 flex items-center justify-end px-6 relative z-10 glass-panel">
<div class="flex items-center gap-6">
<div id="connectionStatus" class="flex items-center gap-2">
<span class="status-dot status-offline transition-colors duration-300"></span>
<span class="text-xs text-gray-400 font-medium tracking-wide uppercase">OFFLINE</span>
</div>
<div class="h-6 w-px bg-white/10"></div>
<div id="authStatusBadge" class="flex items-center gap-2 text-sm text-gray-400">
<div
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold border border-white/10">
?</div>
<span class="hidden sm:inline">Signed Out</span>
</div>
</div>
</header>
<!-- Pages -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
<!-- CLIENT DASHBOARD -->
<section id="screen-home-client" class="hidden flex-col gap-12 max-w-7xl mx-auto h-full">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white tracking-tight">Client Dashboard</h2>
<div class="flex gap-3">
<button id="linkCameraBtn"
class="btn btn-outline border-white/10 text-gray-300 hover:text-white hover:bg-white/5 rounded-xl gap-2 shadow-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Link Camera
</button>
<button id="refreshClientBtn"
class="btn btn-ghost btn-square rounded-xl bg-white/5 border border-white/5 hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
<!-- Cameras -> Horizontal List -->
<div class="glass-card rounded-3xl border border-white/5 p-5 shrink-0 mb-4">
<h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4">Your Cameras</h3>
<div id="linkedCamerasList" class="flex overflow-x-auto gap-4 pb-2 snap-x">
<!-- Populated by JS -->
<div class="w-full text-center py-6 bg-black/20 rounded-2xl border border-dashed border-white/10">
<p class="text-gray-500 text-sm">No cameras linked yet</p>
</div>
</div>
</div>
<!-- Theater Player Layout -->
<div class="flex flex-col xl:flex-row gap-8 flex-1 min-h-0">
<!-- Stream Viewer -->
<div id="clientStreamViewerWrapper"
class="flex-1 glass-card rounded-3xl overflow-hidden border border-white/10 flex flex-col shadow-xl min-h-[400px] hidden">
<div class="px-5 py-4 border-b border-white/5 bg-black/20 flex justify-between items-center">
<div class="flex items-center gap-3">
<span class="w-2 h-2 rounded-full bg-red-500 animate-[pulse_2s_ease-in-out_infinite] hidden"
id="clientLiveDot"></span>
<h3 class="font-medium text-white tracking-wide" id="clientStreamViewerTitle">Live Feed Viewer</h3>
</div>
<button id="closeStreamViewerBtn"
class="btn btn-ghost btn-sm btn-circle text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div id="clientStreamContainer" class="flex-1 bg-black relative flex items-center justify-center">
<!-- This video element receives the WebRTC stream -->
<video id="clientStreamVideo" class="absolute inset-0 w-full h-full object-contain hidden"
playsinline></video>
<img id="clientStreamImage" class="absolute inset-0 w-full h-full object-contain hidden" />
<div id="clientStreamPlaceholder" class="flex flex-col items-center gap-4 animate-pulse">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
<p class="text-sm font-medium text-gray-500 tracking-wide uppercase">Select a camera to view</p>
</div>
</div>
</div>
<!-- Right Sidebar (Recordings) -->
<div class="xl:w-96 shrink-0 flex flex-col gap-6 overflow-y-auto pr-2">
<!-- Recordings -->
<div class="glass-card rounded-3xl border border-white/5 p-5 flex-1">
<h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4">Recent Recordings</h3>
<div id="recordingsList" class="space-y-3">
<!-- Populated by JS -->
</div>
</div>
</div>
</div>
</section>
</div>
</main>
</div> <!-- End Main App Shell -->
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<!-- Recording Modal -->
<div id="recordingModal"
class="fixed inset-0 bg-[#0a0a0c]/90 backdrop-blur z-[100] hidden items-center justify-center p-4 lg:p-10">
<div
class="w-full max-w-4xl glass-card rounded-3xl p-6 space-y-4 shadow-2xl border border-white/10 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between shrink-0">
<h3 id="recordingModalTitle" class="text-lg font-semibold text-white tracking-wide">Recording Playback</h3>
<button id="recordingModalCloseBtn"
class="btn btn-square btn-ghost text-gray-400 hover:text-white rounded-xl hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 min-h-0 bg-black rounded-2xl overflow-hidden relative border border-white/5 shadow-inner">
<video id="recordingModalVideo" class="w-full h-full object-contain" controls playsinline></video>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,290 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCam Web Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.6.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.glass-panel {
background: rgba(15, 15, 20, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(25, 25, 30, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
transition: all 0.2s ease;
}
.btn-premium:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.status-offline {
background-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.toast-enter {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
</head>
<body data-page="onboarding" class="h-screen bg-[#0a0a0c] text-gray-200 overflow-hidden flex">
<!-- Toast Container -->
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
<!-- MAIN APP SHELL -->
<div class="flex h-full w-full">
<!-- SIDEBAR NAVIGATION -->
<aside id="bottomNav"
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full">
<!-- Logo Area -->
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5">
</div>
<!-- Nav Links -->
<nav class="flex-1 py-6 px-3 space-y-2">
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="home">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="font-medium hidden lg:block text-sm">Dashboard</span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all relative data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="activity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="font-medium hidden lg:block text-sm">Activity Feed</span>
<span id="notificationDot"
class="absolute lg:relative lg:top-auto lg:right-auto top-3 right-3 lg:ml-auto w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="font-medium hidden lg:block text-sm">Settings</span>
</button>
</nav>
</aside>
<!-- CONTENT AREA -->
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
<!-- Top Bar -->
<header
class="h-16 shrink-0 border-b border-white/5 flex items-center justify-end px-6 relative z-10 glass-panel">
<div class="flex items-center gap-6">
<div id="connectionStatus" class="flex items-center gap-2">
<span class="status-dot status-offline transition-colors duration-300"></span>
<span class="text-xs text-gray-400 font-medium tracking-wide uppercase">OFFLINE</span>
</div>
<div class="h-6 w-px bg-white/10"></div>
<div id="authStatusBadge" class="flex items-center gap-2 text-sm text-gray-400">
<div
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold border border-white/10">
?</div>
<span class="hidden sm:inline">Signed Out</span>
</div>
</div>
</header>
<!-- Pages -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
<!-- ONBOARDING SCREEN -->
<section id="screen-onboarding"
class="hidden flex-col items-center justify-center min-h-[70vh] max-w-lg mx-auto">
<div class="text-center space-y-2 mb-8">
<h2 class="text-3xl font-bold text-white tracking-tight">Configure Device</h2>
<p class="text-sm text-gray-400">Set up this browser simulator's role</p>
</div>
<div class="w-full glass-card p-6 md:p-8 rounded-3xl space-y-6 shadow-2xl">
<div class="form-control w-full">
<label class="label"><span class="label-text text-xs font-semibold text-gray-400 tracking-wider">DEVICE
NAME</span></label>
<input id="deviceName" type="text" placeholder="e.g. Living Room Cam"
class="input input-bordered h-12 rounded-xl bg-black/40 border-white/10 focus:border-blue-500" />
</div>
<div class="form-control w-full">
<label class="label"><span
class="label-text text-xs font-semibold text-gray-400 tracking-wider">ROLE</span></label>
<div class="grid grid-cols-2 gap-2 p-1.5 bg-black/40 rounded-xl border border-white/5">
<button
class="btn btn-ghost normal-case text-gray-400 data-[active=true]:bg-blue-600 data-[active=true]:text-white rounded-lg h-10 min-h-0"
id="btn-role-camera" data-role="camera" data-active="false">Camera</button>
<button
class="btn btn-ghost normal-case text-gray-400 data-[active=true]:bg-blue-600 data-[active=true]:text-white rounded-lg h-10 min-h-0"
id="btn-role-client" data-role="client" data-active="true">Client</button>
</div>
<input type="hidden" id="role" value="client">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-xs font-semibold text-gray-400 tracking-wider">PUSH
TOKEN (OPTIONAL)</span></label>
<input id="pushToken" type="text" placeholder="simulated_token_123"
class="input input-bordered h-12 rounded-xl bg-black/40 border-white/10 focus:border-blue-500" />
</div>
<div class="pt-6">
<button id="registerBtn" class="btn btn-premium w-full h-12 rounded-xl text-base">Complete Setup</button>
<button id="loadSavedBtn"
class="btn btn-ghost w-full mt-3 text-gray-500 hover:text-white hover:bg-white/5">Load previously saved
device</button>
</div>
</div>
</section>
</div>
</main>
</div> <!-- End Main App Shell -->
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<!-- Recording Modal -->
<div id="recordingModal"
class="fixed inset-0 bg-[#0a0a0c]/90 backdrop-blur z-[100] hidden items-center justify-center p-4 lg:p-10">
<div
class="w-full max-w-4xl glass-card rounded-3xl p-6 space-y-4 shadow-2xl border border-white/10 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between shrink-0">
<h3 id="recordingModalTitle" class="text-lg font-semibold text-white tracking-wide">Recording Playback</h3>
<button id="recordingModalCloseBtn"
class="btn btn-square btn-ghost text-gray-400 hover:text-white rounded-xl hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 min-h-0 bg-black rounded-2xl overflow-hidden relative border border-white/5 shadow-inner">
<video id="recordingModalVideo" class="w-full h-full object-contain" controls playsinline></video>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,310 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCam Web Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.6.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.glass-panel {
background: rgba(15, 15, 20, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(25, 25, 30, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
transition: all 0.2s ease;
}
.btn-premium:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.status-offline {
background-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.toast-enter {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
</head>
<body data-page="settings" class="h-screen bg-[#0a0a0c] text-gray-200 overflow-hidden flex">
<!-- Toast Container -->
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
<!-- MAIN APP SHELL -->
<div class="flex h-full w-full">
<!-- SIDEBAR NAVIGATION -->
<aside id="bottomNav"
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full">
<!-- Logo Area -->
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5">
</div>
<!-- Nav Links -->
<nav class="flex-1 py-6 px-3 space-y-2">
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="home">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="font-medium hidden lg:block text-sm">Dashboard</span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all relative data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="activity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="font-medium hidden lg:block text-sm">Activity Feed</span>
<span id="notificationDot"
class="absolute lg:relative lg:top-auto lg:right-auto top-3 right-3 lg:ml-auto w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="font-medium hidden lg:block text-sm">Settings</span>
</button>
</nav>
</aside>
<!-- CONTENT AREA -->
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
<!-- Top Bar -->
<header
class="h-16 shrink-0 border-b border-white/5 flex items-center justify-end px-6 relative z-10 glass-panel">
<div class="flex items-center gap-6">
<div id="connectionStatus" class="flex items-center gap-2">
<span class="status-dot status-offline transition-colors duration-300"></span>
<span class="text-xs text-gray-400 font-medium tracking-wide uppercase">OFFLINE</span>
</div>
<div class="h-6 w-px bg-white/10"></div>
<div id="authStatusBadge" class="flex items-center gap-2 text-sm text-gray-400">
<div
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold border border-white/10">
?</div>
<span class="hidden sm:inline">Signed Out</span>
</div>
</div>
</header>
<!-- Pages -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
<!-- SETTINGS SCREEN -->
<section id="screen-settings" class="hidden flex-col gap-6 max-w-2xl mx-auto py-8">
<h2 class="text-2xl font-bold text-white tracking-tight px-2">Settings</h2>
<div class="glass-card rounded-3xl border border-white/5 p-8 flex items-center gap-6">
<div
class="w-20 h-20 bg-blue-600/20 text-blue-500 rounded-full flex items-center justify-center leading-none text-2xl font-bold border border-blue-500/30"
id="profileInitials">U</div>
<div>
<h3 class="text-xl text-white font-semibold tracking-wide" id="profileName">User</h3>
<p class="text-gray-400 mt-1" id="profileEmail">user@example.com</p>
</div>
</div>
<div class="glass-card rounded-3xl border border-white/5 overflow-hidden">
<button id="checkOpsBtn"
class="w-full flex items-center justify-between p-5 text-left text-gray-300 hover:bg-white/5 transition-colors border-b border-white/5 group">
<div class="flex items-center gap-4">
<div
class="p-2 rounded-lg bg-white/5 group-hover:bg-blue-500/20 group-hover:text-blue-400 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<span class="font-medium">Run Diagnostics</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<button
class="w-full flex items-center justify-between p-5 text-left text-gray-300 hover:bg-white/5 transition-colors group">
<div class="flex items-center gap-4">
<div
class="p-2 rounded-lg bg-white/5 group-hover:bg-blue-500/20 group-hover:text-blue-400 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<span class="font-medium">Device Information</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<button id="signOutBtn"
class="btn btn-error btn-outline w-full rounded-2xl h-14 text-base font-medium gap-3 border-red-500/50 hover:bg-red-500/10 hover:border-red-500 mt-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
Sign Out
</button>
</section>
</div>
</main>
</div> <!-- End Main App Shell -->
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<!-- Recording Modal -->
<div id="recordingModal"
class="fixed inset-0 bg-[#0a0a0c]/90 backdrop-blur z-[100] hidden items-center justify-center p-4 lg:p-10">
<div
class="w-full max-w-4xl glass-card rounded-3xl p-6 space-y-4 shadow-2xl border border-white/10 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between shrink-0">
<h3 id="recordingModalTitle" class="text-lg font-semibold text-white tracking-wide">Recording Playback</h3>
<button id="recordingModalCloseBtn"
class="btn btn-square btn-ghost text-gray-400 hover:text-white rounded-xl hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 min-h-0 bg-black rounded-2xl overflow-hidden relative border border-white/5 shadow-inner">
<video id="recordingModalVideo" class="w-full h-full object-contain" controls playsinline></video>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,584 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCam Web Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.6.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.glass-panel {
background: rgba(15, 15, 20, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(25, 25, 30, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-premium {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
transition: all 0.2s ease;
}
.btn-premium:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.status-offline {
background-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.toast-enter {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
</head>
<body class="h-screen bg-[#0a0a0c] text-gray-200 overflow-hidden flex">
<!-- Toast Container -->
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
<!-- MAIN APP SHELL -->
<div class="flex h-full w-full">
<!-- SIDEBAR NAVIGATION -->
<aside id="bottomNav"
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full">
<!-- Logo Area -->
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5">
</div>
<!-- Nav Links -->
<nav class="flex-1 py-6 px-3 space-y-2">
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="home">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="font-medium hidden lg:block text-sm">Dashboard</span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all relative data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="activity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="font-medium hidden lg:block text-sm">Activity Feed</span>
<span id="notificationDot"
class="absolute lg:relative lg:top-auto lg:right-auto top-3 right-3 lg:ml-auto w-2 h-2 bg-red-500 rounded-full hidden"></span>
</button>
<button
class="nav-btn w-full flex items-center gap-3 p-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all data-[active=true]:text-white data-[active=true]:bg-blue-600/10 data-[active=true]:border data-[active=true]:border-blue-500/20 group"
data-target="settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="font-medium hidden lg:block text-sm">Settings</span>
</button>
</nav>
</aside>
<!-- CONTENT AREA -->
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
<!-- Top Bar -->
<header
class="h-16 shrink-0 border-b border-white/5 flex items-center justify-end px-6 relative z-10 glass-panel">
<div class="flex items-center gap-6">
<div id="connectionStatus" class="flex items-center gap-2">
<span class="status-dot status-offline transition-colors duration-300"></span>
<span class="text-xs text-gray-400 font-medium tracking-wide uppercase">OFFLINE</span>
</div>
<div class="h-6 w-px bg-white/10"></div>
<div id="authStatusBadge" class="flex items-center gap-2 text-sm text-gray-400">
<div
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold border border-white/10">
?</div>
<span class="hidden sm:inline">Signed Out</span>
</div>
</div>
</header>
<!-- Pages -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
<!-- AUTH SCREEN -->
<section id="screen-auth"
class="flex flex-col items-center justify-center min-h-[70vh] animate-fade-in max-w-sm mx-auto">
<div class="text-center space-y-3 mb-8">
<div
class="w-20 h-20 bg-gradient-to-tr from-blue-600 to-indigo-600 rounded-3xl mx-auto flex items-center justify-center shadow-lg shadow-blue-900/20">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h2 class="text-3xl font-bold text-white tracking-tight">SecureCam Web</h2>
<p class="text-gray-400 text-sm">Sign in to manage visual security from your browser.</p>
</div>
<div class="w-full glass-card p-6 md:p-8 rounded-3xl space-y-4 shadow-2xl">
<div class="form-control">
<label class="label hidden"><span class="label-text text-gray-400">Email</span></label>
<input id="authEmail" type="email" placeholder="Email address"
class="input bg-black/40 border-white/10 text-sm focus:border-blue-500 focus:outline-none transition-colors w-full h-12 rounded-xl" />
</div>
<div class="form-control">
<input id="authPassword" type="password" placeholder="Password"
class="input bg-black/40 border-white/10 text-sm focus:border-blue-500 focus:outline-none transition-colors w-full h-12 rounded-xl" />
</div>
<div id="authNameField" class="form-control hidden">
<input id="authName" type="text" placeholder="Your Name"
class="input bg-black/40 border-white/10 text-sm focus:border-blue-500 focus:outline-none transition-colors w-full h-12 rounded-xl" />
</div>
<div class="pt-4 space-y-4">
<button id="signInBtn"
class="btn btn-premium w-full h-12 rounded-xl shadow-lg shadow-blue-900/20 text-base">Sign In</button>
<div class="divider text-xs text-gray-600">OR</div>
<button id="toggleAuthModeBtn"
class="btn btn-ghost w-full text-gray-400 hover:text-white hover:bg-white/5 rounded-xl border border-white/5">Create
an account</button>
</div>
</div>
</section>
<!-- ONBOARDING SCREEN -->
<section id="screen-onboarding"
class="hidden flex-col items-center justify-center min-h-[70vh] max-w-lg mx-auto">
<div class="text-center space-y-2 mb-8">
<h2 class="text-3xl font-bold text-white tracking-tight">Configure Device</h2>
<p class="text-sm text-gray-400">Set up this browser simulator's role</p>
</div>
<div class="w-full glass-card p-6 md:p-8 rounded-3xl space-y-6 shadow-2xl">
<div class="form-control w-full">
<label class="label"><span class="label-text text-xs font-semibold text-gray-400 tracking-wider">DEVICE
NAME</span></label>
<input id="deviceName" type="text" placeholder="e.g. Living Room Cam"
class="input input-bordered h-12 rounded-xl bg-black/40 border-white/10 focus:border-blue-500" />
</div>
<div class="form-control w-full">
<label class="label"><span
class="label-text text-xs font-semibold text-gray-400 tracking-wider">ROLE</span></label>
<div class="grid grid-cols-2 gap-2 p-1.5 bg-black/40 rounded-xl border border-white/5">
<button
class="btn btn-ghost normal-case text-gray-400 data-[active=true]:bg-blue-600 data-[active=true]:text-white rounded-lg h-10 min-h-0"
id="btn-role-camera" data-role="camera" data-active="false">Camera</button>
<button
class="btn btn-ghost normal-case text-gray-400 data-[active=true]:bg-blue-600 data-[active=true]:text-white rounded-lg h-10 min-h-0"
id="btn-role-client" data-role="client" data-active="true">Client</button>
</div>
<input type="hidden" id="role" value="client">
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text text-xs font-semibold text-gray-400 tracking-wider">PUSH
TOKEN (OPTIONAL)</span></label>
<input id="pushToken" type="text" placeholder="simulated_token_123"
class="input input-bordered h-12 rounded-xl bg-black/40 border-white/10 focus:border-blue-500" />
</div>
<div class="pt-6">
<button id="registerBtn" class="btn btn-premium w-full h-12 rounded-xl text-base">Complete Setup</button>
<button id="loadSavedBtn"
class="btn btn-ghost w-full mt-3 text-gray-500 hover:text-white hover:bg-white/5">Load previously saved
device</button>
</div>
</div>
</section>
<!-- CAMERA DASHBOARD -->
<section id="screen-home-camera" class="hidden flex-col gap-10 max-w-7xl mx-auto h-full">
<div class="flex justify-between items-center shrink-0 mb-4">
<h2 class="text-2xl font-bold text-white tracking-tight">Camera Feed (Broadcasting)</h2>
</div>
<div class="flex-1 flex flex-col lg:flex-row gap-8 min-h-0">
<!-- Main Player -->
<div class="flex-1 glass-card rounded-3xl overflow-hidden relative flex flex-col border border-white/10">
<div id="cameraPreview" class="flex-1 bg-black relative flex items-center justify-center">
<video id="cameraVideo" class="absolute inset-0 w-full h-full object-cover hidden" autoplay playsinline
muted></video>
<!-- Recording indicator -->
<div
class="absolute top-4 left-4 z-20 flex items-center gap-2 px-3 py-1.5 rounded-full bg-black/50 backdrop-blur border border-white/10">
<span
class="w-2.5 h-2.5 bg-red-500 rounded-full shadow-[0_0_8px_rgba(239,68,68,0.8)] animate-pulse"></span>
<span class="text-xs text-white font-medium tracking-wide">REC</span>
</div>
</div>
<div id="cameraOfflineOverlay"
class="absolute inset-0 bg-[#0a0a0c]/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
<p class="text-gray-400 font-medium tracking-wide">Camera Offline</p>
<button id="cameraGoOnlineBtn"
class="btn btn-outline btn-success rounded-xl border-green-500/50 text-green-400 hover:bg-green-500/10 hover:border-green-400">
Go Online
</button>
</div>
</div>
<!-- Controls Sidebar -->
<div class="lg:w-80 shrink-0 flex flex-col gap-6">
<div class="glass-card p-6 rounded-3xl border border-white/5 space-y-4">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider">Manual Controls</h3>
<div class="space-y-3">
<button id="startMotionBtn"
class="btn w-full justify-start h-14 rounded-xl bg-white/5 border-white/5 hover:bg-red-500/10 hover:border-red-500/30 hover:text-red-400 transition-all font-medium group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-red-400 mr-2"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Simulate Motion Event
</button>
<button id="endMotionBtn"
class="btn w-full justify-start h-14 rounded-xl bg-white/5 border-white/5 hover:bg-white/10 font-medium disabled:opacity-30"
disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 mr-2" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg>
Stop Recording
</button>
</div>
</div>
<div class="glass-card p-6 rounded-3xl border border-white/5 h-80 flex flex-col min-h-0 overflow-hidden">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4 shrink-0">System Logs</h3>
<div id="cameraLogs"
class="flex-1 min-h-0 bg-black/40 rounded-xl p-4 text-xs font-mono text-gray-400 overflow-y-auto border border-white/5">
<div class="text-gray-600 italic">Awaiting connection...</div>
</div>
</div>
</div>
</div>
</section>
<!-- CLIENT DASHBOARD -->
<section id="screen-home-client" class="hidden flex-col gap-12 max-w-7xl mx-auto h-full">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white tracking-tight">Client Dashboard</h2>
<div class="flex gap-3">
<button id="linkCameraBtn"
class="btn btn-outline border-white/10 text-gray-300 hover:text-white hover:bg-white/5 rounded-xl gap-2 shadow-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Link Camera
</button>
<button id="refreshClientBtn"
class="btn btn-ghost btn-square rounded-xl bg-white/5 border border-white/5 hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
<!-- Cameras -> Horizontal List -->
<div class="glass-card rounded-3xl border border-white/5 p-5 shrink-0 mb-4">
<h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4">Your Cameras</h3>
<div id="linkedCamerasList" class="flex overflow-x-auto gap-4 pb-2 snap-x">
<!-- Populated by JS -->
<div class="w-full text-center py-6 bg-black/20 rounded-2xl border border-dashed border-white/10">
<p class="text-gray-500 text-sm">No cameras linked yet</p>
</div>
</div>
</div>
<!-- Theater Player Layout -->
<div class="flex flex-col xl:flex-row gap-8 flex-1 min-h-0">
<!-- Stream Viewer -->
<div id="clientStreamViewerWrapper"
class="flex-1 glass-card rounded-3xl overflow-hidden border border-white/10 flex flex-col shadow-xl min-h-[400px] hidden">
<div class="px-5 py-4 border-b border-white/5 bg-black/20 flex justify-between items-center">
<div class="flex items-center gap-3">
<span class="w-2 h-2 rounded-full bg-red-500 animate-[pulse_2s_ease-in-out_infinite] hidden"
id="clientLiveDot"></span>
<h3 class="font-medium text-white tracking-wide" id="clientStreamViewerTitle">Live Feed Viewer</h3>
</div>
<button id="closeStreamViewerBtn"
class="btn btn-ghost btn-sm btn-circle text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div id="clientStreamContainer" class="flex-1 bg-black relative flex items-center justify-center">
<!-- This video element receives the WebRTC stream -->
<video id="clientStreamVideo" class="absolute inset-0 w-full h-full object-contain hidden"
playsinline></video>
<img id="clientStreamImage" class="absolute inset-0 w-full h-full object-contain hidden" />
<div id="clientStreamPlaceholder" class="flex flex-col items-center gap-4 animate-pulse">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
<p class="text-sm font-medium text-gray-500 tracking-wide uppercase">Select a camera to view</p>
</div>
</div>
</div>
<!-- Right Sidebar (Recordings) -->
<div class="xl:w-96 shrink-0 flex flex-col gap-6 overflow-y-auto pr-2">
<!-- Recordings -->
<div class="glass-card rounded-3xl border border-white/5 p-5 flex-1">
<h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4">Recent Recordings</h3>
<div id="recordingsList" class="space-y-3">
<!-- Populated by JS -->
</div>
</div>
</div>
</div>
</section>
<!-- ACTIVITY SCREEN -->
<section id="screen-activity" class="hidden flex-col gap-6 max-w-4xl mx-auto py-4">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold text-white tracking-tight">Activity History</h2>
<button id="clearActivityBtn"
class="btn btn-ghost text-gray-400 hover:bg-white/5 hover:text-white rounded-xl border border-transparent">Clear
Read</button>
</div>
<div class="glass-card rounded-3xl border border-white/5 p-2 overflow-hidden">
<div id="activityFeedList" class="divide-y divide-white/5">
<!-- Empty State -->
<div class="text-center py-16 opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-gray-600" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<p class="text-sm font-medium text-gray-400">All quiet. No notifications yet.</p>
</div>
</div>
</div>
</section>
<!-- SETTINGS SCREEN -->
<section id="screen-settings" class="hidden flex-col gap-6 max-w-2xl mx-auto py-8">
<h2 class="text-2xl font-bold text-white tracking-tight px-2">Settings</h2>
<div class="glass-card rounded-3xl border border-white/5 p-8 flex items-center gap-6">
<div
class="w-20 h-20 bg-blue-600/20 text-blue-500 rounded-full flex items-center justify-center leading-none text-2xl font-bold border border-blue-500/30"
id="profileInitials">U</div>
<div>
<h3 class="text-xl text-white font-semibold tracking-wide" id="profileName">User</h3>
<p class="text-gray-400 mt-1" id="profileEmail">user@example.com</p>
</div>
</div>
<div class="glass-card rounded-3xl border border-white/5 overflow-hidden">
<button id="checkOpsBtn"
class="w-full flex items-center justify-between p-5 text-left text-gray-300 hover:bg-white/5 transition-colors border-b border-white/5 group">
<div class="flex items-center gap-4">
<div
class="p-2 rounded-lg bg-white/5 group-hover:bg-blue-500/20 group-hover:text-blue-400 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<span class="font-medium">Run Diagnostics</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<button
class="w-full flex items-center justify-between p-5 text-left text-gray-300 hover:bg-white/5 transition-colors group">
<div class="flex items-center gap-4">
<div
class="p-2 rounded-lg bg-white/5 group-hover:bg-blue-500/20 group-hover:text-blue-400 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<span class="font-medium">Device Information</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<button id="signOutBtn"
class="btn btn-error btn-outline w-full rounded-2xl h-14 text-base font-medium gap-3 border-red-500/50 hover:bg-red-500/10 hover:border-red-500 mt-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
Sign Out
</button>
</section>
</div>
</main>
</div> <!-- End Main App Shell -->
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<!-- Recording Modal -->
<div id="recordingModal"
class="fixed inset-0 bg-[#0a0a0c]/90 backdrop-blur z-[100] hidden items-center justify-center p-4 lg:p-10">
<div
class="w-full max-w-4xl glass-card rounded-3xl p-6 space-y-4 shadow-2xl border border-white/10 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between shrink-0">
<h3 id="recordingModalTitle" class="text-lg font-semibold text-white tracking-wide">Recording Playback</h3>
<button id="recordingModalCloseBtn"
class="btn btn-square btn-ghost text-gray-400 hover:text-white rounded-xl hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 min-h-0 bg-black rounded-2xl overflow-hidden relative border border-white/5 shadow-inner">
<video id="recordingModalVideo" class="w-full h-full object-contain" controls playsinline></video>
</div>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff