feat(app): add controller, state store, and API client modules

This commit is contained in:
2026-02-26 16:20:00 +00:00
parent 1ee6b21808
commit 13e77294be
3 changed files with 1693 additions and 0 deletions

72
WebApp/src/lib/app/api.js Normal file
View File

@@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { getAppState } from './store';
const request = async (path, options = {}) => {
const { deviceToken } = getAppState();
const headers = { 'Content-Type': 'application/json' };
if (deviceToken) {
headers.Authorization = `Bearer ${deviceToken}`;
}
const response = await fetch(path, {
...options,
headers: {
...headers,
...(options.headers || {})
}
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.message || data.error || response.statusText || 'Request failed');
}
return data;
};
export const api = {
request,
auth: {
signUp: (data) => request('/api/auth/sign-up/email', { method: 'POST', body: JSON.stringify(data) }),
signIn: (data) => request('/api/auth/sign-in/email', { method: 'POST', body: JSON.stringify(data) }),
getSession: () => request('/api/auth/get-session'),
signOut: () => request('/api/auth/sign-out', { method: 'POST', body: JSON.stringify({}) })
},
devices: {
register: (data) => request('/devices/register', { method: 'POST', body: JSON.stringify(data) }),
list: () => request('/devices'),
update: (deviceId, data) => request(`/devices/${deviceId}`, { method: 'PATCH', body: JSON.stringify(data) }),
listLinks: () => request('/device-links'),
link: (cameraDeviceId, clientDeviceId) =>
request('/device-links', { method: 'POST', body: JSON.stringify({ cameraDeviceId, clientDeviceId }) }),
unlink: (linkId) => request(`/device-links/${linkId}`, { method: 'DELETE' })
},
streams: {
request: (cameraDeviceId) =>
request('/streams/request', {
method: 'POST',
body: JSON.stringify({ cameraDeviceId, reason: 'on_demand' })
}),
accept: (id) => request(`/streams/${id}/accept`, { method: 'POST', body: JSON.stringify({}) }),
end: (id) => request(`/streams/${id}/end`, { method: 'POST', body: JSON.stringify({ reason: 'completed' }) }),
getPublishCreds: (id) => request(`/streams/${id}/publish-credentials`),
getSubscribeCreds: (id) => request(`/streams/${id}/subscribe-credentials`)
},
events: {
startMotion: () =>
request('/events/motion/start', {
method: 'POST',
body: JSON.stringify({ title: 'Simulated Motion', triggeredBy: 'motion' })
}),
endMotion: (id) => request(`/events/${id}/motion/end`, { method: 'POST', body: JSON.stringify({ status: 'completed' }) }),
finalizeRecording: (id, payload) => request(`/recordings/${id}/finalize`, { method: 'POST', body: JSON.stringify(payload) })
},
ops: {
listRecordings: () => request('/recordings/me/list'),
getRecordingDownloadUrl: (recordingId) => request(`/recordings/${recordingId}/download-url`),
listNotifications: () => request('/push-notifications/me')
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { derived, get, writable } from 'svelte/store';
export const createInitialState = () => ({
page: 'auth',
session: null,
device: null,
deviceToken: null,
socketConnected: false,
isMotionActive: false,
cameraStatus: 'idle',
cameraPreviewReady: false,
linkedCameras: [],
recordings: [],
motionNotifications: [],
activeCameraDeviceId: null,
activeStreamSessionId: null,
openLinkedCameraMenuId: null,
activityLog: [],
cameraSessions: {},
connectedStreamSessionIds: [],
loading: false,
isRegistering: false,
authForm: {
email: '',
password: '',
name: ''
},
onboardingForm: {
name: '',
role: 'client',
pushToken: ''
},
toasts: [],
recordingModal: {
open: false,
title: 'Recording Playback',
url: ''
},
clientStreamMode: 'none',
clientFallbackFrame: '',
clientPlaceholderText: 'Select a camera to view',
lastError: null
});
export const appState = writable(createInitialState());
export const setAppState = (partial) => {
appState.update((state) => ({ ...state, ...partial }));
};
export const patchAppState = (updater) => {
appState.update((state) => ({ ...state, ...updater(state) }));
};
export const getAppState = () => get(appState);
export const resetAppState = (keep = {}) => {
appState.set({ ...createInitialState(), ...keep });
};
export const unreadNotificationsCount = derived(appState, ($state) =>
$state.motionNotifications.reduce((count, notification) => count + (notification.isRead ? 0 : 1), 0)
);