feat: Introduce mobile simulator UI with new routes, screens, shared application chrome, and updated dependencies.
This commit is contained in:
@@ -179,6 +179,9 @@ Split-page entrypoints are also available:
|
||||
- `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
|
||||
|
||||
934
Backend/public/backend-architecture.html
Normal file
934
Backend/public/backend-architecture.html
Normal file
@@ -0,0 +1,934 @@
|
||||
<!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 same-owner target validation.</li>
|
||||
<li><span class="mono">stream:frame</span> relay fallback (base64 image snapshots).</li>
|
||||
<li>Retry worker for stale sent commands every 5s, max 3 retries.</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 <= 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>
|
||||
@@ -13,10 +13,12 @@
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^24",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"daisyui": "^5.5.19",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"globals": "^17.3.0",
|
||||
"playwright": "^1.58.1",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"svelte": "^5.49.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
@@ -171,6 +173,8 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
|
||||
|
||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
|
||||
@@ -307,6 +311,8 @@
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
@@ -317,6 +323,10 @@
|
||||
|
||||
"devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
|
||||
|
||||
"engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
|
||||
|
||||
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
@@ -517,6 +527,10 @@
|
||||
|
||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||
|
||||
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
||||
|
||||
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
@@ -577,6 +591,8 @@
|
||||
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||
|
||||
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
@@ -607,6 +623,8 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
||||
"eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^24",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"daisyui": "^5.5.19",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"globals": "^17.3.0",
|
||||
"playwright": "^1.58.1",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"svelte": "^5.49.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
|
||||
2040
WebApp/src/lib/sim/mobile-sim.js
Normal file
2040
WebApp/src/lib/sim/mobile-sim.js
Normal file
File diff suppressed because it is too large
Load Diff
27
WebApp/src/lib/sim/screens/ActivityScreen.svelte
Normal file
27
WebApp/src/lib/sim/screens/ActivityScreen.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<section id="screen-activity" class="flex 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">
|
||||
<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>
|
||||
62
WebApp/src/lib/sim/screens/AuthScreen.svelte
Normal file
62
WebApp/src/lib/sim/screens/AuthScreen.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<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" for="authEmail"><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>
|
||||
81
WebApp/src/lib/sim/screens/CameraScreen.svelte
Normal file
81
WebApp/src/lib/sim/screens/CameraScreen.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<section id="screen-home-camera" class="flex 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">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
88
WebApp/src/lib/sim/screens/ClientScreen.svelte
Normal file
88
WebApp/src/lib/sim/screens/ClientScreen.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<section id="screen-home-client" class="flex 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"
|
||||
aria-label="Refresh linked cameras"
|
||||
title="Refresh linked cameras"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="flex flex-col xl:flex-row gap-8 flex-1 min-h-0">
|
||||
<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"
|
||||
aria-label="Close stream viewer"
|
||||
title="Close stream viewer"
|
||||
>
|
||||
<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">
|
||||
<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" alt="Live stream frame" />
|
||||
|
||||
<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>
|
||||
|
||||
<div class="xl:w-96 shrink-0 flex flex-col gap-6 overflow-y-auto pr-2">
|
||||
<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"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
69
WebApp/src/lib/sim/screens/OnboardingScreen.svelte
Normal file
69
WebApp/src/lib/sim/screens/OnboardingScreen.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<section id="screen-onboarding" class="flex 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" for="deviceName"
|
||||
><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" for="role"
|
||||
><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" for="pushToken"
|
||||
><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>
|
||||
74
WebApp/src/lib/sim/screens/SettingsScreen.svelte
Normal file
74
WebApp/src/lib/sim/screens/SettingsScreen.svelte
Normal file
@@ -0,0 +1,74 @@
|
||||
<section id="screen-settings" class="flex 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>
|
||||
145
WebApp/src/lib/sim/ui/AppChrome.svelte
Normal file
145
WebApp/src/lib/sim/ui/AppChrome.svelte
Normal file
@@ -0,0 +1,145 @@
|
||||
<script lang="ts">
|
||||
let { children, pageKey } = $props<{ children: () => unknown; pageKey: string }>();
|
||||
</script>
|
||||
|
||||
<div data-sim-page={pageKey} class="flex h-full w-full">
|
||||
<div id="toast-container" class="toast toast-top toast-end z-50"></div>
|
||||
|
||||
<aside
|
||||
id="bottomNav"
|
||||
class="w-20 lg:w-64 glass-panel border-r border-white/5 flex-col justify-between hidden h-full"
|
||||
>
|
||||
<div class="p-6 flex items-center justify-center lg:justify-start gap-3 border-b border-white/5"></div>
|
||||
|
||||
<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>
|
||||
|
||||
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
|
||||
<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>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
aria-label="Close recording modal"
|
||||
title="Close recording modal"
|
||||
>
|
||||
<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>
|
||||
@@ -1,9 +1,34 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import { onMount } from 'svelte';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
const bodyClasses = ['h-screen', 'bg-[#0a0a0c]', 'text-gray-200', 'overflow-hidden', 'flex'];
|
||||
document.body.classList.add(...bodyClasses);
|
||||
document.documentElement.dataset.theme = 'black';
|
||||
const simWindow = window as Window & { __mobileSimLoaded?: boolean };
|
||||
if (!simWindow.__mobileSimLoaded) {
|
||||
simWindow.__mobileSimLoaded = true;
|
||||
void import('$lib/sim/mobile-sim.js');
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove(...bodyClasses);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
<svelte:head>
|
||||
<title>SecureCam Web Dashboard</title>
|
||||
<link rel="icon" href={favicon} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</svelte:head>
|
||||
{@render children()}
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<script lang="ts">
|
||||
import AuthScreen from '$lib/sim/screens/AuthScreen.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="auth">
|
||||
<AuthScreen />
|
||||
</AppChrome>
|
||||
|
||||
8
WebApp/src/routes/activity/+page.svelte
Normal file
8
WebApp/src/routes/activity/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import ActivityScreen from '$lib/sim/screens/ActivityScreen.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="activity">
|
||||
<ActivityScreen />
|
||||
</AppChrome>
|
||||
8
WebApp/src/routes/camera/+page.svelte
Normal file
8
WebApp/src/routes/camera/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import CameraScreen from '$lib/sim/screens/CameraScreen.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="camera">
|
||||
<CameraScreen />
|
||||
</AppChrome>
|
||||
8
WebApp/src/routes/client/+page.svelte
Normal file
8
WebApp/src/routes/client/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import ClientScreen from '$lib/sim/screens/ClientScreen.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="client">
|
||||
<ClientScreen />
|
||||
</AppChrome>
|
||||
@@ -1 +1,81 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin "daisyui";
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(255 255 255 / 10%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgb(15 15 20 / 70%);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgb(255 255 255 / 5%);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgb(25 25 30 / 60%);
|
||||
border: 1px solid rgb(255 255 255 / 8%);
|
||||
}
|
||||
|
||||
.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 rgb(37 99 235 / 30%);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background-color: #10b981;
|
||||
box-shadow: 0 0 8px rgb(16 185 129 / 40%);
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background-color: #ef4444;
|
||||
box-shadow: 0 0 8px rgb(239 68 68 / 40%);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
8
WebApp/src/routes/onboarding/+page.svelte
Normal file
8
WebApp/src/routes/onboarding/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import OnboardingScreen from '$lib/sim/screens/OnboardingScreen.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="onboarding">
|
||||
<OnboardingScreen />
|
||||
</AppChrome>
|
||||
@@ -4,10 +4,10 @@ import { render } from 'vitest-browser-svelte';
|
||||
import Page from './+page.svelte';
|
||||
|
||||
describe('/+page.svelte', () => {
|
||||
it('should render h1', async () => {
|
||||
it('should render simulator auth heading', async () => {
|
||||
render(Page);
|
||||
|
||||
const heading = page.getByRole('heading', { level: 1 });
|
||||
const heading = page.getByRole('heading', { name: 'SecureCam Web' });
|
||||
await expect.element(heading).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
8
WebApp/src/routes/settings/+page.svelte
Normal file
8
WebApp/src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import SettingsScreen from '$lib/sim/screens/SettingsScreen.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="settings">
|
||||
<SettingsScreen />
|
||||
</AppChrome>
|
||||
@@ -3,8 +3,36 @@ import { playwright } from '@vitest/browser-playwright';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
const backendTarget = process.env.BACKEND_URL ?? 'http://localhost:3000';
|
||||
|
||||
const proxiedPaths = [
|
||||
'/api',
|
||||
'/devices',
|
||||
'/device-links',
|
||||
'/streams',
|
||||
'/events',
|
||||
'/recordings',
|
||||
'/videos',
|
||||
'/push-notifications',
|
||||
'/socket.io'
|
||||
] as const;
|
||||
|
||||
const proxy = Object.fromEntries(
|
||||
proxiedPaths.map((path) => [
|
||||
path,
|
||||
{
|
||||
target: backendTarget,
|
||||
changeOrigin: true,
|
||||
ws: path === '/socket.io'
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
server: {
|
||||
proxy
|
||||
},
|
||||
test: {
|
||||
expect: { requireAssertions: true },
|
||||
projects: [
|
||||
|
||||
Reference in New Issue
Block a user