Compare commits
10 Commits
69ec7108a5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c44043f38d | |||
| 2f74ff0c30 | |||
| fe732b47b4 | |||
| b03ee8ddb5 | |||
| 9b69be299f | |||
| c86efa6ee5 | |||
| e9f4f67eee | |||
| fac6409ec4 | |||
| e97a54ac8d | |||
| 14509aa7e4 |
@@ -7,6 +7,10 @@ DEVICE_ONLINE_STALE_SECONDS=30
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_PUBLIC_ORIGIN=
|
||||
MINIO_PUBLIC_ENDPOINT=
|
||||
MINIO_PUBLIC_PORT=
|
||||
MINIO_PUBLIC_USE_SSL=
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_BUCKET=videos
|
||||
|
||||
@@ -41,6 +41,8 @@ Required env vars:
|
||||
| `MEDIA_RECORDINGS_DIR` | Local output directory for server-side recording workers (planned in SFU mode) |
|
||||
| `MEDIA_MAX_PUBLISHERS` / `MEDIA_MAX_SUBSCRIBERS_PER_ROOM` | Soft concurrency limits for single-server media mode (planned) |
|
||||
| `MINIO_*` | Connection settings for the MinIO/S3 endpoint |
|
||||
| `MINIO_PUBLIC_ORIGIN` | Optional browser-facing MinIO origin used for presigned URLs and CSP (for example `https://storage.example.com`) |
|
||||
| `MINIO_PUBLIC_ENDPOINT` / `MINIO_PUBLIC_PORT` / `MINIO_PUBLIC_USE_SSL` | Optional browser-facing MinIO host settings if you prefer host/port flags instead of `MINIO_PUBLIC_ORIGIN` |
|
||||
| `MINIO_CA_CERT_PATH` | Optional path to a PEM CA bundle used to trust a private/self-managed MinIO certificate |
|
||||
| `MINIO_TLS_REJECT_UNAUTHORIZED` | TLS verification toggle for MinIO HTTPS requests (`true` by default) |
|
||||
| `MINIO_INSECURE_SKIP_TLS_VERIFY` | Dev-only escape hatch to skip MinIO TLS certificate verification |
|
||||
@@ -56,6 +58,7 @@ bun run dev
|
||||
```
|
||||
|
||||
- Server boots after ensuring the configured MinIO bucket exists.
|
||||
- If the backend reaches MinIO on an internal host (for example `minio:9000`) but browsers must upload/download through a different public host, set `MINIO_PUBLIC_ORIGIN` so presigned URLs target the browser-reachable origin instead of the internal one.
|
||||
- If MinIO uses a private or incomplete certificate chain, prefer setting `MINIO_CA_CERT_PATH` to a trusted PEM bundle. Only use `MINIO_INSECURE_SKIP_TLS_VERIFY=true` for local development or temporary debugging.
|
||||
|
||||
## Database (Drizzle ORM)
|
||||
|
||||
@@ -21,7 +21,7 @@ import opsRoutes from './routes/ops';
|
||||
import { rateLimit } from './middleware/security';
|
||||
import { requestContext } from './middleware/observability';
|
||||
import { setupRealtimeGateway } from './realtime/gateway';
|
||||
import { ensureMinioBucket } from './utils/minio';
|
||||
import { ensureMinioBucket, minioPublicOrigin } from './utils/minio';
|
||||
import { startRecordingsWorker } from './workers/recordings';
|
||||
import { startPushWorker } from './services/push';
|
||||
|
||||
@@ -35,31 +35,8 @@ const corsMiddleware = cors({
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
const buildMinioConnectOrigin = (): string | null => {
|
||||
const endpoint = process.env.MINIO_ENDPOINT?.trim();
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
|
||||
try {
|
||||
return new URL(endpoint).origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const useSSL = (process.env.MINIO_USE_SSL ?? 'false').toLowerCase() === 'true';
|
||||
const port = Number(process.env.MINIO_PORT ?? (useSSL ? 443 : 80));
|
||||
const scheme = useSSL ? 'https' : 'http';
|
||||
const includePort = !(useSSL && port === 443) && !(!useSSL && port === 80);
|
||||
|
||||
return `${scheme}://${endpoint}${includePort ? `:${port}` : ''}`;
|
||||
};
|
||||
|
||||
const minioConnectOrigin = buildMinioConnectOrigin();
|
||||
const connectSrcDirectives = ["'self'", 'cdn.jsdelivr.net', ...(minioConnectOrigin ? [minioConnectOrigin] : [])];
|
||||
const mediaSrcDirectives = ["'self'", 'blob:', 'data:', ...(minioConnectOrigin ? [minioConnectOrigin] : [])];
|
||||
const connectSrcDirectives = ["'self'", 'cdn.jsdelivr.net', ...(minioPublicOrigin ? [minioPublicOrigin] : [])];
|
||||
const mediaSrcDirectives = ["'self'", 'blob:', 'data:', ...(minioPublicOrigin ? [minioPublicOrigin] : [])];
|
||||
|
||||
app.get('/', (_req, res) => {
|
||||
res.send('API is running');
|
||||
|
||||
@@ -2,7 +2,13 @@ import type { NextFunction, Request, Response } from 'express';
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ensureMinioBucket, minioBucket, minioClient, minioPresignedExpirySeconds } from '../utils/minio';
|
||||
import {
|
||||
ensureMinioBucket,
|
||||
minioBucket,
|
||||
minioClient,
|
||||
minioPresignClient,
|
||||
minioPresignedExpirySeconds,
|
||||
} from '../utils/minio';
|
||||
|
||||
const adminUsername = process.env.ADMIN_USERNAME;
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
@@ -325,7 +331,7 @@ router.post('/upload-url', async (req, res) => {
|
||||
await ensureMinioBucket();
|
||||
|
||||
const objectKey = buildObjectKey(parsed.data.fileName, parsed.data.prefix);
|
||||
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
|
||||
const uploadUrl = await minioPresignClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
|
||||
const expiresAt = new Date(Date.now() + minioPresignedExpirySeconds * 1000);
|
||||
|
||||
res.status(201).json({
|
||||
|
||||
@@ -6,7 +6,13 @@ import { db } from '../db/client';
|
||||
import { recordings } from '../db/schema';
|
||||
import { requireDeviceAuth } from '../middleware/device-auth';
|
||||
import { writeAuditLog } from '../services/audit';
|
||||
import { ensureMinioBucket, minioBucket, minioClient, minioPresignedExpirySeconds } from '../utils/minio';
|
||||
import {
|
||||
ensureMinioBucket,
|
||||
minioBucket,
|
||||
minioClient,
|
||||
minioPresignClient,
|
||||
minioPresignedExpirySeconds,
|
||||
} from '../utils/minio';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -227,7 +233,7 @@ router.get('/:recordingId/download-url', requireDeviceAuth, async (req, res) =>
|
||||
throw error;
|
||||
}
|
||||
|
||||
const downloadUrl = await minioClient.presignedGetObject(
|
||||
const downloadUrl = await minioPresignClient.presignedGetObject(
|
||||
recording.bucket,
|
||||
recording.objectKey,
|
||||
minioPresignedExpirySeconds,
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
ensureMinioBucket,
|
||||
minioBucket,
|
||||
minioClient,
|
||||
minioPresignClient,
|
||||
minioPresignedExpirySeconds,
|
||||
minioPublicOrigin,
|
||||
} from '../utils/minio';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,6 +28,10 @@ const downloadUrlSchema = z.object({
|
||||
objectKey: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
const uploadProxyParamsSchema = z.object({
|
||||
recordingId: z.string().uuid(),
|
||||
});
|
||||
|
||||
const listSchema = z.object({
|
||||
prefix: z.string().trim().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
@@ -42,6 +48,105 @@ const buildObjectKey = (userId: string, fileName: string, prefix?: string): stri
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.put('/upload/:recordingId', async (req, res) => {
|
||||
const parsedParams = uploadProxyParamsSchema.safeParse(req.params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
res.status(400).json({ message: 'Invalid recordingId', errors: parsedParams.error.flatten() });
|
||||
return;
|
||||
}
|
||||
|
||||
const authSession = req.auth;
|
||||
|
||||
if (!authSession?.user) {
|
||||
res.status(401).json({ message: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const recording = await db.query.recordings.findFirst({
|
||||
where: and(eq(recordings.id, parsedParams.data.recordingId), eq(recordings.ownerUserId, authSession.user.id)),
|
||||
});
|
||||
|
||||
if (!recording) {
|
||||
res.status(404).json({ message: 'Recording not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!recording.bucket || !recording.objectKey) {
|
||||
res.status(409).json({ message: 'Recording does not have a storage target yet' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (recording.status !== 'awaiting_upload') {
|
||||
res.status(409).json({ message: `Recording is not awaiting upload (current status: ${recording.status})` });
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = typeof req.headers['content-type'] === 'string' && req.headers['content-type'].trim()
|
||||
? req.headers['content-type'].trim()
|
||||
: 'application/octet-stream';
|
||||
const rawContentLength = Array.isArray(req.headers['content-length'])
|
||||
? req.headers['content-length'][0]
|
||||
: req.headers['content-length'];
|
||||
const parsedSize = rawContentLength ? Number(rawContentLength) : undefined;
|
||||
|
||||
if (parsedSize !== undefined && (!Number.isFinite(parsedSize) || parsedSize < 0)) {
|
||||
res.status(400).json({ message: 'Invalid Content-Length header' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureMinioBucket();
|
||||
|
||||
console.info('[recording.proxy-upload] streaming upload via backend', {
|
||||
ownerUserId: authSession.user.id,
|
||||
recordingId: recording.id,
|
||||
deviceId: recording.cameraDeviceId,
|
||||
bucket: recording.bucket,
|
||||
objectKey: recording.objectKey,
|
||||
contentType,
|
||||
sizeBytes: parsedSize ?? null,
|
||||
});
|
||||
|
||||
const uploadResult = await minioClient.putObject(
|
||||
recording.bucket,
|
||||
recording.objectKey,
|
||||
req,
|
||||
parsedSize,
|
||||
{ 'Content-Type': contentType },
|
||||
);
|
||||
|
||||
console.info('[recording.proxy-upload] upload complete', {
|
||||
ownerUserId: authSession.user.id,
|
||||
recordingId: recording.id,
|
||||
bucket: recording.bucket,
|
||||
objectKey: recording.objectKey,
|
||||
etag: uploadResult.etag,
|
||||
versionId: uploadResult.versionId ?? null,
|
||||
sizeBytes: parsedSize ?? null,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Recording uploaded via backend proxy',
|
||||
recordingId: recording.id,
|
||||
bucket: recording.bucket,
|
||||
objectKey: recording.objectKey,
|
||||
etag: uploadResult.etag,
|
||||
versionId: uploadResult.versionId ?? null,
|
||||
sizeBytes: parsedSize ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[recording.proxy-upload] failed', {
|
||||
ownerUserId: authSession.user.id,
|
||||
recordingId: recording.id,
|
||||
bucket: recording.bucket,
|
||||
objectKey: recording.objectKey,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/upload-url', async (req, res) => {
|
||||
const parsed = uploadUrlSchema.safeParse(req.body);
|
||||
|
||||
@@ -83,7 +188,7 @@ router.post('/upload-url', async (req, res) => {
|
||||
}
|
||||
|
||||
const objectKey = buildObjectKey(authSession.user.id, parsed.data.fileName, parsed.data.prefix);
|
||||
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
|
||||
const uploadUrl = await minioPresignClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000);
|
||||
|
||||
@@ -98,6 +203,7 @@ router.post('/upload-url', async (req, res) => {
|
||||
minioEndpoint: process.env.MINIO_ENDPOINT ?? 'localhost',
|
||||
minioPort: Number(process.env.MINIO_PORT ?? 9000),
|
||||
minioUseSSL: (process.env.MINIO_USE_SSL ?? 'false').toLowerCase() === 'true',
|
||||
minioPublicOrigin,
|
||||
});
|
||||
|
||||
let persistedRecording;
|
||||
@@ -183,7 +289,7 @@ router.get('/download-url', async (req, res) => {
|
||||
|
||||
await ensureMinioBucket();
|
||||
|
||||
const downloadUrl = await minioClient.presignedGetObject(
|
||||
const downloadUrl = await minioPresignClient.presignedGetObject(
|
||||
minioBucket,
|
||||
parsed.data.objectKey,
|
||||
minioPresignedExpirySeconds,
|
||||
|
||||
@@ -2,14 +2,90 @@ import { readFileSync } from 'node:fs';
|
||||
import { Agent as HttpsAgent } from 'node:https';
|
||||
import { Client } from 'minio';
|
||||
|
||||
const endpoint = process.env.MINIO_ENDPOINT ?? 'localhost';
|
||||
const port = Number(process.env.MINIO_PORT ?? 9000);
|
||||
const useSSL = (process.env.MINIO_USE_SSL ?? 'false').toLowerCase() === 'true';
|
||||
type MinioTarget = {
|
||||
endPoint: string;
|
||||
port: number;
|
||||
useSSL: boolean;
|
||||
origin: string;
|
||||
};
|
||||
|
||||
const parseBoolean = (value: string | undefined, fallback: boolean): boolean => {
|
||||
if (value == null || value.trim() === '') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return value.toLowerCase() === 'true';
|
||||
};
|
||||
|
||||
const resolveMinioTarget = ({
|
||||
origin,
|
||||
endpoint,
|
||||
port,
|
||||
useSSL,
|
||||
}: {
|
||||
origin?: string;
|
||||
endpoint?: string;
|
||||
port?: string | number;
|
||||
useSSL: boolean;
|
||||
}): MinioTarget => {
|
||||
const rawOrigin = origin?.trim();
|
||||
|
||||
if (rawOrigin) {
|
||||
const url = new URL(rawOrigin);
|
||||
const targetUseSSL = url.protocol === 'https:';
|
||||
const targetPort = Number(url.port || (targetUseSSL ? 443 : 80));
|
||||
|
||||
return {
|
||||
endPoint: url.hostname,
|
||||
port: targetPort,
|
||||
useSSL: targetUseSSL,
|
||||
origin: url.origin,
|
||||
};
|
||||
}
|
||||
|
||||
const rawEndpoint = endpoint?.trim() || 'localhost';
|
||||
|
||||
if (rawEndpoint.startsWith('http://') || rawEndpoint.startsWith('https://')) {
|
||||
const url = new URL(rawEndpoint);
|
||||
const targetUseSSL = url.protocol === 'https:';
|
||||
const targetPort = Number(url.port || (targetUseSSL ? 443 : 80));
|
||||
|
||||
return {
|
||||
endPoint: url.hostname,
|
||||
port: targetPort,
|
||||
useSSL: targetUseSSL,
|
||||
origin: url.origin,
|
||||
};
|
||||
}
|
||||
|
||||
const targetPort = Number(port ?? (useSSL ? 443 : 80));
|
||||
const includePort = !(useSSL && targetPort === 443) && !(!useSSL && targetPort === 80);
|
||||
|
||||
return {
|
||||
endPoint: rawEndpoint,
|
||||
port: targetPort,
|
||||
useSSL,
|
||||
origin: `${useSSL ? 'https' : 'http'}://${rawEndpoint}${includePort ? `:${targetPort}` : ''}`,
|
||||
};
|
||||
};
|
||||
|
||||
const accessKey = process.env.MINIO_ACCESS_KEY;
|
||||
const secretKey = process.env.MINIO_SECRET_KEY;
|
||||
const insecureSkipTlsVerify = (process.env.MINIO_INSECURE_SKIP_TLS_VERIFY ?? 'false').toLowerCase() === 'true';
|
||||
const tlsRejectUnauthorized = (process.env.MINIO_TLS_REJECT_UNAUTHORIZED ?? 'true').toLowerCase() !== 'false';
|
||||
const insecureSkipTlsVerify = parseBoolean(process.env.MINIO_INSECURE_SKIP_TLS_VERIFY, false);
|
||||
const tlsRejectUnauthorized = parseBoolean(process.env.MINIO_TLS_REJECT_UNAUTHORIZED, true);
|
||||
const minioCaCertPath = process.env.MINIO_CA_CERT_PATH?.trim();
|
||||
const internalUseSSL = parseBoolean(process.env.MINIO_USE_SSL, false);
|
||||
const internalTarget = resolveMinioTarget({
|
||||
endpoint: process.env.MINIO_ENDPOINT ?? 'localhost',
|
||||
port: process.env.MINIO_PORT ?? 9000,
|
||||
useSSL: internalUseSSL,
|
||||
});
|
||||
const publicTarget = resolveMinioTarget({
|
||||
origin: process.env.MINIO_PUBLIC_ORIGIN,
|
||||
endpoint: process.env.MINIO_PUBLIC_ENDPOINT ?? internalTarget.endPoint,
|
||||
port: process.env.MINIO_PUBLIC_PORT ?? internalTarget.port,
|
||||
useSSL: parseBoolean(process.env.MINIO_PUBLIC_USE_SSL, internalTarget.useSSL),
|
||||
});
|
||||
|
||||
if (!accessKey || !secretKey) {
|
||||
throw new Error('MINIO_ACCESS_KEY and MINIO_SECRET_KEY must be set');
|
||||
@@ -17,8 +93,9 @@ if (!accessKey || !secretKey) {
|
||||
|
||||
export const minioBucket = process.env.MINIO_BUCKET ?? 'videos';
|
||||
export const minioPresignedExpirySeconds = Number(process.env.MINIO_PRESIGNED_EXPIRY_SECONDS ?? 60 * 10);
|
||||
export const minioPublicOrigin = publicTarget.origin;
|
||||
const customCa = minioCaCertPath ? readFileSync(minioCaCertPath) : undefined;
|
||||
const transportAgent = useSSL
|
||||
const transportAgent = internalTarget.useSSL
|
||||
? new HttpsAgent({
|
||||
keepAlive: true,
|
||||
ca: customCa,
|
||||
@@ -27,14 +104,24 @@ const transportAgent = useSSL
|
||||
: undefined;
|
||||
|
||||
export const minioClient = new Client({
|
||||
endPoint: endpoint,
|
||||
port,
|
||||
useSSL,
|
||||
endPoint: internalTarget.endPoint,
|
||||
port: internalTarget.port,
|
||||
useSSL: internalTarget.useSSL,
|
||||
accessKey,
|
||||
secretKey,
|
||||
region: process.env.MINIO_REGION ?? 'us-east-1',
|
||||
transportAgent,
|
||||
});
|
||||
|
||||
export const minioPresignClient = new Client({
|
||||
endPoint: publicTarget.endPoint,
|
||||
port: publicTarget.port,
|
||||
useSSL: publicTarget.useSSL,
|
||||
accessKey,
|
||||
secretKey,
|
||||
region: process.env.MINIO_REGION ?? 'us-east-1',
|
||||
});
|
||||
|
||||
let ensureBucketPromise: Promise<void> | null = null;
|
||||
|
||||
export const ensureMinioBucket = async (): Promise<void> => {
|
||||
|
||||
213
README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# SecureCam Final Year Project
|
||||
|
||||
This repository contains the current SecureCam prototype:
|
||||
|
||||
- `Backend/` - Bun + Express API, Better Auth, Socket.IO, Drizzle ORM, MinIO integration
|
||||
- `WebApp/` - SvelteKit browser dashboard for camera and client flows
|
||||
- `docker-compose.yml` - local development stack for Postgres, MinIO, TURN, backend, and web app
|
||||
|
||||
The easiest way to run the project is with Docker Compose. Manual setup instructions are included below if you want to run the backend and web app directly on your machine.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
### Start the full local stack
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
This starts:
|
||||
|
||||
- Postgres
|
||||
- MinIO
|
||||
- coturn
|
||||
- Backend on `http://localhost:3000`
|
||||
- Web app on `http://localhost:5173`
|
||||
|
||||
### Open the app
|
||||
|
||||
- Web app: `http://localhost:5173`
|
||||
- Backend API: `http://localhost:3000`
|
||||
- Swagger docs: `http://localhost:3000/docs`
|
||||
- OpenAPI JSON: `http://localhost:3000/openapi.json`
|
||||
- Admin dashboard: `http://localhost:3000/admin`
|
||||
- MinIO console: `http://localhost:9001`
|
||||
|
||||
Default admin dashboard credentials:
|
||||
|
||||
- Username: `admin`
|
||||
- Password: `strong-password`
|
||||
|
||||
Default MinIO credentials:
|
||||
|
||||
- Username: `minioadmin`
|
||||
- Password: `minioadmin`
|
||||
|
||||
### Stop the stack
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
To remove volumes as well:
|
||||
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## Manual Development Setup
|
||||
|
||||
Use this path if you want to run the backend and web app outside Docker.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh) for the backend
|
||||
- Node.js + npm for the web app
|
||||
- A running Postgres instance
|
||||
- A running MinIO-compatible object store
|
||||
- Optional: coturn if you want the local TURN configuration to work as-is
|
||||
|
||||
### 1. Configure the backend
|
||||
|
||||
```bash
|
||||
cd Backend
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Set the values in `Backend/.env` for your environment, especially:
|
||||
|
||||
- `DATABASE_URL`
|
||||
- `BETTER_AUTH_SECRET`
|
||||
- `BETTER_AUTH_BASE_URL`
|
||||
- `BETTER_AUTH_TRUSTED_ORIGINS`
|
||||
- `MINIO_ENDPOINT`
|
||||
- `MINIO_PORT`
|
||||
- `MINIO_USE_SSL`
|
||||
- `MINIO_ACCESS_KEY`
|
||||
- `MINIO_SECRET_KEY`
|
||||
- `MINIO_BUCKET`
|
||||
|
||||
If your backend connects to MinIO on an internal hostname but the browser must upload to a different public hostname, also set:
|
||||
|
||||
- `MINIO_PUBLIC_ORIGIN`
|
||||
|
||||
Example:
|
||||
|
||||
```env
|
||||
BETTER_AUTH_BASE_URL=http://localhost:3000
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:5173
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
```
|
||||
|
||||
### 2. Install backend dependencies
|
||||
|
||||
```bash
|
||||
cd Backend
|
||||
bun install
|
||||
```
|
||||
|
||||
### 3. Run backend migrations
|
||||
|
||||
```bash
|
||||
cd Backend
|
||||
bun run db:migrate
|
||||
```
|
||||
|
||||
### 4. Start the backend
|
||||
|
||||
```bash
|
||||
cd Backend
|
||||
bun run dev
|
||||
```
|
||||
|
||||
The backend will be available at `http://localhost:3000`.
|
||||
|
||||
### 5. Install web app dependencies
|
||||
|
||||
```bash
|
||||
cd WebApp
|
||||
npm install
|
||||
```
|
||||
|
||||
### 6. Start the web app
|
||||
|
||||
```bash
|
||||
cd WebApp
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The web app will be available at `http://localhost:5173`.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
docker compose logs -f backend webapp
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd Backend
|
||||
bun run dev
|
||||
bun test
|
||||
bun run db:migrate
|
||||
bun run db:studio
|
||||
```
|
||||
|
||||
### Web app
|
||||
|
||||
```bash
|
||||
cd WebApp
|
||||
npm run dev
|
||||
npm run build
|
||||
npm run check
|
||||
npm run test
|
||||
```
|
||||
|
||||
## First-Time Usage
|
||||
|
||||
Once the app is running:
|
||||
|
||||
1. Open `http://localhost:5173`
|
||||
2. Create an account or sign in
|
||||
3. Register the browser as either a `camera` or `client` device
|
||||
4. Open a second browser window or profile if you want to simulate both roles locally
|
||||
|
||||
The web app is the primary interface now. The old static simulator pages and mobile scaffold have been removed from the repo.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Browser upload fails with a MinIO URL that looks unreachable
|
||||
|
||||
If uploads point to an internal hostname such as `http://minio:9000`, set `MINIO_PUBLIC_ORIGIN` in the backend env to a browser-reachable MinIO host and restart the backend.
|
||||
|
||||
### Backend starts but MinIO checks fail
|
||||
|
||||
Make sure:
|
||||
|
||||
- the MinIO server is running
|
||||
- the bucket credentials are correct
|
||||
- the backend can reach the configured MinIO host
|
||||
- TLS settings such as `MINIO_CA_CERT_PATH` or `MINIO_INSECURE_SKIP_TLS_VERIFY` match your environment
|
||||
|
||||
### TURN issues on other devices
|
||||
|
||||
The default compose setup uses `127.0.0.1` in coturn-related configuration. If camera and client browsers are on different devices, replace those localhost TURN references with the host machine's LAN IP.
|
||||
|
||||
## More Detail
|
||||
|
||||
- Backend setup, env, and API details: [Backend/README.md](./Backend/README.md)
|
||||
- Web app source: [WebApp/](./WebApp)
|
||||
@@ -110,8 +110,8 @@
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
/* Custom brand colors and glassmorphism tokens */
|
||||
--color-premium: #2563eb;
|
||||
--color-premium-foreground: #ffffff;
|
||||
--color-premium: #ff7a59;
|
||||
--color-premium-foreground: #17110d;
|
||||
--color-glass-background: rgb(25 25 30 / 60%);
|
||||
--color-glass-border: rgb(255 255 255 / 8%);
|
||||
--color-glass-panel: rgb(15 15 20 / 70%);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</head>
|
||||
<body
|
||||
data-sveltekit-preload-data="hover"
|
||||
class="h-screen overflow-hidden flex bg-[#0a0a0c] text-gray-200"
|
||||
class="bg-[#0a0a0c] text-gray-200"
|
||||
style="background:#0a0a0c; color:#e5e7eb"
|
||||
>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
||||
@@ -13,6 +13,17 @@ const toBackendUrl = (path) => {
|
||||
return `${backendUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
};
|
||||
|
||||
const parseResponseBody = async (response) => {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
return response.json().catch(() => ({}));
|
||||
}
|
||||
|
||||
const text = await response.text().catch(() => '');
|
||||
return text ? { message: text } : {};
|
||||
};
|
||||
|
||||
const request = async (path, options = {}) => {
|
||||
const { deviceToken } = getAppState();
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
@@ -38,6 +49,36 @@ const request = async (path, options = {}) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const uploadBinary = async (path, body, options = {}) => {
|
||||
const { deviceToken } = getAppState();
|
||||
const headers = {};
|
||||
|
||||
if (deviceToken) {
|
||||
headers.Authorization = `Bearer ${deviceToken}`;
|
||||
}
|
||||
|
||||
if (options.contentType) {
|
||||
headers['Content-Type'] = options.contentType;
|
||||
}
|
||||
|
||||
const response = await fetch(toBackendUrl(path), {
|
||||
method: options.method || 'PUT',
|
||||
body,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...headers,
|
||||
...(options.headers || {})
|
||||
}
|
||||
});
|
||||
|
||||
const data = await parseResponseBody(response);
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || response.statusText || 'Request failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getBackendUrl = () => backendUrl;
|
||||
|
||||
export const api = {
|
||||
@@ -83,5 +124,9 @@ export const api = {
|
||||
},
|
||||
pushNotifications: {
|
||||
markRead: (notificationId) => request(`/push-notifications/${notificationId}/read`, { method: 'POST', body: JSON.stringify({}) })
|
||||
},
|
||||
uploads: {
|
||||
uploadRecordingBlob: (recordingId, blob, contentType = 'application/octet-stream') =>
|
||||
uploadBinary(`/videos/upload/${recordingId}`, blob, { method: 'PUT', contentType })
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,6 +36,8 @@ export const createControllerMediaModule = ({
|
||||
let cameraVideoElement = null;
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const uploadFailureHint =
|
||||
'Check MinIO bucket CORS and the public upload host/certificate if browser uploads are failing.';
|
||||
|
||||
const updateMotionDetectionRuntime = (updates) => {
|
||||
patchAppState((state) => ({
|
||||
@@ -148,6 +150,62 @@ export const createControllerMediaModule = ({
|
||||
return parts.join(' · ');
|
||||
};
|
||||
|
||||
const uploadRecordingBlobToStorage = async ({ uploadMeta, blob, streamSessionId = null, eventId = null }) => {
|
||||
const contentType = blob.type || 'video/webm';
|
||||
const recordingId = uploadMeta?.video?.id ?? null;
|
||||
|
||||
try {
|
||||
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': contentType },
|
||||
body: blob
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'direct',
|
||||
status: uploadResponse.status
|
||||
};
|
||||
} catch (directError) {
|
||||
const directErrorMessage =
|
||||
directError instanceof Error ? directError.message : String(directError);
|
||||
|
||||
console.warn('[recording.upload] direct upload failed, retrying via backend proxy', {
|
||||
recordingId,
|
||||
streamSessionId,
|
||||
eventId,
|
||||
objectKey: uploadMeta?.objectKey,
|
||||
error: directErrorMessage
|
||||
});
|
||||
addActivity(
|
||||
'Recording',
|
||||
`Direct upload failed for ${uploadMeta?.objectKey ?? 'recording'}, retrying via backend proxy`
|
||||
);
|
||||
|
||||
if (!recordingId) {
|
||||
throw directError;
|
||||
}
|
||||
|
||||
try {
|
||||
const proxyUpload = await api.uploads.uploadRecordingBlob(recordingId, blob, contentType);
|
||||
return {
|
||||
mode: 'proxy',
|
||||
status: 201,
|
||||
proxyUpload
|
||||
};
|
||||
} catch (proxyError) {
|
||||
const proxyErrorMessage =
|
||||
proxyError instanceof Error ? proxyError.message : String(proxyError);
|
||||
throw new Error(
|
||||
`Direct upload failed (${directErrorMessage}); backend proxy upload failed (${proxyErrorMessage})`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCameraInputDevices = async () => {
|
||||
if (!navigator.mediaDevices?.enumerateDevices) {
|
||||
setAppState({ cameraInputDevices: [], selectedCameraInputId: '' });
|
||||
@@ -566,23 +624,19 @@ export const createControllerMediaModule = ({
|
||||
'Recording',
|
||||
`Upload URL ready for ${uploadMeta.objectKey} via ${uploadOrigin}`
|
||||
);
|
||||
|
||||
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': compressedBlob.type || 'video/webm' },
|
||||
body: compressedBlob
|
||||
const uploadResult = await uploadRecordingBlobToStorage({
|
||||
uploadMeta,
|
||||
blob: compressedBlob,
|
||||
streamSessionId
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
|
||||
}
|
||||
|
||||
console.info('[recording.upload] object uploaded', {
|
||||
recordingId: recording.id,
|
||||
streamSessionId,
|
||||
objectKey: uploadMeta.objectKey,
|
||||
bucket: uploadMeta.bucket,
|
||||
status: uploadResponse.status,
|
||||
mode: uploadResult.mode,
|
||||
status: uploadResult.status,
|
||||
sizeBytes: compressedBlob.size
|
||||
});
|
||||
addActivity('Recording', `Upload completed for ${uploadMeta.objectKey}`);
|
||||
@@ -604,7 +658,7 @@ export const createControllerMediaModule = ({
|
||||
addActivity('Recording', 'Recording uploaded and finalized');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[recording.upload] stream upload failed, falling back to simulated key', {
|
||||
console.error('[recording.upload] stream upload failed before object reached storage', {
|
||||
recordingId: recording.id,
|
||||
streamSessionId,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
@@ -613,14 +667,9 @@ export const createControllerMediaModule = ({
|
||||
'Recording',
|
||||
`Upload failed for stream ${streamSessionId}: ${error instanceof Error ? error.message : 'unknown error'}`
|
||||
);
|
||||
const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`;
|
||||
await api.events.finalizeRecording(recording.id, {
|
||||
objectKey: fallbackObjectKey,
|
||||
durationSeconds: captureResult?.durationSeconds ?? 6,
|
||||
sizeBytes: captureResult?.blob?.size ?? 5000000
|
||||
});
|
||||
addActivity('Recording', 'Upload failed; finalized with simulator fallback');
|
||||
return true;
|
||||
addActivity('Recording', uploadFailureHint);
|
||||
pushToast('Recording upload failed before reaching storage.', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,23 +721,19 @@ export const createControllerMediaModule = ({
|
||||
blobType: compressedBlob.type || 'video/webm'
|
||||
});
|
||||
addActivity('Recording', `Standalone upload URL ready via ${uploadOrigin}`);
|
||||
|
||||
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': compressedBlob.type || 'video/webm' },
|
||||
body: compressedBlob
|
||||
const uploadResult = await uploadRecordingBlobToStorage({
|
||||
uploadMeta,
|
||||
blob: compressedBlob,
|
||||
eventId: lastMotionEventId
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
|
||||
}
|
||||
|
||||
console.info('[recording.upload] standalone object uploaded', {
|
||||
eventId: lastMotionEventId,
|
||||
recordingId: uploadMeta.video?.id,
|
||||
objectKey: uploadMeta.objectKey,
|
||||
bucket: uploadMeta.bucket,
|
||||
status: uploadResponse.status,
|
||||
mode: uploadResult.mode,
|
||||
status: uploadResult.status,
|
||||
sizeBytes: compressedBlob.size
|
||||
});
|
||||
addActivity('Recording', `Standalone upload completed for ${uploadMeta.objectKey}`);
|
||||
@@ -718,6 +763,8 @@ export const createControllerMediaModule = ({
|
||||
'Recording',
|
||||
`Standalone motion upload failed: ${error instanceof Error ? error.message : 'unknown error'}`
|
||||
);
|
||||
addActivity('Recording', uploadFailureHint);
|
||||
pushToast('Motion clip upload failed before reaching storage.', 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// @ts-nocheck
|
||||
|
||||
const PAGE_PATHS = {
|
||||
auth: '/',
|
||||
onboarding: '/onboarding',
|
||||
camera: '/camera',
|
||||
client: '/client',
|
||||
activity: '/activity',
|
||||
settings: '/settings'
|
||||
auth: '/auth/login',
|
||||
onboarding: '/app/onboarding',
|
||||
camera: '/app/camera',
|
||||
client: '/app/client',
|
||||
activity: '/app/activity',
|
||||
settings: '/app/settings'
|
||||
};
|
||||
|
||||
const DEVICE_STORAGE_KEY = 'mobileSimDevice';
|
||||
@@ -66,15 +66,21 @@ const normalizePath = (path) => path.replace(/\/+$/, '') || '/';
|
||||
|
||||
export const pageFromPath = (path) => {
|
||||
switch (normalizePath(path)) {
|
||||
case '/onboarding':
|
||||
case '/auth':
|
||||
case '/auth/login':
|
||||
case '/auth/signup':
|
||||
return 'auth';
|
||||
case '/app':
|
||||
return 'app';
|
||||
case '/app/onboarding':
|
||||
return 'onboarding';
|
||||
case '/camera':
|
||||
case '/app/camera':
|
||||
return 'camera';
|
||||
case '/client':
|
||||
case '/app/client':
|
||||
return 'client';
|
||||
case '/activity':
|
||||
case '/app/activity':
|
||||
return 'activity';
|
||||
case '/settings':
|
||||
case '/app/settings':
|
||||
return 'settings';
|
||||
default:
|
||||
return 'auth';
|
||||
|
||||
@@ -55,6 +55,9 @@ let initPromise = null;
|
||||
let socket = null;
|
||||
let pollInterval = null;
|
||||
let socketHeartbeatInterval = null;
|
||||
const handleBeforeUnload = () => {
|
||||
void cleanupConnectionState();
|
||||
};
|
||||
|
||||
let clientVideoElement = null;
|
||||
|
||||
@@ -839,7 +842,13 @@ const invalidateSavedDevice = async (message, options = {}) => {
|
||||
const enforceRouteForSession = () => {
|
||||
const state = getAppState();
|
||||
const page = pageFromPath(window.location.pathname);
|
||||
setAppState({ page });
|
||||
const initialPage =
|
||||
page === 'app'
|
||||
? state.deviceToken
|
||||
? getHomePageKeyForRole(state.device?.role)
|
||||
: 'onboarding'
|
||||
: page;
|
||||
setAppState({ page: initialPage });
|
||||
|
||||
if (!state.session) {
|
||||
if (page !== 'auth') {
|
||||
@@ -856,7 +865,7 @@ const enforceRouteForSession = () => {
|
||||
}
|
||||
|
||||
const expectedHome = getHomePageKeyForRole(state.device?.role);
|
||||
if ((page === 'auth' || page === 'onboarding') && expectedHome) {
|
||||
if ((page === 'auth' || page === 'onboarding' || page === 'app') && expectedHome) {
|
||||
navigateToScreen('home', { replace: true, role: state.device?.role });
|
||||
return;
|
||||
}
|
||||
@@ -903,9 +912,7 @@ const init = async () => {
|
||||
void refreshCameraInputDevices();
|
||||
applyMotionDetectionReadiness();
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
void cleanupConnectionState();
|
||||
});
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
initialized = true;
|
||||
})().finally(() => {
|
||||
@@ -923,6 +930,7 @@ const destroy = async () => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
}
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
initialized = false;
|
||||
await cleanupConnectionState();
|
||||
};
|
||||
@@ -955,6 +963,10 @@ const actions = {
|
||||
patchAppState((state) => ({ isRegistering: !state.isRegistering }));
|
||||
},
|
||||
|
||||
setAuthMode(isRegistering) {
|
||||
setAppState({ isRegistering });
|
||||
},
|
||||
|
||||
async submitAuth() {
|
||||
const state = getAppState();
|
||||
const { email, password, name } = state.authForm;
|
||||
|
||||
@@ -1 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<title>PhoneCam</title>
|
||||
<rect width="512" height="512" rx="120" fill="#0A0A0C" />
|
||||
<rect x="28" y="28" width="456" height="456" rx="96" fill="#111827" stroke="#2563EB" stroke-width="8" />
|
||||
<text
|
||||
x="256"
|
||||
y="230"
|
||||
fill="#F8FAFC"
|
||||
font-family="DejaVu Sans, Arial, sans-serif"
|
||||
font-size="88"
|
||||
font-weight="700"
|
||||
text-anchor="middle"
|
||||
>
|
||||
Phone
|
||||
</text>
|
||||
<text
|
||||
x="256"
|
||||
y="330"
|
||||
fill="#60A5FA"
|
||||
font-family="DejaVu Sans, Arial, sans-serif"
|
||||
font-size="88"
|
||||
font-weight="700"
|
||||
text-anchor="middle"
|
||||
>
|
||||
Cam
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 666 B |
110
WebApp/src/lib/auth/AuthPanel.svelte
Normal file
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { appController } from '$lib/app/controller';
|
||||
import { appState } from '$lib/app/store';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
|
||||
let { mode = 'login' } = $props<{ mode?: 'login' | 'signup' }>();
|
||||
|
||||
const isSignup = () => mode === 'signup';
|
||||
const title = () => (isSignup() ? 'Create your PhoneCam account' : 'Sign in to PhoneCam');
|
||||
const description = () =>
|
||||
isSignup()
|
||||
? 'Create an account to finish setting up this browser.'
|
||||
: 'Use your account to open the dashboard and continue setup.';
|
||||
const primaryLabel = () => (isSignup() ? 'Create account' : 'Sign in');
|
||||
const secondaryLabel = () => (isSignup() ? 'Sign in instead' : 'Create an account');
|
||||
const secondaryPrompt = () => (isSignup() ? 'Already have an account?' : 'Need an account?');
|
||||
const secondaryHref = () => (isSignup() ? '/auth/login' : '/auth/signup');
|
||||
|
||||
$effect(() => {
|
||||
appController.setAuthMode(isSignup());
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="mx-auto flex min-h-full w-full max-w-5xl flex-col px-4 py-8 sm:px-6">
|
||||
<nav class="flex items-center border-b border-white/10 pb-4">
|
||||
<Button
|
||||
href="/"
|
||||
variant="ghost"
|
||||
class="h-auto rounded-full px-3 text-sm font-semibold text-white hover:bg-white/5 hover:text-sky-200"
|
||||
>
|
||||
PhoneCam
|
||||
</Button>
|
||||
</nav>
|
||||
|
||||
<div class="flex flex-1 items-center justify-center pt-8">
|
||||
<Card variant="glass" class="w-full max-w-md rounded-3xl border-white/10 bg-black/45 backdrop-blur-xl">
|
||||
<CardHeader class="space-y-3 px-6 pt-6 text-left sm:px-7 sm:pt-7">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-sky-200/80">PhoneCam</p>
|
||||
<div class="space-y-2">
|
||||
<CardTitle class="text-3xl font-semibold tracking-tight text-white">{title()}</CardTitle>
|
||||
<CardDescription class="text-sm leading-6 text-slate-400">{description()}</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-5 px-6 pb-6 sm:px-7 sm:pb-7">
|
||||
{#if isSignup()}
|
||||
<div id="authNameField" class="space-y-2">
|
||||
<Label for="authName" class="text-sm font-medium text-slate-200">Display name</Label>
|
||||
<Input
|
||||
id="authName"
|
||||
type="text"
|
||||
placeholder="Jane Doe"
|
||||
class="h-11 rounded-2xl border-white/10 bg-white/[0.04] text-sm text-white placeholder:text-slate-500"
|
||||
value={$appState.authForm.name}
|
||||
oninput={(event) => appController.setAuthField('name', (event.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="authEmail" class="text-sm font-medium text-slate-200">Email</Label>
|
||||
<Input
|
||||
id="authEmail"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
class="h-11 rounded-2xl border-white/10 bg-white/[0.04] text-sm text-white placeholder:text-slate-500"
|
||||
value={$appState.authForm.email}
|
||||
oninput={(event) => appController.setAuthField('email', (event.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="authPassword" class="text-sm font-medium text-slate-200">Password</Label>
|
||||
<Input
|
||||
id="authPassword"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
class="h-11 rounded-2xl border-white/10 bg-white/[0.04] text-sm text-white placeholder:text-slate-500"
|
||||
value={$appState.authForm.password}
|
||||
oninput={(event) => appController.setAuthField('password', (event.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
id="signInBtn"
|
||||
variant="premium"
|
||||
class="h-11 w-full rounded-2xl text-sm font-semibold"
|
||||
onclick={() => appController.submitAuth()}
|
||||
>
|
||||
{primaryLabel()}
|
||||
</Button>
|
||||
|
||||
<div class="border-t border-white/10 pt-4 text-center text-sm text-slate-400">
|
||||
<span>{secondaryPrompt()}</span>
|
||||
<Button
|
||||
href={secondaryHref()}
|
||||
variant="ghost"
|
||||
class="ml-1 h-auto rounded-full px-2 text-sm font-medium text-white hover:bg-transparent hover:text-sky-200"
|
||||
>
|
||||
{secondaryLabel()}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
19
WebApp/src/routes/(shell)/+layout.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { onMount } from 'svelte';
|
||||
import { appController } from '$lib/app/controller';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
void appController.init();
|
||||
|
||||
return () => {
|
||||
void appController.destroy();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen overflow-hidden bg-[#0a0a0c] text-gray-200">
|
||||
{@render children()}
|
||||
</div>
|
||||
34
WebApp/src/routes/(shell)/app/+page.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { appController } from '$lib/app/controller';
|
||||
import { appState } from '$lib/app/store';
|
||||
|
||||
let redirected = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (redirected || $appState.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$appState.session) {
|
||||
appController.navigate('auth');
|
||||
redirected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$appState.deviceToken) {
|
||||
appController.navigate('onboarding');
|
||||
redirected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
appController.navigate('home');
|
||||
redirected = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="flex flex-1 items-center justify-center px-6">
|
||||
<div class="rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm text-slate-300">
|
||||
Preparing your workspace…
|
||||
</div>
|
||||
</section>
|
||||
5
WebApp/src/routes/(shell)/auth/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
throw redirect(307, '/auth/login');
|
||||
}
|
||||
9
WebApp/src/routes/(shell)/auth/login/+page.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import AuthPanel from '$lib/auth/AuthPanel.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="auth">
|
||||
<AuthPanel mode="login" />
|
||||
</AppChrome>
|
||||
9
WebApp/src/routes/(shell)/auth/signup/+page.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import AuthPanel from '$lib/auth/AuthPanel.svelte';
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="auth">
|
||||
<AuthPanel mode="signup" />
|
||||
</AppChrome>
|
||||
@@ -3,7 +3,6 @@
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { appController } from '$lib/app/controller';
|
||||
import {
|
||||
clearInstallPrompt,
|
||||
setInstallPrompt,
|
||||
@@ -85,14 +84,11 @@
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
void appController.init();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
window.removeEventListener('appinstalled', handleAppInstalled);
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
void appController.destroy();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,90 +1,342 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
|
||||
import { appController } from '$lib/app/controller';
|
||||
import { appState } from '$lib/app/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { ShieldCheck } from '@lucide/svelte';
|
||||
import { ArrowRight } from '@lucide/svelte';
|
||||
|
||||
let shaderCanvas: HTMLCanvasElement | null = null;
|
||||
|
||||
const heroPoints = ['Camera station', 'Client viewer', 'Motion events'];
|
||||
|
||||
const vertexShaderSource = `
|
||||
attribute vec2 a_position;
|
||||
void main() {
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShaderSource = `
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_time;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
float a = hash(i);
|
||||
float b = hash(i + vec2(1.0, 0.0));
|
||||
float c = hash(i + vec2(0.0, 1.0));
|
||||
float d = hash(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
float fbm(vec2 p) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
value += amplitude * noise(p);
|
||||
p = p * 2.0 + vec2(17.1, 9.2);
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
mat2 rot(float angle) {
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return mat2(c, -s, s, c);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y;
|
||||
float t = u_time * 0.18;
|
||||
|
||||
vec2 q = p * 1.45;
|
||||
q *= rot(-0.25);
|
||||
|
||||
float fieldA = fbm(q * 2.1 + vec2(t, -t * 0.35));
|
||||
float fieldB = fbm(q * 3.0 - vec2(t * 0.7, -t));
|
||||
|
||||
float ribbonA = smoothstep(
|
||||
0.62,
|
||||
0.96,
|
||||
1.0 - abs(p.y + 0.20 * sin(p.x * 2.7 + t * 1.8) + (fieldA - 0.5) * 0.30)
|
||||
);
|
||||
float ribbonB = smoothstep(
|
||||
0.58,
|
||||
0.95,
|
||||
1.0 - abs(p.y - 0.26 * cos(p.x * 1.9 - t * 1.2) + (fieldB - 0.5) * 0.22)
|
||||
);
|
||||
|
||||
float glow = exp(-3.4 * length(p * vec2(0.8, 1.15)));
|
||||
float grain = noise(gl_FragCoord.xy * 0.35 + t * 8.0) * 0.04;
|
||||
|
||||
vec3 base = vec3(0.055, 0.05, 0.07);
|
||||
vec3 underpaint = vec3(0.14, 0.09, 0.08);
|
||||
vec3 accentA = vec3(1.0, 0.48, 0.35);
|
||||
vec3 accentB = vec3(0.99, 0.67, 0.46);
|
||||
|
||||
vec3 color = base;
|
||||
color += underpaint * (fieldA * 0.38 + fieldB * 0.18);
|
||||
color += accentA * ribbonA * 0.52;
|
||||
color += accentB * ribbonB * 0.34;
|
||||
color += accentA * glow * 0.12;
|
||||
color += grain;
|
||||
|
||||
float vignette = smoothstep(1.35, 0.18, length(p));
|
||||
color *= vignette;
|
||||
color += vec3(0.012, 0.012, 0.018);
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const createShader = (gl: WebGLRenderingContext, type: number, source: string) => {
|
||||
const shader = gl.createShader(type);
|
||||
if (!shader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error(gl.getShaderInfoLog(shader));
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
};
|
||||
|
||||
const createProgram = (gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string) => {
|
||||
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
|
||||
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
||||
|
||||
if (!vertexShader || !fragmentShader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const program = gl.createProgram();
|
||||
if (!program) {
|
||||
gl.deleteShader(vertexShader);
|
||||
gl.deleteShader(fragmentShader);
|
||||
return null;
|
||||
}
|
||||
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error(gl.getProgramInfoLog(program));
|
||||
gl.deleteProgram(program);
|
||||
gl.deleteShader(vertexShader);
|
||||
gl.deleteShader(fragmentShader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return { program, vertexShader, fragmentShader };
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (!shaderCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gl = shaderCanvas.getContext('webgl', {
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
premultipliedAlpha: false
|
||||
});
|
||||
|
||||
if (!gl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const programBundle = createProgram(gl, vertexShaderSource, fragmentShaderSource);
|
||||
if (!programBundle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { program, vertexShader, fragmentShader } = programBundle;
|
||||
const positionLocation = gl.getAttribLocation(program, 'a_position');
|
||||
const timeLocation = gl.getUniformLocation(program, 'u_time');
|
||||
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
|
||||
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW
|
||||
);
|
||||
|
||||
let frameId = 0;
|
||||
let lastWidth = 0;
|
||||
let lastHeight = 0;
|
||||
|
||||
const resize = () => {
|
||||
if (!shaderCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
const width = Math.floor(shaderCanvas.clientWidth * dpr);
|
||||
const height = Math.floor(shaderCanvas.clientHeight * dpr);
|
||||
|
||||
if (width === lastWidth && height === lastHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastWidth = width;
|
||||
lastHeight = height;
|
||||
shaderCanvas.width = width;
|
||||
shaderCanvas.height = height;
|
||||
gl.viewport(0, 0, width, height);
|
||||
};
|
||||
|
||||
const render = (time: number) => {
|
||||
resize();
|
||||
|
||||
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.useProgram(program);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
gl.enableVertexAttribArray(positionLocation);
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.uniform1f(timeLocation, time * 0.001);
|
||||
gl.uniform2f(resolutionLocation, shaderCanvas?.width || 0, shaderCanvas?.height || 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
frameId = window.requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
frameId = window.requestAnimationFrame(render);
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
window.removeEventListener('resize', resize);
|
||||
gl.deleteBuffer(positionBuffer);
|
||||
gl.deleteProgram(program);
|
||||
gl.deleteShader(vertexShader);
|
||||
gl.deleteShader(fragmentShader);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<AppChrome pageKey="auth">
|
||||
<section id="screen-auth" class="flex flex-col items-center justify-center min-h-[70vh] animate-fade-in max-w-sm mx-auto">
|
||||
<Card variant="glass" class="w-full rounded-3xl">
|
||||
<CardHeader class="items-center gap-4 px-6 pt-6 text-center">
|
||||
<div
|
||||
class="flex size-20 items-center justify-center rounded-3xl bg-gradient-to-tr from-blue-600 to-indigo-600 shadow-lg shadow-blue-900/20"
|
||||
<svelte:head>
|
||||
<title>PhoneCam | Browser-Based Security Intelligence</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="PhoneCam turns any browser into a practical security console with live monitoring, motion detection, and recording playback."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="relative min-h-screen overflow-hidden bg-[#0a0910] text-[#f6efe7]"
|
||||
style="font-family: var(--landing-sans);"
|
||||
>
|
||||
<ShieldCheck class="size-10 text-white" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<CardTitle class="text-3xl font-bold tracking-tight text-white">PhoneCam Web</CardTitle>
|
||||
<CardDescription class="text-sm text-gray-400">Sign in to manage visual security from your browser.</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-4 px-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label class="sr-only" for="authEmail">Email</Label>
|
||||
<Input
|
||||
id="authEmail"
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
class="h-12 rounded-xl border-white/10 bg-black/40 text-sm text-white placeholder:text-gray-500"
|
||||
value={$appState.authForm.email}
|
||||
oninput={(event) => appController.setAuthField('email', (event.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label class="sr-only" for="authPassword">Password</Label>
|
||||
<Input
|
||||
id="authPassword"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
class="h-12 rounded-xl border-white/10 bg-black/40 text-sm text-white placeholder:text-gray-500"
|
||||
value={$appState.authForm.password}
|
||||
oninput={(event) => appController.setAuthField('password', (event.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
{#if $appState.isRegistering}
|
||||
<div id="authNameField" class="flex flex-col gap-2">
|
||||
<Label class="sr-only" for="authName">Name</Label>
|
||||
<Input
|
||||
id="authName"
|
||||
type="text"
|
||||
placeholder="Your Name"
|
||||
class="h-12 rounded-xl border-white/10 bg-black/40 text-sm text-white placeholder:text-gray-500"
|
||||
value={$appState.authForm.name}
|
||||
oninput={(event) => appController.setAuthField('name', (event.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
<CardFooter class="flex flex-col items-stretch gap-4 border-0 bg-transparent px-6 pb-6 pt-2">
|
||||
<Button
|
||||
id="signInBtn"
|
||||
variant="premium"
|
||||
class="h-12 w-full rounded-xl text-base shadow-lg shadow-blue-900/20"
|
||||
onclick={() => appController.submitAuth()}
|
||||
<canvas bind:this={shaderCanvas} class="absolute inset-0 h-full w-full"></canvas>
|
||||
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(10,9,16,0.24),transparent_28%),linear-gradient(180deg,rgba(10,9,16,0.45),rgba(10,9,16,0.78))]"></div>
|
||||
<div class="absolute inset-0 bg-[linear-gradient(90deg,rgba(10,9,16,0.88)_0%,rgba(10,9,16,0.62)_42%,rgba(10,9,16,0.28)_100%)]"></div>
|
||||
|
||||
<div class="relative z-10 flex min-h-screen flex-col">
|
||||
<header class="px-6 py-5 sm:px-8 lg:px-10">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between">
|
||||
<a
|
||||
href="/"
|
||||
class="text-xl font-semibold tracking-tight text-[#f6efe7]"
|
||||
style="font-family: var(--landing-display);"
|
||||
>
|
||||
{$appState.isRegistering ? 'Create Account' : 'Sign In'}
|
||||
</Button>
|
||||
<div class="flex w-full items-center gap-3 text-xs text-gray-600">
|
||||
<Separator class="flex-1 bg-white/10" />
|
||||
<span>OR</span>
|
||||
<Separator class="flex-1 bg-white/10" />
|
||||
</div>
|
||||
PhoneCam
|
||||
</a>
|
||||
<nav class="flex items-center gap-2">
|
||||
<Button
|
||||
id="toggleAuthModeBtn"
|
||||
href="/auth/login"
|
||||
variant="ghost"
|
||||
class="h-12 w-full rounded-xl border border-white/5 text-gray-400 hover:bg-white/5 hover:text-white"
|
||||
onclick={() => appController.toggleAuthMode()}
|
||||
class="rounded-full px-4 text-sm text-[#d7c9bd] hover:bg-white/5 hover:text-[#f6efe7]"
|
||||
>
|
||||
{$appState.isRegistering ? 'I already have an account' : 'Create an account'}
|
||||
Sign in
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Button
|
||||
href="/auth/signup"
|
||||
variant="premium"
|
||||
class="rounded-full px-5 text-sm font-semibold"
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mx-auto flex w-full max-w-6xl flex-1 items-center px-6 pb-12 pt-8 sm:px-8 lg:px-10">
|
||||
<div in:fly={{ y: 28, duration: 500 }} class="max-w-3xl space-y-8">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.32em] text-[#ff7a59]">
|
||||
Live browser monitoring
|
||||
</p>
|
||||
<div class="space-y-5">
|
||||
<p
|
||||
class="text-[clamp(3.2rem,10vw,7rem)] leading-none tracking-[-0.07em] text-[#f6efe7]"
|
||||
style="font-family: var(--landing-display);"
|
||||
>
|
||||
PhoneCam
|
||||
</p>
|
||||
<h1
|
||||
class="max-w-2xl text-4xl font-semibold leading-tight tracking-[-0.05em] text-[#f6efe7] sm:text-5xl lg:text-6xl"
|
||||
style="font-family: var(--landing-display);"
|
||||
>
|
||||
Monitoring that feels immediate.
|
||||
</h1>
|
||||
<p class="max-w-2xl text-base leading-7 text-[#d7c9bd] sm:text-lg">
|
||||
Use one browser as the camera station, another as the viewer, and keep feeds,
|
||||
recordings, and activity in one place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<Button href="/auth/signup" variant="premium" class="h-12 rounded-full px-6 text-sm font-semibold">
|
||||
Create account
|
||||
<ArrowRight class="ml-2 size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
href="/app"
|
||||
variant="ghost"
|
||||
class="h-12 rounded-full border border-white/12 bg-white/[0.03] px-6 text-sm font-medium text-[#f6efe7] hover:bg-white/[0.08]"
|
||||
>
|
||||
Open dashboard
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 pt-2 text-sm text-[#b7a696]">
|
||||
{#each heroPoints as item, index}
|
||||
<span>{item}</span>
|
||||
{#if index < heroPoints.length - 1}
|
||||
<span class="text-[#ff7a59]">•</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</AppChrome>
|
||||
|
||||
<style>
|
||||
:global(:root) {
|
||||
--landing-display: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
|
||||
--landing-sans: 'Inter Variable', 'Inter', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
background-color: #0a0910;
|
||||
}
|
||||
</style>
|
||||
|
||||
5
WebApp/src/routes/activity/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
throw redirect(307, '/app/activity');
|
||||
}
|
||||
5
WebApp/src/routes/camera/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
throw redirect(307, '/app/camera');
|
||||
}
|
||||
5
WebApp/src/routes/client/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
throw redirect(307, '/app/client');
|
||||
}
|
||||
5
WebApp/src/routes/onboarding/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
throw redirect(307, '/app/onboarding');
|
||||
}
|
||||
5
WebApp/src/routes/settings/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
throw redirect(307, '/app/settings');
|
||||
}
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,16 +1,27 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>PhoneCam</title>
|
||||
<rect width="512" height="512" rx="120" fill="#0A0A0C" />
|
||||
<rect x="84" y="84" width="344" height="344" rx="92" fill="url(#bg)"/>
|
||||
<rect x="116" y="132" width="280" height="248" rx="48" fill="#15161D" stroke="rgba(255,255,255,0.08)" stroke-width="8"/>
|
||||
<rect x="146" y="164" width="220" height="160" rx="28" fill="#0E1016"/>
|
||||
<circle cx="256" cy="244" r="52" fill="#2563EB"/>
|
||||
<circle cx="256" cy="244" r="28" fill="#0A0A0C"/>
|
||||
<path d="M186 122C186 108.745 196.745 98 210 98H302C315.255 98 326 108.745 326 122V144H186V122Z" fill="#2563EB"/>
|
||||
<path d="M220 290L256 326L322 214" stroke="white" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="84" y1="84" x2="428" y2="428" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1D4ED8"/>
|
||||
<stop offset="1" stop-color="#1E40AF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="28" y="28" width="456" height="456" rx="96" fill="#111827" stroke="#2563EB" stroke-width="8" />
|
||||
<text
|
||||
x="256"
|
||||
y="230"
|
||||
fill="#F8FAFC"
|
||||
font-family="DejaVu Sans, Arial, sans-serif"
|
||||
font-size="88"
|
||||
font-weight="700"
|
||||
text-anchor="middle"
|
||||
>
|
||||
Phone
|
||||
</text>
|
||||
<text
|
||||
x="256"
|
||||
y="330"
|
||||
fill="#60A5FA"
|
||||
font-family="DejaVu Sans, Arial, sans-serif"
|
||||
font-size="88"
|
||||
font-weight="700"
|
||||
text-anchor="middle"
|
||||
>
|
||||
Cam
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 991 B After Width: | Height: | Size: 678 B |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,32 +0,0 @@
|
||||
# Foreground Web Motion Detection Absolute Checklist
|
||||
|
||||
- `MWM-001` Confirm first-release scope is web camera role only and foreground-tab only.
|
||||
- `MWM-002` Add detector-related state fields to [WebApp/src/lib/app/store.js](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/lib/app/store.js:4).
|
||||
- `MWM-003` Define default motion detector config values for low-power foreground operation.
|
||||
- `MWM-004` Create a dedicated detector module under `WebApp/src/lib/app/`.
|
||||
- `MWM-005` Implement low-resolution frame sampling from the existing local camera stream.
|
||||
- `MWM-006` Implement grayscale conversion and normalized frame-difference scoring.
|
||||
- `MWM-007` Implement score smoothing or block aggregation to reduce noise flicker.
|
||||
- `MWM-008` Implement detector state machine transitions `idle`, `warming_up`, `monitoring`, `triggered`, `cooldown`.
|
||||
- `MWM-009` Implement adaptive sampling so idle sampling is slower than candidate-motion sampling.
|
||||
- `MWM-010` Ensure the detector starts only when the device role is `camera`, preview is ready, and detection is armed.
|
||||
- `MWM-011` Ensure the detector pauses or stops on camera permission loss, preview teardown, socket disconnect, or page hide.
|
||||
- `MWM-012` Add camera dashboard controls for arm/disarm and sensitivity.
|
||||
- `MWM-013` Add camera dashboard status UI for detector state and live motion score.
|
||||
- `MWM-014` Keep existing manual motion buttons intact and usable.
|
||||
- `MWM-015` Wire automatic detector trigger to the existing `startMotion` flow with `triggeredBy: "auto_motion"`.
|
||||
- `MWM-016` Prevent duplicate `startMotion` calls while a motion event is already active.
|
||||
- `MWM-017` Implement minimum event duration and quiet cooldown before automatic end.
|
||||
- `MWM-018` Wire automatic quiet-state completion to the existing `endMotion` flow.
|
||||
- `MWM-019` Add activity-log messages for detector armed/disarmed, trigger start, trigger end, and detector pause reasons.
|
||||
- `MWM-020` Add local persistence for detector settings so operator choices survive reloads.
|
||||
- `MWM-021` Validate that existing client notifications still fire through the unchanged backend event routes.
|
||||
- `MWM-022` Validate that existing recording behavior still works for auto-started motion events.
|
||||
- `MWM-023` Add unit-level tests for detector scoring and state-machine transitions.
|
||||
- `MWM-024` Add integration tests or deterministic mocks for event deduplication and cooldown handling.
|
||||
- `MWM-025` Perform manual runtime validation for no-motion idle scene, real person entry, lighting flicker, and camera shake.
|
||||
- `MWM-026` Measure runtime CPU/thermal behavior during a long foreground session and tune defaults if needed.
|
||||
- `MWM-027` Document the foreground-only assumption and known limits in project docs.
|
||||
- `MWM-028` Document recommended operator settings for lowest battery and heat.
|
||||
- `MWM-029` Add rollback behavior so disabling detection immediately stops the detector loop and leaves manual controls intact.
|
||||
- `MWM-030` Finalize a rollout note describing first-release non-goals such as background detection and ML classification.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Foreground Web Motion Detection Execution Prompt
|
||||
|
||||
Implement automatic foreground-only motion detection for the web camera role using the artifacts below as the source of truth:
|
||||
|
||||
- [FOREGROUND_WEB_MOTION_DETECTION_IMPLEMENTATION_PLAN.md](/home/matiss/Documents/Final%20Year%20Project/docs/FOREGROUND_WEB_MOTION_DETECTION_IMPLEMENTATION_PLAN.md)
|
||||
- [FOREGROUND_WEB_MOTION_DETECTION_ABSOLUTE_CHECKLIST.md](/home/matiss/Documents/Final%20Year%20Project/docs/FOREGROUND_WEB_MOTION_DETECTION_ABSOLUTE_CHECKLIST.md)
|
||||
- [FOREGROUND_WEB_MOTION_DETECTION_VALIDATION_MATRIX.md](/home/matiss/Documents/Final%20Year%20Project/docs/FOREGROUND_WEB_MOTION_DETECTION_VALIDATION_MATRIX.md)
|
||||
|
||||
## Delivery Rules
|
||||
|
||||
- Treat the implementation plan as authoritative for architecture and sequencing.
|
||||
- Complete checklist items atomically and update the validation matrix with evidence as work lands.
|
||||
- Do not silently skip hidden work such as store updates, detector lifecycle cleanup, tests, docs, and runtime validation.
|
||||
- Reuse the existing backend motion lifecycle before introducing any new backend motion pipeline.
|
||||
- Preserve manual motion controls and existing client notification behavior.
|
||||
|
||||
## Evidence Rules
|
||||
|
||||
- Every completed checklist item must have at least one evidence entry in the validation matrix.
|
||||
- `Code` evidence must reference the file changed.
|
||||
- `Test` evidence must reference a deterministic automated or manual validation step.
|
||||
- `Runtime` evidence must reference an observed app behavior, command result, or manual scenario outcome.
|
||||
|
||||
## Completion Gates
|
||||
|
||||
- Automatic motion detection works only for the web `camera` role.
|
||||
- The detector runs only while the browser tab is in the foreground and the detector is armed.
|
||||
- The detector starts and ends existing motion events automatically without duplicate starts.
|
||||
- Client notifications and recordings continue to function through existing backend routes.
|
||||
- Manual motion controls still work.
|
||||
- Detector settings are user-visible and persisted locally.
|
||||
- Tests and runtime validation cover idle scenes, real motion, flicker, and camera shake.
|
||||
|
||||
## Suggested First Batch
|
||||
|
||||
1. Add store fields and camera dashboard controls.
|
||||
2. Add the detector module with live score reporting only.
|
||||
3. Tune thresholds manually on the camera dashboard.
|
||||
4. Wire automatic start/end to the existing motion event flow.
|
||||
5. Add tests, docs, and validation evidence.
|
||||
@@ -1,229 +0,0 @@
|
||||
# Foreground Web Motion Detection Implementation Plan
|
||||
|
||||
## Summary
|
||||
|
||||
Implement automatic motion detection for the `camera` role in the web app while the browser remains open in the foreground on a plugged-in phone. Reuse the existing backend motion event, notification, recording, and stream flows instead of introducing a new backend motion pipeline.
|
||||
|
||||
## Goals
|
||||
|
||||
- Detect motion automatically from the local camera preview in the web app.
|
||||
- Trigger the existing motion event lifecycle with minimal backend change.
|
||||
- Minimize battery and heat by using low-resolution frame analysis and adaptive sampling.
|
||||
- Keep the first release heuristic and deterministic rather than ML-based.
|
||||
- Make behavior observable and tunable from the camera dashboard.
|
||||
|
||||
## Constraints
|
||||
|
||||
- The browser tab is assumed to remain visible in the foreground.
|
||||
- The initial target is the web app camera dashboard, not the Expo mobile app.
|
||||
- Existing backend endpoints remain the source of truth for motion events.
|
||||
- The current camera role UX in [WebApp/src/routes/camera/+page.svelte](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/routes/camera/+page.svelte:31) must continue to work.
|
||||
- The current motion event routes in [Backend/routes/events.ts](/home/matiss/Documents/Final%20Year%20Project/Backend/routes/events.ts:35) and [Backend/routes/events.ts](/home/matiss/Documents/Final%20Year%20Project/Backend/routes/events.ts:142) should be reused.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Manual motion controls remain available as an operator override.
|
||||
- `triggeredBy` can distinguish manual versus automatic events without a schema change.
|
||||
- Persisted per-device motion settings are desirable, but an in-memory first pass is acceptable.
|
||||
- The existing recording behavior tied to stream/motion events remains the intended user experience.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- True background detection while the browser tab is hidden.
|
||||
- ML person/pet/vehicle classification in the first release.
|
||||
- Replacing the current notification or recording architecture.
|
||||
- Solving appliance-grade uptime guarantees.
|
||||
|
||||
## Existing Integration Points
|
||||
|
||||
- Camera preview and camera dashboard actions are already hosted in [WebApp/src/routes/camera/+page.svelte](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/routes/camera/+page.svelte:105).
|
||||
- Local camera capture and preview lifecycle already exist in [WebApp/src/lib/app/controller.js](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/lib/app/controller.js:233).
|
||||
- Manual motion start/end already call the backend in [WebApp/src/lib/app/controller.js](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/lib/app/controller.js:1374).
|
||||
- Camera-side state is stored in [WebApp/src/lib/app/store.js](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/lib/app/store.js:4).
|
||||
|
||||
## Strategy
|
||||
|
||||
### Phase 1: Add Motion Detector State And Controls
|
||||
|
||||
Add camera-side detector state to the Svelte store:
|
||||
|
||||
- `motionDetectionEnabled`
|
||||
- `motionDetectorStatus` such as `idle`, `warming_up`, `monitoring`, `triggered`, `cooldown`
|
||||
- `motionSensitivity`
|
||||
- `motionSampleIntervalMs`
|
||||
- `motionTriggerConsecutiveFrames`
|
||||
- `motionQuietCooldownMs`
|
||||
- `motionMinimumEventMs`
|
||||
- `motionScore`
|
||||
- `motionDebugEnabled`
|
||||
|
||||
Expose controls on the camera dashboard:
|
||||
|
||||
- Arm/disarm automatic detection
|
||||
- Sensitivity slider or preset selector
|
||||
- Low-power mode preset
|
||||
- Live debug score and current detector state
|
||||
|
||||
Keep the existing manual `Simulate Motion Event` and `Stop Recording` actions for fallback.
|
||||
|
||||
### Phase 2: Build A Lightweight Detector Engine
|
||||
|
||||
Create a dedicated detector module, for example `WebApp/src/lib/app/motion-detector.js`, owned by the camera dashboard flow.
|
||||
|
||||
Detector design:
|
||||
|
||||
- Read from the existing `localCameraStream`
|
||||
- Draw frames into an offscreen or hidden canvas
|
||||
- Downsample aggressively to about `160x90` or `192x108`
|
||||
- Convert to grayscale
|
||||
- Compare the current frame against the previous smoothed frame
|
||||
- Compute a normalized motion score such as changed-pixel ratio or block-delta score
|
||||
- Ignore tiny isolated noise with thresholding and optional block aggregation
|
||||
|
||||
Battery and heat controls:
|
||||
|
||||
- Default sampling at `1 fps`
|
||||
- Burst to `4-6 fps` only after suspicious motion begins
|
||||
- Return to low sampling after cooldown
|
||||
- Skip work if preview is not ready, detector is disarmed, or document visibility changes
|
||||
- Avoid full-resolution processing and avoid network uploads during detection itself
|
||||
|
||||
### Phase 3: Add Event State Machine And Backend Reuse
|
||||
|
||||
Implement a camera-side state machine:
|
||||
|
||||
- `idle` -> `monitoring`
|
||||
- `monitoring` -> `candidate_motion`
|
||||
- `candidate_motion` -> `triggered`
|
||||
- `triggered` -> `cooldown`
|
||||
- `cooldown` -> `monitoring`
|
||||
|
||||
Trigger rules:
|
||||
|
||||
- Require `N` consecutive high-motion frames before starting an event
|
||||
- Call the existing backend motion start endpoint once
|
||||
- Set `triggeredBy` to `auto_motion`
|
||||
- Hold the event open for at least `motionMinimumEventMs`
|
||||
- Only end after the score stays below threshold for `motionQuietCooldownMs`
|
||||
|
||||
This keeps backend changes minimal because the existing event lifecycle already fans out realtime alerts and push notifications.
|
||||
|
||||
### Phase 4: Make Recording Behavior Predictable
|
||||
|
||||
The detector should not record constantly.
|
||||
|
||||
Recommended behavior:
|
||||
|
||||
- Motion detection itself only analyzes low-res frames locally
|
||||
- When automatic motion is confirmed, call the existing start-motion flow
|
||||
- Continue using the current recording logic already associated with motion and streaming
|
||||
- End the motion event only after quiet cooldown, not on every instantaneous dip
|
||||
|
||||
This avoids repeated start/stop loops that waste device resources.
|
||||
|
||||
### Phase 5: Add Persistence For Operator Settings
|
||||
|
||||
Initial implementation can use local storage on the web app for speed.
|
||||
|
||||
Second step:
|
||||
|
||||
- Add motion settings persistence per camera device
|
||||
- Store settings either in `devices.metadata` if introduced later, or in a new `device_motion_settings` table
|
||||
- Load persisted settings on device registration or camera dashboard open
|
||||
|
||||
Suggested settings:
|
||||
|
||||
- Enabled/armed
|
||||
- Sensitivity
|
||||
- Sample interval
|
||||
- Quiet cooldown
|
||||
- Minimum event duration
|
||||
- Optional region of interest
|
||||
|
||||
### Phase 6: Observability And Debugging
|
||||
|
||||
Add operator-visible debug surfaces:
|
||||
|
||||
- Current motion score
|
||||
- Detector state
|
||||
- Last trigger time
|
||||
- Count of suppressed candidate triggers
|
||||
|
||||
Add activity log entries for:
|
||||
|
||||
- Detector armed/disarmed
|
||||
- Detector warmed up
|
||||
- Automatic motion started
|
||||
- Automatic motion ended
|
||||
- Detector paused because preview or socket is unavailable
|
||||
|
||||
### Phase 7: Test And Tune
|
||||
|
||||
Testing should cover:
|
||||
|
||||
- Low-motion idle scenes
|
||||
- Moderate lighting flicker
|
||||
- Real person entry into frame
|
||||
- Camera shake false positives
|
||||
- Reconnection behavior
|
||||
- Event deduplication
|
||||
|
||||
Tuning targets:
|
||||
|
||||
- Low false positive rate in static indoor scenes
|
||||
- Trigger latency below about `2 seconds`
|
||||
- CPU usage low enough to avoid obvious thermal throttling during foreground operation
|
||||
|
||||
## File-Level Change Plan
|
||||
|
||||
Primary files:
|
||||
|
||||
- [WebApp/src/lib/app/store.js](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/lib/app/store.js:4)
|
||||
- [WebApp/src/lib/app/controller.js](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/lib/app/controller.js:233)
|
||||
- [WebApp/src/routes/camera/+page.svelte](/home/matiss/Documents/Final%20Year%20Project/WebApp/src/routes/camera/+page.svelte:105)
|
||||
|
||||
Likely new files:
|
||||
|
||||
- `WebApp/src/lib/app/motion-detector.js`
|
||||
- `WebApp/src/lib/app/motion-detector.test.js` or equivalent
|
||||
|
||||
Optional later backend files:
|
||||
|
||||
- [Backend/db/schema.ts](/home/matiss/Documents/Final%20Year%20Project/Backend/db/schema.ts:14)
|
||||
- [Backend/routes/devices.ts](/home/matiss/Documents/Final%20Year%20Project/Backend/routes/devices.ts:41)
|
||||
|
||||
## Risk Controls
|
||||
|
||||
- Use hysteresis so one threshold starts motion and a lower threshold ends it.
|
||||
- Require consecutive-frame confirmation before starting events.
|
||||
- Pause detection when preview, permission, or socket connectivity is unavailable.
|
||||
- Keep all frame processing local and low resolution.
|
||||
- Keep manual controls available during rollout.
|
||||
- Ship with debug mode so threshold tuning is possible without code changes.
|
||||
|
||||
## Recommended Operator Settings
|
||||
|
||||
- Start with the `Balanced` profile on a plugged-in phone.
|
||||
- Use `Low Power` if the phone runs warm or the scene is mostly static.
|
||||
- Keep the browser tab visible and the camera dashboard open while detection is armed.
|
||||
- Leave debug mode off during normal operation and enable it only while tuning thresholds.
|
||||
- Prefer a stable camera mount and a consistent indoor lighting setup to reduce false positives.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A camera-role web device can arm automatic motion detection from the camera dashboard.
|
||||
- When visible motion enters frame, the web app starts one backend motion event without duplicate starts.
|
||||
- Linked clients receive the same notifications they currently receive for manual motion events.
|
||||
- Motion events remain open through continuous motion and close only after quiet cooldown.
|
||||
- The detector does not continuously upload frames or full video for analysis.
|
||||
- Manual motion controls continue to work.
|
||||
- Detector state survives normal page usage and fails safely on disconnect or permission loss.
|
||||
|
||||
## Recommended Delivery Order
|
||||
|
||||
1. Add store state and camera dashboard controls.
|
||||
2. Add local detector engine with score reporting only, no event triggering.
|
||||
3. Tune thresholds against manual test scenes.
|
||||
4. Wire score transitions to automatic event start/end.
|
||||
5. Add persistence for detector settings.
|
||||
6. Add tests, docs, and rollout notes.
|
||||
@@ -1,34 +0,0 @@
|
||||
# Foreground Web Motion Detection Validation Matrix
|
||||
|
||||
| Row ID | Checklist Item | Status | Code | Test | Runtime |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `VM-MWM-001` | Confirm first-release scope is web camera role only and foreground-tab only | done | `WebApp/src/lib/app/controller.js` `shouldRunMotionDetector`; `docs/FOREGROUND_WEB_MOTION_DETECTION_IMPLEMENTATION_PLAN.md` Constraints/Non-Goals | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-002` | Add detector-related state fields to store | done | `WebApp/src/lib/app/store.js` `createInitialState`; `WebApp/src/lib/app/controller.js` `getDefaultMotionDetectionState` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-003` | Define default detector config values | done | `WebApp/src/lib/app/controller.js` `MOTION_DETECTION_PROFILES`, `buildMotionDetectionState` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-004` | Create dedicated detector module | done | `WebApp/src/lib/app/motion-detector.js` `createMotionDetector` | `bun run test -- src/lib/app/motion-detector.spec.js` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-005` | Implement low-resolution frame sampling | done | `WebApp/src/lib/app/motion-detector.js` `DEFAULT_FRAME_WIDTH`, `DEFAULT_FRAME_HEIGHT`, `analyzeFrame` | `bun run test -- src/lib/app/motion-detector.spec.js` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-006` | Implement grayscale conversion and frame-difference scoring | done | `WebApp/src/lib/app/motion-detector.js` `computeLuma`, `computeChangedPixelRatio` | `src/lib/app/motion-detector.spec.js` `computes grayscale luminance...`, `reports the ratio...` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-007` | Implement score smoothing or block aggregation | done | `WebApp/src/lib/app/motion-detector.js` `DEFAULT_SMOOTHING_FACTOR`, `analyzeFrame` | `bun run test -- src/lib/app/motion-detector.spec.js` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-008` | Implement detector state machine transitions | done | `WebApp/src/lib/app/motion-detector.js` `createMotionMachineSnapshot`, `applyMotionStateMachine` | `src/lib/app/motion-detector.spec.js` `requires consecutive high-motion frames...`, `holds motion until minimum duration...` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-009` | Implement adaptive sampling | done | `WebApp/src/lib/app/motion-detector.js` `tick`; `WebApp/src/lib/app/controller.js` `MOTION_DETECTION_PROFILES` | `bun run test -- src/lib/app/motion-detector.spec.js` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-010` | Start detector only when camera role, preview, and armed state are valid | done | `WebApp/src/lib/app/controller.js` `shouldRunMotionDetector`, `applyMotionDetectionReadiness` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-011` | Pause detector on permission loss, preview teardown, disconnect, or page hide | done | `WebApp/src/lib/app/controller.js` `getMotionDetectionPauseReason`, `applyMotionDetectionReadiness`, `onVisibilityChange` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-012` | Add camera dashboard arm/disarm and sensitivity controls | done | `WebApp/src/routes/camera/+page.svelte` Automatic Detection controls; `WebApp/src/lib/app/controller.js` `setMotionDetectionEnabled`, `setMotionDetectionProfile` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-013` | Add camera dashboard detector status and score UI | done | `WebApp/src/routes/camera/+page.svelte` Detector State/Motion Score cards | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-014` | Preserve manual motion controls | done | `WebApp/src/routes/camera/+page.svelte` manual buttons; `WebApp/src/lib/app/controller.js` `actions.startMotion`, `actions.endMotion` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-015` | Trigger existing start-motion flow with `auto_motion` | done | `WebApp/src/lib/app/controller.js` `getMotionStartPayload`, `startMotionEvent`, `syncAutoMotionLifecycle`; `WebApp/src/lib/app/api.js` `events.startMotion` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-016` | Prevent duplicate automatic motion starts | done | `WebApp/src/lib/app/controller.js` `startMotionEvent`, `syncAutoMotionLifecycle`, `autoMotionTransitionInFlight` | `src/lib/app/motion-detector.spec.js` `requires consecutive high-motion frames...` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-017` | Implement minimum event duration and quiet cooldown | done | `WebApp/src/lib/app/motion-detector.js` `applyMotionStateMachine`; `WebApp/src/lib/app/controller.js` `syncAutoMotionLifecycle` | `src/lib/app/motion-detector.spec.js` `holds motion until minimum duration...` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-018` | Wire quiet-state completion to existing end-motion flow | done | `WebApp/src/lib/app/controller.js` `endMotionEvent`, `syncAutoMotionLifecycle`, `applyMotionDetectionReadiness` | `src/lib/app/motion-detector.spec.js` `holds motion until minimum duration...` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-019` | Add activity-log messages for detector lifecycle | done | `WebApp/src/lib/app/controller.js` `addActivity` calls in `applyMotionDetectionReadiness`, `setMotionDetectionEnabled`, `startMotionEvent`, `endMotionEvent` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-020` | Add local persistence for detector settings | done | `WebApp/src/lib/app/controller.js` `loadMotionDetectionSettings`, `persistMotionDetectionSettings`, `updateMotionDetectionState` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-021` | Validate existing client notifications still work | pending | Existing backend fan-out remains unchanged in `Backend/routes/events.ts` | | Runtime validation still required with linked client devices |
|
||||
| `VM-MWM-022` | Validate recording behavior still works for auto-started events | pending | Existing recording paths remain in `WebApp/src/lib/app/controller.js` `startLocalRecording`, `endMotionEvent`, `uploadStandaloneMotionRecording` | | Runtime validation still required with an actual auto-triggered event |
|
||||
| `VM-MWM-023` | Add unit tests for scoring and state-machine transitions | done | `WebApp/src/lib/app/motion-detector.js` exported helpers; `WebApp/src/lib/app/motion-detector.spec.js` | `bun run test -- src/lib/app/motion-detector.spec.js` | `bun run test -- src/lib/app/motion-detector.spec.js` -> `4 passed` |
|
||||
| `VM-MWM-024` | Add integration tests or deterministic mocks for cooldown and dedupe | in_progress | `WebApp/src/lib/app/motion-detector.spec.js` covers deterministic cooldown and trigger gating; controller dedupe still untested | `bun run test -- src/lib/app/motion-detector.spec.js` | Additional controller-level mock coverage still needed |
|
||||
| `VM-MWM-025` | Perform manual validation for idle, real motion, flicker, and camera shake | pending | | | |
|
||||
| `VM-MWM-026` | Measure runtime CPU and thermal behavior during long foreground session | pending | | | |
|
||||
| `VM-MWM-027` | Document foreground-only assumption and limits | done | `docs/FOREGROUND_WEB_MOTION_DETECTION_IMPLEMENTATION_PLAN.md` Constraints/Non-Goals/Acceptance Criteria | | Doc updated in repo |
|
||||
| `VM-MWM-028` | Document recommended low-battery and low-heat settings | done | `docs/FOREGROUND_WEB_MOTION_DETECTION_IMPLEMENTATION_PLAN.md` Battery and heat controls; Recommended Operator Settings | | Doc updated in repo |
|
||||
| `VM-MWM-029` | Add immediate rollback behavior when detection is disabled | done | `WebApp/src/lib/app/controller.js` `setMotionDetectionEnabled`, `applyMotionDetectionReadiness`, `endMotionEvent` | `bun run check` | `bun run check` -> `svelte-check found 0 errors and 0 warnings` |
|
||||
| `VM-MWM-030` | Publish rollout note for first-release non-goals | done | `docs/FOREGROUND_WEB_MOTION_DETECTION_IMPLEMENTATION_PLAN.md` Non-Goals | | Doc updated in repo |
|
||||
@@ -1,75 +0,0 @@
|
||||
# Absolute Checklist: Streaming Simplification (WebRTC-only)
|
||||
|
||||
## Backend
|
||||
|
||||
1. Add/confirm `SIMPLE_STREAMING` environment/config switch (default: true for new deployment profile).
|
||||
2. In `Backend/routes/streams.ts`, update `POST /streams/request` to:
|
||||
- keep device role/link validation
|
||||
- create session with status `requested`
|
||||
- emit request event directly compatible with WebRTC-only flow
|
||||
- remove stream-provider payload assumptions from response
|
||||
3. In `Backend/routes/streams.ts`, update `POST /streams/:id/accept` to:
|
||||
- validate camera ownership and state
|
||||
- set status to `streaming`
|
||||
- set `startedAt`
|
||||
- emit `stream:started`
|
||||
- remove or gate all publish/subscribe/media-provider dependency
|
||||
4. In `Backend/routes/streams.ts`, update `POST /streams/:id/end` to:
|
||||
- require authorized participant (camera or requester)
|
||||
- set status to `ended`
|
||||
- set `endedAt`
|
||||
- emit `stream:ended`
|
||||
- stop any active SFU session only if explicitly still active and configured
|
||||
5. In `Backend/routes/streams.ts`, remove any hard dependency on `mediaProvider.createSession` for live functionality.
|
||||
6. Gate recording creation in stream end logic behind feature flag (default OFF) or remove endpoint path from phase-1 path.
|
||||
7. In `Backend/routes/streams.ts`, return minimal deterministic payload for stream start/accept/end responses.
|
||||
8. In `Backend/db/schema.ts`, mark legacy media fields as deprecated comments only (no schema migration required in phase 1).
|
||||
9. In `Backend/media/service.ts`, make provider calls no-op for phase-1 streaming runtime or guard them behind non-simplified feature flag.
|
||||
10. In `Backend/media/providers/mock.ts`, annotate mock provider methods as metadata-only and remove calls from request/accept/end flow.
|
||||
11. In `Backend/media/sfu/service.ts` and `Backend/media/sfu/noop.ts`, ensure no required startup path is executed for default simplified mode.
|
||||
12. In `Backend/routes/commands.ts`, document command-table deprecation for stream lifecycle command path; remove active usage if simplified mode is enabled.
|
||||
|
||||
## Realtime Gateway
|
||||
|
||||
13. In `Backend/realtime/gateway.ts`, remove `stream:frame` event listener and sender path.
|
||||
14. In `Backend/realtime/gateway.ts`, remove frame-relay participant cache and related validation branch.
|
||||
15. In `Backend/realtime/gateway.ts`, keep only validated `webrtc:signal` logic for offer/answer/candidate/hangup.
|
||||
16. In `Backend/realtime/gateway.ts`, simplify signal forwarding so target validation is session ownership + same user context.
|
||||
17. In `Backend/realtime/gateway.ts`, remove periodic command retry polling for stream commands when in simplified mode.
|
||||
18. Keep `stream:started` and `stream:ended` emit paths with recipient resolution.
|
||||
19. Ensure socket middleware still enforces device token auth and role identity checks.
|
||||
|
||||
## Web frontend
|
||||
|
||||
20. In `WebApp/src/lib/app/controller.js`, collapse stream state to `idle|connecting|active|ended`.
|
||||
21. Remove fallback image mode, latest-frame state, and frame receiver logic from controller.
|
||||
22. Ensure client side only uses WebRTC peer connection and `<video>` rendering path.
|
||||
23. Simplify `ontrack` handling to set remote stream once and ignore duplicate tracks.
|
||||
24. Add deterministic peer teardown on `stream:ended`, page unload, and navigation changes.
|
||||
25. In `WebApp/src/routes/camera/+page.svelte`, remove preview-to-frame interval fallback sender.
|
||||
26. In `WebApp/src/routes/camera/+page.svelte`, ensure camera-side stream path emits offer only and no JPEG payload.
|
||||
27. In `WebApp/src/routes/camera/+page.svelte`, remove recording branch that depends on frame transport.
|
||||
28. In `WebApp/src/routes/client/+page.svelte`, remove fallback image DOM branch and related status labels.
|
||||
|
||||
## Mobile frontend
|
||||
|
||||
29. In `MobileApp/src/app-context.tsx`, remove base64 frame stream state and timer lifecycle for frame relay.
|
||||
30. In `MobileApp/src/app-context.tsx` and `MobileApp/app/(tabs)/index.tsx`, align stream lifecycle state transitions with web logic.
|
||||
31. In `MobileApp/app/(tabs)/index.tsx`, remove capture+`stream:frame` emission loop.
|
||||
32. In `MobileApp/app/(tabs)/index.tsx`, remove any image fallback UI path and render strategy for active stream.
|
||||
33. In `MobileApp/src/api.ts`, remove calls to publish/subscribe credentials endpoints from simplified runtime flow.
|
||||
34. Ensure mobile camera cleanup stops local media and closes RTCPeerConnection on end and unmount.
|
||||
|
||||
## Cross-cutting API/docs/config
|
||||
|
||||
35. In `Backend/docs/openapi.ts`, update documented streaming contract to only include:
|
||||
- request
|
||||
- accept
|
||||
- end
|
||||
- webrtc signal event behavior
|
||||
- remove references implying frame fallback as primary path
|
||||
36. Update any internal docs or README sections describing mock media endpoints as runtime-critical.
|
||||
37. Add migration notes to avoid breaking integrations (deprecation and rollback behavior).
|
||||
38. Add explicit log markers for stream request/accept/offer/answer/end events with session id and participant ids.
|
||||
39. Add compile/build compatibility checks for both apps after removing unused symbols/imports.
|
||||
40. Add runtime rollback checklist for disabling `SIMPLE_STREAMING` and re-enabling legacy paths.
|
||||
@@ -1,73 +0,0 @@
|
||||
# Execution Prompt: Streaming Simplification
|
||||
|
||||
You are implementing `STREAMING_SIMPLIFICATION` in `/home/matiss/Documents/Final Year Project`.
|
||||
|
||||
## Objective
|
||||
Deliver a WebRTC-only streaming architecture for this codebase by implementing the four artifacts:
|
||||
1. `docs/STREAMING_SIMPLIFICATION_IMPLEMENTATION_PLAN.md`
|
||||
2. `docs/STREAMING_SIMPLIFICATION_ABSOLUTE_CHECKLIST.md`
|
||||
3. `docs/STREAMING_SIMPLIFICATION_VALIDATION_MATRIX.md`
|
||||
4. This execution prompt.
|
||||
|
||||
## Governing constraints
|
||||
- Preserve device token auth and role checks.
|
||||
- Keep request/accept/end endpoints and WebRTC signaling functional.
|
||||
- Remove socket JPEG frame fallback from runtime path.
|
||||
- Keep rollback-capable behavior using `SIMPLE_STREAMING` where possible.
|
||||
- Do not reintroduce any media-path dependency on mock provider endpoints.
|
||||
|
||||
## Execution sequence (strict)
|
||||
|
||||
1. Implement all backend stream endpoint and session-state simplifications first.
|
||||
2. Simplify realtime gateway signaling path second.
|
||||
3. Update web frontend streaming controller and pages.
|
||||
4. Update mobile frontend streaming behavior.
|
||||
5. Update docs, config notes, and OpenAPI/event description.
|
||||
6. Run validation matrix gating; do not mark complete until all required evidence rows are filled.
|
||||
|
||||
## Completion gates
|
||||
|
||||
### Gate A — Backend
|
||||
- `Backend/routes/streams.ts` follows simplified lifecycle only.
|
||||
- `Backend/realtime/gateway.ts` contains no frame-relay path in active flow.
|
||||
- Unauthorized access checks remain strict and verifiable.
|
||||
|
||||
### Gate B — Frontends
|
||||
- Web and Mobile each have one stream state machine and one active rendering strategy.
|
||||
- No scheduled frame-relay sender/receiver code remains in runtime path.
|
||||
- End-of-stream cleanup is deterministic and symmetric.
|
||||
|
||||
### Gate C — Delivery readiness
|
||||
- OpenAPI/docs updated for simplified contract.
|
||||
- Feature flag/rollback documented.
|
||||
- Validation matrix entries populated from observed evidence.
|
||||
|
||||
## Evidence rules
|
||||
|
||||
For every checklist line in `docs/STREAMING_SIMPLIFICATION_ABSOLUTE_CHECKLIST.md`:
|
||||
1. Fill corresponding row in `docs/STREAMING_SIMPLIFICATION_VALIDATION_MATRIX.md` from pending -> done.
|
||||
2. Populate columns:
|
||||
- **Code Evidence**: file path + symbols/line references
|
||||
- **Test Evidence**: command output or test report artifact names
|
||||
- **Runtime Evidence**: screenshot/video/log excerpt references and scenario details
|
||||
3. Keep one evidence item per row if possible; if blocked, include blocker and retry plan.
|
||||
|
||||
## Required validation commands (when execution resumes)
|
||||
|
||||
- Backend: validate API/types/contracts compile/build and lint status.
|
||||
- Gateway: static check that `stream:frame` handlers are absent in effective runtime mode.
|
||||
- Frontend: build check for both web and mobile after removing unused imports/state.
|
||||
- Runtime smoke checks:
|
||||
- client request -> camera accept -> stream start -> stream end
|
||||
- unauthorized request rejection
|
||||
- cleanup path after end and reconnect
|
||||
|
||||
## Final report format
|
||||
|
||||
Return a final report with:
|
||||
1. Completed tasks grouped by file.
|
||||
2. Validation matrix fully updated.
|
||||
3. Any deviations from this spec and why.
|
||||
4. Exact first and second risk identified after rollout.
|
||||
|
||||
Do not introduce additional assumptions not covered in the checklist without updating the checklist and matrix.
|
||||
@@ -1,138 +0,0 @@
|
||||
# Implementation Plan: Streaming Simplification (WebRTC-only)
|
||||
|
||||
## 0) Goal and scope
|
||||
|
||||
### Goal
|
||||
Simplify the current streaming architecture to a single production-pragmatic implementation path:
|
||||
- Control plane via REST + socket signaling
|
||||
- Media plane via WebRTC only
|
||||
- No JPEG frame fallback relay
|
||||
- Preserve security and device-role authorization
|
||||
|
||||
### Scope in/out
|
||||
- In scope:
|
||||
- Stream request/accept/end lifecycle
|
||||
- Device role and ownership checks
|
||||
- WebRTC signaling and connection setup/teardown
|
||||
- Frontend Web + Mobile stream runtime paths
|
||||
- API/docs updates for simplified contract
|
||||
- Out of scope:
|
||||
- Multi-party broadcast or conferencing
|
||||
- SFU/ingest pipeline replacement in this phase
|
||||
- Media provider feature parity with mock provider URLs
|
||||
- Full recording workflow and upload/finalization
|
||||
|
||||
## 1) Current-state constraints to keep
|
||||
|
||||
- Devices use bearer tokens and role-based socket/session authentication.
|
||||
- Stream request is initiated by a `client` device targeting a linked `camera`.
|
||||
- Current schema/event names exist and are used across components.
|
||||
- Backend and frontends already support `stream:requested`, `stream:started`, `stream:ended`, and `webrtc:signal`.
|
||||
|
||||
## 2) Explicit requirements
|
||||
|
||||
1. Runtime complexity reduction:
|
||||
- Keep one media transport: WebRTC.
|
||||
- Remove socket JPEG fallback flow from runtime.
|
||||
2. Reliability and safety:
|
||||
- Keep strict owner/role checks for all stream/session endpoints.
|
||||
- Ensure deterministic teardown when stream ends.
|
||||
3. Maintainability:
|
||||
- Remove dead or non-critical branches.
|
||||
- Minimize special-case flags and dual state machines.
|
||||
4. Backward compatibility strategy:
|
||||
- Prefer a feature flag `SIMPLE_STREAMING` for controlled rollout.
|
||||
|
||||
## 3) Assumptions
|
||||
|
||||
- Existing database schema can remain (deprecating fields is acceptable for now).
|
||||
- Camera and client are expected to be online for primary WebRTC path; if not reachable, stream request/accept will fail gracefully.
|
||||
- `stream:frame` is not required for core user value in phase 1.
|
||||
- Recording features can be delayed or gated without blocking stream acceptance.
|
||||
- Existing command dispatch infrastructure can be simplified or replaced by direct request/session events without redesigning auth.
|
||||
|
||||
## 4) Non-goals and excluded behaviors
|
||||
|
||||
- No immediate migration to true production-grade media server or SFU.
|
||||
- No auto-retry queue for offline command delivery.
|
||||
- No new codec/transcoding pipeline.
|
||||
|
||||
## 5) Target architecture
|
||||
|
||||
### Backend
|
||||
- Stream endpoints: request -> accept -> end.
|
||||
- Session state transitions:
|
||||
- `requested` -> `streaming` -> `ended`
|
||||
- Commands table may be retained only for audit/logging or removed later.
|
||||
- Gateway relays:
|
||||
- `webrtc:signal` only for media negotiation.
|
||||
- `stream:started` and `stream:ended` for lifecycle updates.
|
||||
|
||||
### Frontend (Web + Mobile)
|
||||
- Shared runtime model:
|
||||
- `idle` -> `connecting` -> `active` -> `ended`
|
||||
- On camera accept: start WebRTC offer.
|
||||
- On client started event: start WebRTC answer/handling.
|
||||
- On ended: stop peer connections, clear state.
|
||||
- No frame image fallback state/UI.
|
||||
|
||||
## 6) Phased implementation strategy
|
||||
|
||||
### Phase 1: Contract freeze and safety gate (Day 0)
|
||||
1. Lock simplified state contract and event matrix.
|
||||
2. Add/validate `SIMPLE_STREAMING` flag and default behavior.
|
||||
3. Add migration notes for legacy paths.
|
||||
|
||||
### Phase 2: Backend control-plane simplification (Day 1)
|
||||
1. Refactor stream request/accept/end handlers to only manage session lifecycle and signaling.
|
||||
2. Remove/disable stream-provider-specific runtime side effects from these handlers.
|
||||
3. Ensure response payloads are minimal and stable.
|
||||
|
||||
### Phase 3: Gateway simplification (Day 1)
|
||||
1. Remove `stream:frame` event handler and related relay state.
|
||||
2. Keep only validated WebRTC signaling and lifecycle event emitters.
|
||||
3. Remove command retry loop if not required by flag.
|
||||
|
||||
### Phase 4: Web frontend simplification (Day 2)
|
||||
1. Simplify stream controller into single mode.
|
||||
2. Remove image fallback flow and related timers/state.
|
||||
3. Keep single video render path and deterministic cleanup.
|
||||
|
||||
### Phase 5: Mobile frontend simplification (Day 2)
|
||||
1. Remove periodic frame capture/relay code and image fallback rendering.
|
||||
2. Keep camera capture + WebRTC publish path.
|
||||
3. Align end/cleanup behavior with web.
|
||||
|
||||
### Phase 6: API/doc/ops cleanup (Day 3)
|
||||
1. Update OpenAPI/event docs and internal docs.
|
||||
2. Deprecate references to frame-based streaming and mock endpoints in code comments.
|
||||
3. Add rollout and rollback runbook.
|
||||
|
||||
### Phase 7: Validation and rollout (Day 4)
|
||||
1. Manual runtime smoke tests for request/accept/offer/answer/end.
|
||||
2. Validate unauthorized access rejection for all endpoints.
|
||||
3. Rollout in flag-off/flag-on modes and monitor.
|
||||
|
||||
## 7) Sequence and dependency reasoning
|
||||
|
||||
- Backend simplification must land before frontend simplification to avoid client calling removed paths.
|
||||
- Gateway simplification is independent after stream endpoints are stable but should be merged before frontend hardening.
|
||||
- Frontend changes rely on a stable event contract (`webrtc:signal`, `stream:started`, `stream:ended`).
|
||||
- Docs/validation are final tasks once behavior stabilizes.
|
||||
|
||||
## 8) Failure handling and risk controls
|
||||
|
||||
1. **Rollback strategy:** feature flag switch routes behavior back to existing legacy logic.
|
||||
2. **Forward progress gate:** stream accept/start should not require any media provider credential.
|
||||
3. **Safety gate:** reject unknown/non-matching event payloads in gateway before forwarding.
|
||||
4. **Error handling:** explicit peer connection cleanup on timeout/error and session end.
|
||||
5. **Observability:** structured logs for request/accept/offer/answer/hangup/end with stream session ids.
|
||||
|
||||
## 9) Acceptance criteria
|
||||
|
||||
1. `client` -> request -> camera `accept` -> live WebRTC stream works on web and mobile.
|
||||
2. No `stream:frame` event is used in runtime media path.
|
||||
3. Stream teardown always emits `stream:ended`, clears peer connections, and stops capture.
|
||||
4. Unauthorized role/device combinations are rejected with explicit errors.
|
||||
5. OpenAPI and internal docs only advertise simplified contract by default.
|
||||
6. Feature flag allows toggling between simplified and legacy behavior for quick rollback.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Streaming Simplification Rollout And Rollback
|
||||
|
||||
## Rollout intent
|
||||
|
||||
`SIMPLE_STREAMING` enables the new WebRTC-only control path:
|
||||
- direct `stream:requested` fan-out to the camera
|
||||
- no media-provider session requirement for request/accept/end
|
||||
- no `stream:frame` relay in the backend gateway
|
||||
- web client uses only WebRTC offer/answer/candidate signaling
|
||||
|
||||
## Migration notes
|
||||
|
||||
1. Native mobile is not yet on the WebRTC path.
|
||||
2. `MobileApp/package.json` does not include a native WebRTC dependency such as `react-native-webrtc`.
|
||||
3. `MobileApp/README.md` documents that the mobile app still relies on legacy `stream:frame` relay.
|
||||
4. Because of this, `SIMPLE_STREAMING` defaults to `false`.
|
||||
5. Web and backend are prepared for the simplified path now.
|
||||
|
||||
## Safe enablement sequence
|
||||
|
||||
1. Deploy backend and web changes with `SIMPLE_STREAMING=false`.
|
||||
2. Validate legacy mobile behavior still works.
|
||||
3. Validate web camera/client behavior with `SIMPLE_STREAMING=true` in a controlled environment.
|
||||
4. Add native mobile WebRTC support before changing the default flag value.
|
||||
|
||||
## Runtime checks when enabling
|
||||
|
||||
1. Request a stream from a web client to a web camera.
|
||||
2. Confirm the camera receives `stream:requested`.
|
||||
3. Confirm `stream:started` is emitted after accept.
|
||||
4. Confirm WebRTC offer/answer/candidate exchange succeeds.
|
||||
5. Confirm `stream:ended` tears down both sides cleanly.
|
||||
6. Confirm no `stream:frame` messages appear in gateway logs.
|
||||
|
||||
## Rollback checklist
|
||||
|
||||
1. Set `SIMPLE_STREAMING=false`.
|
||||
2. Restart backend instances.
|
||||
3. Verify `/streams/request` creates `start_stream` commands again.
|
||||
4. Verify mobile frame-relay streaming resumes.
|
||||
5. Re-run web and mobile smoke checks on the legacy path.
|
||||
|
||||
## Known blocker
|
||||
|
||||
Native mobile parity is blocked until a supported RN WebRTC stack is added and wired into `MobileApp/src/app-context.tsx`.
|
||||
@@ -1,44 +0,0 @@
|
||||
# Validation Matrix: Streaming Simplification (WebRTC-only)
|
||||
|
||||
| Row ID | Checklist Item | Status | Code Evidence | Test Evidence | Runtime Evidence |
|
||||
|---|---|---|---|---|---|
|
||||
| B1 | Add/confirm `SIMPLE_STREAMING` environment/config switch | done | `Backend/media/config.ts:28,44-50` (`parseFeatureFlag`, `simpleStreamingEnabled`, `streamRecordingEnabled`) | `bun test` -> `tests/media-config.test.ts` passed (3 cases) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B2 | Update stream request endpoint with simplified flow and role/link checks | done | `Backend/routes/streams.ts:168-184` (`createStreamRequestedPayload`, simplified `/streams/request` response) | `bun test` -> `tests/streaming-simple.test.ts` passed (`builds deterministic requested and started payloads`) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B3 | Update stream accept endpoint to set `streaming` and emit started | done | `Backend/routes/streams.ts:344-356` (`createStreamStartedPayload`, simplified accept response) | `bun test` -> `tests/streaming-simple.test.ts` passed (`builds deterministic requested and started payloads`) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B4 | Update stream end endpoint with proper status, end timestamp, lifecycle emit | done | `Backend/routes/streams.ts:660-721` (`status: nextStatus`, `createStreamEndedPayload`, simplified end response) | `bun test` -> `tests/streaming-simple.test.ts` passed (`normalizes ended payload and strips provider fields from API response`) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B5 | Remove media provider dependency in accept logic | done | `Backend/media/service.ts:17-24` (`mediaProviderRuntimeEnabled`, `createLiveMediaSession`), `Backend/routes/streams.ts:320-334` (accept uses wrapper result) | `bun test` -> `tests/media-config.test.ts`, `tests/streaming-simple.test.ts` both passed | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B6 | Gate/remove recording creation path in stream end logic | done | `Backend/media/config.ts:45,50`, `Backend/routes/streams.ts:681-683` (`streamRecordingEnabled`) | `bun test` passed all 17 tests in `Backend/tests/*` | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B7 | Return minimal deterministic stream payloads | done | `Backend/streaming/simple.ts:35-73` (`createStream*Payload`, `toSimpleStreamSessionResponse`), `Backend/routes/streams.ts:184,356,721` | `bun test` -> `tests/streaming-simple.test.ts` passed (3 cases) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B8 | Document deprecated media fields in schema | done | `Backend/db/schema.ts:68-69` (legacy provider field comments) | `bun test` passed all 17 tests in `Backend/tests/*` | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B9 | Guard media service calls behind non-simplified mode | done | `Backend/media/service.ts:17-24` (`mediaProviderRuntimeEnabled`, `createLiveMediaSession`) | `bun test` -> `tests/media-config.test.ts` passed (3 cases) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B10 | Annotate mock provider as metadata-only in phase-1 path | done | `Backend/media/providers/mock.ts:40-41` (runtime compatibility comment), `Backend/routes/streams.ts:320-334` (runtime no longer requires provider session in simplified mode) | `bun test` passed all 17 tests in `Backend/tests/*` | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B11 | Ensure SFU service not required in simplified mode | done | `Backend/media/sfu/service.ts:1,6-8` (`simpleStreamingEnabled` short-circuit) | `bun test` passed all 17 tests in `Backend/tests/*` | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| B12 | Deprecate command-table stream lifecycle usage under simplified mode | done | `Backend/routes/commands.ts:53`, `Backend/routes/streams.ts:168-184`, `Backend/realtime/gateway.ts:110-116` | `bun test` passed all 17 tests in `Backend/tests/*` | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| R13 | Remove `stream:frame` event listener/sender from gateway | done | `Backend/realtime/gateway.ts:323-349` (`webrtc:signal` only in streaming path) | `bun test` passed all 17 tests in `Backend/tests/*` | `rg -n "stream:frame" Backend/realtime/gateway.ts` returned no matches; `bunx tsc --noEmit` exited 0 |
|
||||
| R14 | Remove frame-relay participant cache and validation branches | done | `Backend/realtime/gateway.ts:240-349` (no `verifiedRelayTargets`, no frame validation branch) | `bun test` passed all 17 tests in `Backend/tests/*` | `rg -n "stream:frame" Backend/realtime/gateway.ts` returned no matches; `bunx tsc --noEmit` exited 0 |
|
||||
| R15 | Keep only `webrtc:signal` relay logic for streaming | done | `Backend/realtime/gateway.ts:323-349` (`socket.on('webrtc:signal'...)`) | `bun test` -> `tests/streaming-simple.test.ts` passed (`only relays WebRTC signals between stream participants`) | `rg -n "stream:frame" Backend/realtime/gateway.ts` returned no matches; `bunx tsc --noEmit` exited 0 |
|
||||
| R16 | Simplify signal validation to ownership + session checks | done | `Backend/realtime/gateway.ts:330-343` (`streamSessions` lookup + `canRelayWebrtcSignal`), `Backend/streaming/simple.ts:20-33` | `bun test` -> `tests/streaming-simple.test.ts` passed (`only relays WebRTC signals between stream participants`) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| R17 | Remove command retry polling for stream command queue when flag is enabled | done | `Backend/realtime/gateway.ts:110-116,155-161` (`start_stream` dispatch/retry disabled under `SIMPLE_STREAMING`) | `bun test` passed all 17 tests in `Backend/tests/*` | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| R18 | Preserve `stream:started` and `stream:ended` event emission | done | `Backend/routes/streams.ts:344-353,685-714`, `Backend/realtime/gateway.ts:36-49` (`sendRealtimeToDevice`) | `bun test` -> `tests/streaming-simple.test.ts` passed (`builds deterministic requested and started payloads`, `normalizes ended payload`) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| R19 | Keep strict socket auth and room handling for devices | done | `Backend/realtime/gateway.ts:176-235` (token auth, role match, room join) | `bun test` -> `tests/device-token.test.ts` passed (2 cases) | `bunx tsc --noEmit` exited 0 in `Backend/` |
|
||||
| W20 | Simplify web stream state machine | done | `WebApp/src/lib/app/controller.js:144-152,950-975`, `WebApp/src/lib/app/store.js:43-44` | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `npm run build` in `WebApp/` passed |
|
||||
| W21 | Remove web fallback image mode and frame state | done | `WebApp/src/lib/app/store.js:43-44`, `WebApp/src/lib/app/controller.js:144-152`, `WebApp/src/routes/client/+page.svelte:183-189` | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `rg -n "stream:frame|clientFallbackFrame|getPublishCreds|getSubscribeCreds|frameRelay" WebApp/src` returned no matches; `npm run build` passed |
|
||||
| W22 | Enforce video-only rendering path on client | done | `WebApp/src/routes/client/+page.svelte:183-189`, `WebApp/src/lib/app/controller.js:723,950-968` | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `npm run build` in `WebApp/` passed |
|
||||
| W23 | Simplify `ontrack` handling | done | `WebApp/src/lib/app/controller.js:711-723` (`connection.ontrack`) | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `npm run build` in `WebApp/` passed |
|
||||
| W24 | Add deterministic teardown path on end/unmount/navigation | done | `WebApp/src/lib/app/controller.js:596-614,975-992,1125-1132` (`teardownPeerConnection`, `stream:ended`, cleanup) | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `npm run build` in `WebApp/` passed |
|
||||
| W25 | Remove frame capture/sender fallback on web camera page | done | `WebApp/src/lib/app/controller.js:869-882,908-928` (camera request handling now only starts preview/accept/offer), `WebApp/src/routes/camera/+page.svelte` remains preview-only | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `rg -n "stream:frame|clientFallbackFrame|getPublishCreds|getSubscribeCreds|frameRelay" WebApp/src` returned no matches; `npm run build` passed |
|
||||
| W26 | Ensure camera only sends WebRTC offer path | done | `WebApp/src/lib/app/controller.js:738-749,869-882,908-928` (`startOfferToClient`, `handleCameraStreamRequest`) | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `npm run build` in `WebApp/` passed |
|
||||
| W27 | Remove recording hooks tied to fallback transport | done | `WebApp/src/lib/app/controller.js:869-882,975-992` (recording kept, fallback transport removed), no frame relay refs remain in `WebApp/src` | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `rg -n "stream:frame|clientFallbackFrame|getPublishCreds|getSubscribeCreds|frameRelay" WebApp/src` returned no matches; `npm run build` passed |
|
||||
| W28 | Remove client-side image fallback UI branches | done | `WebApp/src/routes/client/+page.svelte:183-189`, `WebApp/src/lib/app/store.js:43-44` | `npm run check` in `WebApp/` passed (`svelte-check found 0 errors and 0 warnings`) | `npm run build` in `WebApp/` passed |
|
||||
| M29 | Remove mobile frame relay state and timer lifecycle | blocked | `MobileApp/src/app-context.tsx:86-87,175-213`, `MobileApp/README.md:75-79` show native mobile still depends on frame relay; `MobileApp/package.json` has no `react-native-webrtc` dependency | `npm run lint` in `MobileApp/` passed | `rg -n "react-native-webrtc" MobileApp/package.json` returned no matches; native WebRTC stack still absent |
|
||||
| M30 | Align mobile state transitions with web logic | blocked | `MobileApp/src/app-context.tsx:105-113,349-381`, `MobileApp/src/state.ts:92-93`, `MobileApp/README.md:75-79` | `npm run lint` in `MobileApp/` passed | `npx tsc --noEmit` exited 0 in `MobileApp/`; implementation remains blocked by missing native WebRTC runtime |
|
||||
| M31 | Remove mobile JPEG relay loop | blocked | `MobileApp/src/app-context.tsx:183-213,312-319`, `MobileApp/README.md:75-79` | `npm run lint` in `MobileApp/` passed | `rg -n "react-native-webrtc" MobileApp/package.json` returned no matches; removing the JPEG loop would break the only current mobile stream path |
|
||||
| M32 | Remove mobile image fallback rendering path | blocked | `MobileApp/app/(tabs)/index.tsx:150-151,284`, `MobileApp/src/state.ts:92-93` | `npm run lint` in `MobileApp/` passed | `npx tsc --noEmit` exited 0 in `MobileApp/`; image fallback remains required until native WebRTC view/render support exists |
|
||||
| M33 | Remove publish/subscribe credential calls from streaming runtime | blocked | `MobileApp/src/app-context.tsx:312,361`, `MobileApp/src/api.ts:84-85` | `npm run lint` in `MobileApp/` passed | `npx tsc --noEmit` exited 0 in `MobileApp/`; credential calls remain because mobile still follows the legacy relay path |
|
||||
| M34 | Ensure mobile peer cleanup on end/unmount | blocked | `MobileApp/src/app-context.tsx:375-412` has no RTCPeerConnection lifecycle to clean up; `MobileApp/README.md:79` documents missing RN WebRTC stack | `npm run lint` in `MobileApp/` passed | `rg -n "react-native-webrtc" MobileApp/package.json` returned no matches; unblock by adding supported native WebRTC dependency and peer lifecycle code |
|
||||
| C35 | Update OpenAPI streaming contract | done | `Backend/docs/openapi.ts:740,786,832`; legacy credential/playback stream paths removed from OpenAPI | `bunx tsc --noEmit` exited 0 in `Backend/` | `rg -n "publish-credentials|subscribe-credentials|playback-token" Backend/docs/openapi.ts` returned no matches |
|
||||
| C36 | Update docs/comments to remove fallback as primary path | done | `Backend/README.md:133,147,191`, `MobileApp/README.md:75-79`, `Backend/public/backend-architecture.html:499-500`, `Backend/db/schema.ts:68-69` | `npm run check` in `WebApp/` passed; `bunx tsc --noEmit` in `Backend/` passed | `rg -n "stream:frame|frame relay|SIMPLE_STREAMING" Backend/README.md MobileApp/README.md Backend/public/backend-architecture.html docs/STREAMING_SIMPLIFICATION_ROLLOUT_AND_ROLLBACK.md` confirmed updated docs |
|
||||
| C37 | Add migration and deprecation guidance | done | `docs/STREAMING_SIMPLIFICATION_ROLLOUT_AND_ROLLBACK.md:11-19`, `Backend/README.md:147-149` | `bunx tsc --noEmit` exited 0 in `Backend/` | `rg -n "Migration notes|Safe enablement sequence" docs/STREAMING_SIMPLIFICATION_ROLLOUT_AND_ROLLBACK.md` returned matches at lines 11 and 19 |
|
||||
| C38 | Add structured stream lifecycle logging | done | `Backend/routes/streams.ts:171,230,359,719`, `Backend/realtime/gateway.ts:348` | `bun test` passed all 17 tests in `Backend/tests/*` | `bunx tsc --noEmit` exited 0 in `Backend/`; log markers present for request/accept/end/signal events |
|
||||
| C39 | Add compile/build checks after cleanup | done | Validation spans `Backend/`, `WebApp/`, and `MobileApp/` | `bun test` passed (17/17), `npm run check` passed, `npm run lint` passed | `bunx tsc --noEmit` in `Backend/` exited 0, `npm run build` in `WebApp/` passed, `npx tsc --noEmit` in `MobileApp/` exited 0 |
|
||||
| C40 | Add rollback runbook for flag behavior | done | `docs/STREAMING_SIMPLIFICATION_ROLLOUT_AND_ROLLBACK.md:35-43` | `bunx tsc --noEmit` exited 0 in `Backend/` | `rg -n "Rollback checklist|Known blocker" docs/STREAMING_SIMPLIFICATION_ROLLOUT_AND_ROLLBACK.md` returned matches at lines 35 and 43 |
|
||||
@@ -1,48 +0,0 @@
|
||||
# SecureCam codebase architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CAMERA["Camera device
|
||||
Mobile camera app / browser simulator"]:::client
|
||||
|
||||
USER["User devices
|
||||
Web app / mobile app"]:::client
|
||||
|
||||
subgraph PLATFORM["SecureCam platform"]
|
||||
BACKEND["Backend service
|
||||
• authentication
|
||||
• device coordination
|
||||
• live stream control
|
||||
• motion events
|
||||
• recordings access"]:::backend
|
||||
end
|
||||
|
||||
DATA["Core data layer
|
||||
PostgreSQL"]:::data
|
||||
|
||||
MEDIA["Media storage
|
||||
MinIO / S3-compatible object storage"]:::data
|
||||
|
||||
NOTIFY["Notifications
|
||||
push + in-app activity updates"]:::service
|
||||
|
||||
CAMERA -->|"motion, stream, recordings"| BACKEND
|
||||
USER -->|"auth, viewing, control"| BACKEND
|
||||
|
||||
BACKEND -->|"app state, users, devices, events"| DATA
|
||||
BACKEND -->|"video and recording assets"| MEDIA
|
||||
BACKEND -->|"alerts and activity fan-out"| NOTIFY
|
||||
|
||||
NOTIFY --> USER
|
||||
|
||||
classDef client fill:#e8f1ff,stroke:#3b82f6,stroke-width:2px,color:#111827;
|
||||
classDef backend fill:#ecfdf3,stroke:#16a34a,stroke-width:2px,color:#111827;
|
||||
classDef data fill:#fff7e8,stroke:#f59e0b,stroke-width:2px,color:#111827;
|
||||
classDef service fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#111827;
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This version is intentionally high level.
|
||||
- It treats the web app, mobile app, and simulator as client surfaces around one backend platform.
|
||||
- Internal backend modules such as routes, workers, realtime gateway, and media scaffolding are abstracted into a single service block.
|
||||
@@ -1,218 +0,0 @@
|
||||
# SecureCam database schema
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS {
|
||||
uuid id PK
|
||||
varchar email UK
|
||||
varchar name
|
||||
varchar password_hash
|
||||
boolean email_verified
|
||||
text image
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
DEVICES {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
varchar name
|
||||
varchar role
|
||||
varchar platform
|
||||
varchar app_version
|
||||
text push_token
|
||||
varchar status
|
||||
boolean is_camera
|
||||
timestamp last_seen_at
|
||||
timestamp updated_at
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
DEVICE_LINKS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid camera_device_id FK
|
||||
uuid client_device_id FK
|
||||
varchar status
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
COMMANDS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid source_device_id FK
|
||||
uuid target_device_id FK
|
||||
varchar command_type
|
||||
jsonb payload
|
||||
varchar status
|
||||
integer retry_count
|
||||
timestamp last_dispatched_at
|
||||
timestamp acknowledged_at
|
||||
text error
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
STREAM_SESSIONS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid camera_device_id FK
|
||||
uuid requester_device_id FK
|
||||
varchar status
|
||||
varchar reason
|
||||
varchar media_provider
|
||||
varchar media_session_id
|
||||
text publish_endpoint
|
||||
text subscribe_endpoint
|
||||
varchar stream_key
|
||||
timestamp started_at
|
||||
timestamp ended_at
|
||||
jsonb metadata
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
RECORDINGS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid stream_session_id FK
|
||||
uuid camera_device_id FK
|
||||
uuid requester_device_id FK
|
||||
uuid event_id FK
|
||||
varchar object_key
|
||||
varchar bucket
|
||||
integer duration_seconds
|
||||
integer size_bytes
|
||||
varchar status
|
||||
timestamp available_at
|
||||
text error
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
EVENTS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid device_id FK
|
||||
varchar title
|
||||
varchar triggered_by
|
||||
varchar status
|
||||
timestamp started_at
|
||||
timestamp ended_at
|
||||
timestamp notified_at
|
||||
varchar video_url UK
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
NOTIFICATIONS {
|
||||
uuid id PK
|
||||
uuid event_id FK
|
||||
uuid user_id FK
|
||||
timestamp sent_at
|
||||
varchar channel
|
||||
varchar status
|
||||
boolean is_read
|
||||
}
|
||||
|
||||
NOTIFICATION_DELIVERIES {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid recipient_device_id FK
|
||||
varchar type
|
||||
jsonb payload
|
||||
varchar status
|
||||
integer attempts
|
||||
text last_error
|
||||
timestamp sent_at
|
||||
timestamp next_attempt_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
AUDIT_LOGS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid actor_device_id FK
|
||||
varchar action
|
||||
varchar target_type
|
||||
varchar target_id
|
||||
jsonb metadata
|
||||
text ip_address
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
ACCOUNT {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
text account_id
|
||||
text provider_id
|
||||
text access_token
|
||||
text refresh_token
|
||||
timestamp access_token_expires_at
|
||||
timestamp refresh_token_expires_at
|
||||
text id_token
|
||||
text scope
|
||||
text password
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
SESSION {
|
||||
uuid id PK
|
||||
text token UK
|
||||
uuid user_id FK
|
||||
timestamp expires_at
|
||||
text ip_address
|
||||
text user_agent
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
VERIFICATION {
|
||||
uuid id PK
|
||||
text identifier
|
||||
text value
|
||||
timestamp expires_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
USERS ||--o{ DEVICES : owns
|
||||
USERS ||--o{ DEVICE_LINKS : owns
|
||||
USERS ||--o{ COMMANDS : owns
|
||||
USERS ||--o{ STREAM_SESSIONS : owns
|
||||
USERS ||--o{ RECORDINGS : owns
|
||||
USERS ||--o{ EVENTS : creates
|
||||
USERS ||--o{ NOTIFICATIONS : receives
|
||||
USERS ||--o{ NOTIFICATION_DELIVERIES : owns
|
||||
USERS ||--o{ AUDIT_LOGS : owns
|
||||
USERS ||--o{ ACCOUNT : auth_account
|
||||
USERS ||--o{ SESSION : auth_session
|
||||
|
||||
DEVICES ||--o{ DEVICE_LINKS : camera_device
|
||||
DEVICES ||--o{ DEVICE_LINKS : client_device
|
||||
DEVICES ||--o{ COMMANDS : source_device
|
||||
DEVICES ||--o{ COMMANDS : target_device
|
||||
DEVICES ||--o{ STREAM_SESSIONS : camera_device
|
||||
DEVICES ||--o{ STREAM_SESSIONS : requester_device
|
||||
DEVICES ||--o{ RECORDINGS : camera_device
|
||||
DEVICES ||--o{ RECORDINGS : requester_device
|
||||
DEVICES ||--o{ EVENTS : triggers
|
||||
DEVICES ||--o{ NOTIFICATION_DELIVERIES : recipient_device
|
||||
DEVICES ||--o{ AUDIT_LOGS : actor_device
|
||||
|
||||
STREAM_SESSIONS ||--o{ RECORDINGS : produces
|
||||
EVENTS o|--o{ RECORDINGS : may_attach
|
||||
EVENTS ||--o{ NOTIFICATIONS : generates
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This is generated from the current Drizzle schema in [schema.ts](/home/matiss/Documents/Final Year Project/Backend/db/schema.ts).
|
||||
- `VERIFICATION` is currently standalone in the schema and does not reference `USERS`.
|
||||
- `DEVICE_LINKS`, `COMMANDS`, `STREAM_SESSIONS`, and `RECORDINGS` each reference `DEVICES` more than once for different roles.
|
||||
- `ACCOUNT`, `SESSION`, and `VERIFICATION` are the Better Auth support tables.
|
||||
- `recordings` now absorbs the old upload metadata responsibility that previously lived in `videos`.
|
||||
- `notification_deliveries` represents push delivery jobs, while `notifications` remains the user-facing event notification history.
|
||||
@@ -1,56 +0,0 @@
|
||||
# SecureCam ideal project Gantt chart
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title SecureCam Project Timeline
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %b
|
||||
excludes weekends
|
||||
|
||||
section Initiation and Planning
|
||||
Project scoping and proposal :done, a1, 2025-10-15, 10d
|
||||
Requirements gathering and research :done, a2, after a1, 12d
|
||||
Early architecture and feasibility :done, a3, after a1, 15d
|
||||
|
||||
section Initial Prototype
|
||||
Core prototype implementation :done, b1, 2025-10-28, 18d
|
||||
Basic camera-to-client workflow :done, b2, after b1, 10d
|
||||
|
||||
section Scope Change and Refactor
|
||||
Major scope change identified :milestone, m1, 2025-11-12, 0d
|
||||
Requirements redefinition :crit, c1, 2025-11-13, 8d
|
||||
Architecture redesign :crit, c2, after c1, 10d
|
||||
Backend refactor and model cleanup :crit, c3, after c2, 18d
|
||||
Web and mobile flow realignment :crit, c4, after c2, 14d
|
||||
|
||||
section Platform Rebuild
|
||||
Authentication and device model :d1, 2025-12-09, 12d
|
||||
Realtime communication layer :d2, after d1, 14d
|
||||
Stream control and recording pipeline :d3, after d2, 16d
|
||||
Storage integration and signed media URLs :d4, after d1, 12d
|
||||
Motion events and activity feed :d5, after d2, 10d
|
||||
|
||||
section Client Applications
|
||||
Web app feature completion :e1, 2026-01-20, 18d
|
||||
Mobile app implementation :e2, 2026-01-27, 24d
|
||||
Push notifications and user feedback loop :e3, after d5, 12d
|
||||
|
||||
section Quality and Evaluation
|
||||
Integration testing and bug fixing :f1, 2026-02-24, 18d
|
||||
Performance tuning and resilience review :f2, after f1, 10d
|
||||
Final validation and demo preparation :f3, after f2, 8d
|
||||
|
||||
section Dissertation and Delivery
|
||||
Documentation and architecture diagrams :g1, 2026-03-17, 12d
|
||||
Report writing and iteration :g2, 2026-03-10, 26d
|
||||
Final edits and submission prep :g3, after g2, 8d
|
||||
Project complete :milestone, m2, 2026-04-17, 0d
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Start date assumed: `2025-10-15` to reflect a mid-October project start.
|
||||
- Final deadline set to: `2026-04-17`.
|
||||
- The chart assumes a major scope shift in mid-November that forced the team to revisit requirements and refactor the platform.
|
||||
- The refactor is treated as a real disruption rather than a minor iteration, so the later phases are shown as a rebuild on firmer architecture rather than a straight continuation of the original prototype.
|
||||
- This is an "idealised but realistic" plan for documentation purposes, not a claim that every task happened exactly in this order.
|
||||
@@ -1,29 +0,0 @@
|
||||
# 5.3.1 Server Bootstrap and Runtime Setup
|
||||
|
||||
This diagram shows how the backend starts, mounts services, and becomes ready to handle API and realtime traffic.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Node as Node Runtime
|
||||
participant Index as Backend/index.ts
|
||||
participant App as Express App
|
||||
participant Auth as Better Auth
|
||||
participant Routes as Route Modules
|
||||
participant MinIO as MinIO
|
||||
participant RT as Socket.IO Gateway
|
||||
participant Rec as Recordings Worker
|
||||
participant Push as Push Worker
|
||||
|
||||
Node->>Index: start process
|
||||
Index->>App: create express app
|
||||
Index->>App: configure helmet + cors + JSON middleware
|
||||
Index->>Auth: mount /api/auth/*
|
||||
Index->>Routes: mount videos/admin/devices/links/streams/events/recordings/ops
|
||||
Index->>MinIO: ensureMinioBucket()
|
||||
MinIO-->>Index: bucket ready
|
||||
Index->>RT: setupRealtimeGateway(server)
|
||||
Index->>Rec: startRecordingsWorker()
|
||||
Index->>Push: startPushWorker()
|
||||
Index->>App: listen on configured port
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
# 5.3.10 Operational Documentation and Support Assets
|
||||
|
||||
This diagram maps the implementation code to the support assets used to inspect, document, and validate the system.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Runtime[Runtime System]
|
||||
APIs[API Routes]
|
||||
Docs[OpenAPI Docs]
|
||||
Sim[Simulator Pages]
|
||||
Workers[Background Workers]
|
||||
Validation[Validation and rollout docs]
|
||||
Admin[Admin / Ops routes]
|
||||
|
||||
Runtime --> APIs
|
||||
Runtime --> Workers
|
||||
Runtime --> Admin
|
||||
APIs --> Docs
|
||||
APIs --> Sim
|
||||
Runtime --> Validation
|
||||
|
||||
subgraph Support["Support layer"]
|
||||
Docs
|
||||
Sim
|
||||
Workers
|
||||
Validation
|
||||
Admin
|
||||
end
|
||||
```
|
||||
@@ -1,30 +0,0 @@
|
||||
# 5.3.2 User Authentication and Session Handling
|
||||
|
||||
This diagram separates human user authentication from device-level authentication.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
User[User in Browser]
|
||||
AuthAPI[/Better Auth Endpoints/]
|
||||
Session[(session table)]
|
||||
Users[(users table)]
|
||||
Accounts[(account table)]
|
||||
DeviceReg[/Device Registration API/]
|
||||
DeviceToken[Signed Device Token]
|
||||
DeviceAPI[/Device Auth Routes/]
|
||||
|
||||
User -->|sign up / sign in| AuthAPI
|
||||
AuthAPI --> Users
|
||||
AuthAPI --> Accounts
|
||||
AuthAPI --> Session
|
||||
Session -->|cookie-backed session| User
|
||||
|
||||
User -->|authenticated session| DeviceReg
|
||||
DeviceReg -->|register browser as camera/client| DeviceToken
|
||||
DeviceToken --> DeviceAPI
|
||||
|
||||
classDef auth fill:#e8f1ff,stroke:#2563eb,stroke-width:2px,color:#111827;
|
||||
classDef data fill:#fff7e8,stroke:#d97706,stroke-width:2px,color:#111827;
|
||||
class AuthAPI,DeviceReg,DeviceAPI,DeviceToken auth;
|
||||
class Session,Users,Accounts data;
|
||||
```
|
||||
@@ -1,26 +0,0 @@
|
||||
# 5.3.3 Device Identity Registration and Presence
|
||||
|
||||
This diagram shows how a signed-in user registers a browser as a device and how presence is maintained after realtime connection.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User as Signed-in User
|
||||
participant Web as WebApp Controller
|
||||
participant Devices as POST /devices/register
|
||||
participant DB as devices + device_links
|
||||
participant Token as Device Token
|
||||
participant Socket as Socket.IO Gateway
|
||||
|
||||
User->>Web: submit onboarding role + name
|
||||
Web->>Devices: register device
|
||||
Devices->>DB: insert devices row
|
||||
Devices->>DB: auto-link to opposite-role devices if present
|
||||
Devices-->>Web: return device + deviceToken
|
||||
Web->>Web: persist saved device record
|
||||
Web->>Socket: connect with device token
|
||||
Socket->>DB: mark device online
|
||||
Web->>Socket: periodic heartbeat
|
||||
Socket->>DB: update lastSeenAt and status
|
||||
Socket-->>Web: connection + heartbeat acknowledgements
|
||||
```
|
||||
@@ -1,97 +0,0 @@
|
||||
# 5.3.4 Database Schema and Persistence Model
|
||||
|
||||
## Core Entity Relationship Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS ||--o{ DEVICES : owns
|
||||
USERS ||--o{ DEVICE_LINKS : owns
|
||||
DEVICES ||--o{ DEVICE_LINKS : cameraDeviceId
|
||||
DEVICES ||--o{ DEVICE_LINKS : clientDeviceId
|
||||
USERS ||--o{ STREAM_SESSIONS : owns
|
||||
DEVICES ||--o{ STREAM_SESSIONS : cameraDeviceId
|
||||
DEVICES ||--o{ STREAM_SESSIONS : requesterDeviceId
|
||||
|
||||
USERS {
|
||||
uuid id PK
|
||||
varchar email
|
||||
varchar name
|
||||
}
|
||||
DEVICES {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
varchar role
|
||||
varchar status
|
||||
timestamp last_seen_at
|
||||
}
|
||||
DEVICE_LINKS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid camera_device_id FK
|
||||
uuid client_device_id FK
|
||||
varchar status
|
||||
}
|
||||
STREAM_SESSIONS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid camera_device_id FK
|
||||
uuid requester_device_id FK
|
||||
varchar status
|
||||
varchar reason
|
||||
}
|
||||
```
|
||||
|
||||
## Media and Event Persistence
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS ||--o{ EVENTS : owns
|
||||
DEVICES ||--o{ EVENTS : triggeredBy
|
||||
STREAM_SESSIONS ||--o{ RECORDINGS : produces
|
||||
EVENTS ||--o{ RECORDINGS : mayCreate
|
||||
USERS ||--o{ NOTIFICATIONS : receives
|
||||
EVENTS ||--o{ NOTIFICATIONS : generates
|
||||
USERS ||--o{ NOTIFICATION_DELIVERIES : owns
|
||||
DEVICES ||--o{ NOTIFICATION_DELIVERIES : targets
|
||||
USERS ||--o{ AUDIT_LOGS : owns
|
||||
DEVICES ||--o{ AUDIT_LOGS : actor
|
||||
|
||||
EVENTS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid device_id FK
|
||||
varchar status
|
||||
timestamp started_at
|
||||
timestamp ended_at
|
||||
}
|
||||
RECORDINGS {
|
||||
uuid id PK
|
||||
uuid stream_session_id FK
|
||||
uuid event_id FK
|
||||
varchar object_key
|
||||
varchar bucket
|
||||
varchar status
|
||||
}
|
||||
NOTIFICATIONS {
|
||||
uuid id PK
|
||||
uuid event_id FK
|
||||
uuid user_id FK
|
||||
varchar channel
|
||||
varchar status
|
||||
}
|
||||
NOTIFICATION_DELIVERIES {
|
||||
uuid id PK
|
||||
uuid recipient_device_id FK
|
||||
varchar type
|
||||
varchar status
|
||||
int attempts
|
||||
}
|
||||
AUDIT_LOGS {
|
||||
uuid id PK
|
||||
uuid owner_user_id FK
|
||||
uuid actor_device_id FK
|
||||
varchar action
|
||||
varchar target_type
|
||||
varchar target_id
|
||||
}
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
# 5.3.5 Device Linking and Command-Oriented Control
|
||||
|
||||
This comparison diagram contrasts the older command-driven path with the current simplified stream request path.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Legacy["Legacy command-oriented path"]
|
||||
LClient[Client Device]
|
||||
LBackend[Backend]
|
||||
LCmd[(commands table)]
|
||||
LGateway[Realtime Gateway]
|
||||
LCamera[Camera Device]
|
||||
|
||||
LClient -->|request stream| LBackend
|
||||
LBackend --> LCmd
|
||||
LCmd --> LGateway
|
||||
LGateway -->|command:received| LCamera
|
||||
LCamera -->|command:ack| LGateway
|
||||
LGateway --> LBackend
|
||||
end
|
||||
|
||||
subgraph Simple["Simplified linked-device path"]
|
||||
SClient[Client Device]
|
||||
SBackend[Backend]
|
||||
SLinks[(device_links)]
|
||||
SSession[(stream_sessions)]
|
||||
SCamera[Camera Device]
|
||||
|
||||
SClient -->|request stream| SBackend
|
||||
SBackend --> SLinks
|
||||
SBackend --> SSession
|
||||
SBackend -->|stream:requested| SCamera
|
||||
SCamera -->|accept| SBackend
|
||||
SBackend -->|stream:started| SClient
|
||||
end
|
||||
```
|
||||
@@ -1,46 +0,0 @@
|
||||
# 5.3.6 Stream Sessions and Signalling
|
||||
|
||||
## Stream Request and WebRTC Signalling Sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Client as Client Browser
|
||||
participant Backend as Backend /streams
|
||||
participant Links as device_links
|
||||
participant Session as stream_sessions
|
||||
participant Camera as Camera Browser
|
||||
participant RT as Socket.IO Relay
|
||||
|
||||
Client->>Backend: POST /streams/request
|
||||
Backend->>Links: verify active camera-client link
|
||||
Backend->>Session: insert requested session
|
||||
Backend->>Camera: emit stream:requested
|
||||
Camera->>Backend: POST /streams/:id/accept
|
||||
Backend->>Session: update status to streaming
|
||||
Backend->>Client: emit stream:started
|
||||
Camera->>RT: webrtc:signal offer
|
||||
RT->>Client: relay offer
|
||||
Client->>RT: webrtc:signal answer
|
||||
RT->>Camera: relay answer
|
||||
Client->>RT: ICE candidates
|
||||
RT->>Camera: relay ICE candidates
|
||||
Camera->>RT: ICE candidates
|
||||
RT->>Client: relay ICE candidates
|
||||
```
|
||||
|
||||
## Stream Session State Diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> requested
|
||||
requested --> streaming: camera accepts
|
||||
requested --> cancelled: requester cancels
|
||||
requested --> failed: validation or delivery failure
|
||||
streaming --> completed: normal end
|
||||
streaming --> cancelled: manual stop
|
||||
streaming --> failed: connection or upload failure
|
||||
completed --> [*]
|
||||
cancelled --> [*]
|
||||
failed --> [*]
|
||||
```
|
||||
@@ -1,42 +0,0 @@
|
||||
# 5.3.7 Motion Events Notifications and Audit Trail
|
||||
|
||||
## Motion Event Lifecycle
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Trigger[Manual trigger or browser motion detector]
|
||||
Start[/POST /events/motion/start/]
|
||||
Event[(events table)]
|
||||
Links[(device_links)]
|
||||
RT[Realtime motion:detected]
|
||||
Push[Queued push notification]
|
||||
End[/POST /events/:id/motion/end/]
|
||||
|
||||
Trigger --> Start
|
||||
Start --> Event
|
||||
Start --> Links
|
||||
Links --> RT
|
||||
Links --> Push
|
||||
RT --> End
|
||||
Push --> End
|
||||
End --> Event
|
||||
```
|
||||
|
||||
## Activity and Audit Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Camera[Camera Device]
|
||||
Event[(events)]
|
||||
Notif[(notifications)]
|
||||
Delivery[(notification_deliveries)]
|
||||
Audit[(audit_logs)]
|
||||
Client[Client Device]
|
||||
|
||||
Camera --> Event
|
||||
Event --> Notif
|
||||
Event --> Delivery
|
||||
Event --> Audit
|
||||
Notif --> Client
|
||||
Delivery --> Client
|
||||
```
|
||||
@@ -1,40 +0,0 @@
|
||||
# 5.3.8 Recordings and Object Storage
|
||||
|
||||
## Recording Pipeline
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Cam as Camera Browser
|
||||
participant Ctrl as Web Controller
|
||||
participant Videos as /videos/upload-url
|
||||
participant MinIO as MinIO Object Storage
|
||||
participant Rec as /recordings/:id/finalize
|
||||
participant DB as recordings table
|
||||
participant Viewer as Client Browser
|
||||
|
||||
Cam->>Ctrl: stop local MediaRecorder
|
||||
Ctrl->>Ctrl: compress recording blob
|
||||
Ctrl->>Videos: request presigned upload URL
|
||||
Videos-->>Ctrl: objectKey + uploadUrl
|
||||
Ctrl->>MinIO: PUT recording blob
|
||||
Ctrl->>Rec: finalize recording metadata
|
||||
Rec->>DB: mark recording ready
|
||||
Viewer->>Rec: GET /recordings/:id/download-url
|
||||
Rec-->>Viewer: presigned download URL
|
||||
```
|
||||
|
||||
## Storage Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Backend[Backend Service]
|
||||
PG[(Postgres)]
|
||||
MinIO[(MinIO / S3)]
|
||||
Web[Web Client]
|
||||
|
||||
Web -->|stream, event, recording metadata| Backend
|
||||
Backend -->|users, devices, links, sessions, events, recordings| PG
|
||||
Backend -->|presigned upload/download + object checks| MinIO
|
||||
Web -->|PUT / GET via presigned URLs| MinIO
|
||||
```
|
||||
@@ -1,53 +0,0 @@
|
||||
# 5.3.9 Web Application Implementation
|
||||
|
||||
## Web Application Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Routes[Svelte Route Pages]
|
||||
Controller[controller.js]
|
||||
Store[store.js]
|
||||
API[api.js]
|
||||
Socket[Socket.IO Client]
|
||||
Backend[Backend APIs]
|
||||
|
||||
Routes --> Controller
|
||||
Controller --> Store
|
||||
Controller --> API
|
||||
Controller --> Socket
|
||||
API --> Backend
|
||||
Socket --> Backend
|
||||
|
||||
subgraph RoutePages["Main route pages"]
|
||||
Auth[Auth]
|
||||
Onboarding[Onboarding]
|
||||
Camera[Camera]
|
||||
Client[Client]
|
||||
Activity[Activity]
|
||||
Settings[Settings]
|
||||
end
|
||||
|
||||
Routes --> RoutePages
|
||||
```
|
||||
|
||||
## UI Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Open WebApp]
|
||||
Auth[Sign in / Sign up]
|
||||
Onboarding[Register browser as device]
|
||||
Role{Chosen role}
|
||||
Camera[Camera dashboard]
|
||||
Client[Client dashboard]
|
||||
Activity[Activity page]
|
||||
Settings[Settings page]
|
||||
|
||||
Start --> Auth --> Onboarding --> Role
|
||||
Role -->|camera| Camera
|
||||
Role -->|client| Client
|
||||
Camera --> Activity
|
||||
Camera --> Settings
|
||||
Client --> Activity
|
||||
Client --> Settings
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
# Temporary Section 5.3 Diagrams
|
||||
|
||||
These temporary files contain Mermaid diagrams for the `5.3` implementation chapter.
|
||||
|
||||
## Files
|
||||
|
||||
- [5.3.1 Server Bootstrap](./5.3.1-server-bootstrap.md)
|
||||
- [5.3.2 Authentication and Sessions](./5.3.2-authentication-and-sessions.md)
|
||||
- [5.3.3 Device Registration and Presence](./5.3.3-device-registration-and-presence.md)
|
||||
- [5.3.4 Database Schema and Persistence](./5.3.4-database-schema-and-persistence.md)
|
||||
- [5.3.5 Linking and Control Flow](./5.3.5-linking-and-control-flow.md)
|
||||
- [5.3.6 Stream Sessions and Signalling](./5.3.6-stream-sessions-and-signalling.md)
|
||||
- [5.3.7 Motion Events Notifications Audit](./5.3.7-motion-events-notifications-audit.md)
|
||||
- [5.3.8 Recordings and Object Storage](./5.3.8-recordings-and-object-storage.md)
|
||||
- [5.3.9 Web Application Implementation](./5.3.9-web-application-implementation.md)
|
||||
- [5.3.10 Operational and Support Assets](./5.3.10-operational-and-support-assets.md)
|
||||
|
||||
These are temporary working diagrams intended for screenshots and report drafting.
|
||||