From f19cbe0bb27ff31ac10928d714b23920fd448330 Mon Sep 17 00:00:00 2001 From: Brandon Bothell Date: Thu, 3 Sep 2026 07:59:54 -0400 Subject: [PATCH 01/71] feat: begin rewrite of web client --- capacitor.config.js | 12 + eslint.config.mjs | 235 ++++++ eslint.config.mts | 10 +- package.json | 6 +- prisma.config.js | 11 + source/camera.ts | 6 +- tsconfig.json | 2 + web/src/hooks/useSignedUrl.ts | 60 +- web/tsconfig.json | 7 +- web2/.gitignore | 134 ++++ web2/README.md | 5 + web2/index.html | 17 + web2/package.json | 42 ++ web2/postcss.config.cjs | 14 + web2/public/ic_launcher_round.png | Bin 0 -> 60730 bytes web2/src/App.tsx | 560 ++++++++++++++ .../Component/SessionsLogoutButton.module.css | 11 + web2/src/Component/SessionsLogoutButton.tsx | 44 ++ web2/src/Component/StreamVideo.tsx | 52 ++ web2/src/Component/StreamsDisplay.tsx | 34 + web2/src/Layout/FullLayout.tsx | 88 +++ web2/src/Layout/LoginLayout.tsx | 176 +++++ web2/src/device-info.ts | 90 +++ web2/src/main.tsx | 206 ++++++ web2/src/theme.ts | 5 + web2/src/vite-env.d.ts | 1 + web2/styles.css | 5 + web2/tsconfig.app.json | 22 + web2/tsconfig.json | 14 + web2/tsconfig.node.json | 10 + web2/vite.config.ts | 7 + yarn.lock | 692 +++++++++++++++++- 32 files changed, 2548 insertions(+), 30 deletions(-) create mode 100644 capacitor.config.js create mode 100644 eslint.config.mjs create mode 100644 prisma.config.js create mode 100644 web2/.gitignore create mode 100644 web2/README.md create mode 100644 web2/index.html create mode 100644 web2/package.json create mode 100644 web2/postcss.config.cjs create mode 100644 web2/public/ic_launcher_round.png create mode 100644 web2/src/App.tsx create mode 100644 web2/src/Component/SessionsLogoutButton.module.css create mode 100644 web2/src/Component/SessionsLogoutButton.tsx create mode 100644 web2/src/Component/StreamVideo.tsx create mode 100644 web2/src/Component/StreamsDisplay.tsx create mode 100644 web2/src/Layout/FullLayout.tsx create mode 100644 web2/src/Layout/LoginLayout.tsx create mode 100644 web2/src/device-info.ts create mode 100644 web2/src/main.tsx create mode 100644 web2/src/theme.ts create mode 100644 web2/src/vite-env.d.ts create mode 100644 web2/styles.css create mode 100644 web2/tsconfig.app.json create mode 100644 web2/tsconfig.json create mode 100644 web2/tsconfig.node.json create mode 100644 web2/vite.config.ts diff --git a/capacitor.config.js b/capacitor.config.js new file mode 100644 index 0000000..2ccb209 --- /dev/null +++ b/capacitor.config.js @@ -0,0 +1,12 @@ +import '@dotenvx/dotenvx/config'; +import localConfig from './config.json'; +const config = { + appId: localConfig.appId, + appName: localConfig.appName, + webDir: './web/dist', + server: { + url: process.env.VITE_BASE_URL ?? 'http://localhost:3000', + cleartext: process.env.CAPACITOR_ENV === 'development', // Allow cleartext traffic in development + }, +}; +export default config; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..bb7fcc9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,235 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import globals from 'globals'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import tsParser from '@typescript-eslint/parser'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import { FlatCompat } from '@eslint/eslintrc'; +import unusedImports from 'eslint-plugin-unused-imports'; +import stylistic from '@stylistic/eslint-plugin'; +import js from '@eslint/js'; +import { fixupConfigRules } from '@eslint/compat'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); +export default defineConfig([ + globalIgnores([ + 'web/', + 'compiled/**/*', + '**/.pnp.*', + '.yarn/', + 'android/', + 'source/generated/', + ]), + { + extends: fixupConfigRules(compat.extends('plugin:import/recommended')), + files: [ + 'source/**/*.ts', + 'eslint.config.mts', + 'scripts/**/*.ts', + 'prisma.config.ts', + 'capacitor.config.ts', + 'web2/**/*.ts', + 'web2/**/*.tsx', + ], + plugins: { + '@stylistic': stylistic, + // @ts-ignore + '@typescript-eslint': typescriptEslint, + 'unused-imports': unusedImports, + }, + languageOptions: { + globals: { + ...globals.node, + }, + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + parserOptions: { + project: ['tsconfig.json'], + }, + }, + rules: { + 'preserve-caught-error': 'off', + 'import/no-unresolved': [0], + 'import/order': [ + 'error', + { + alphabetize: { + order: 'desc', + }, + }, + ], + 'unused-imports/no-unused-imports': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'variable', + modifiers: ['const'], + format: ['PascalCase', 'UPPER_CASE', 'camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'objectLiteralProperty', + format: [], + custom: { match: true, regex: '[a-zA-Z]*_*[a-zA-Z]+' }, + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'classProperty', + modifiers: ['static'], + format: ['UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'function', + format: ['camelCase', 'PascalCase'], + }, + { + selector: 'variable', + format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + }, + { + selector: 'enumMember', + format: ['PascalCase'], + }, + { + selector: 'import', + format: ['camelCase', 'PascalCase'], + }, + ], + '@typescript-eslint/consistent-type-assertions': 'error', + '@stylistic/member-delimiter-style': [ + 'error', + { + multiline: { + delimiter: 'none', + requireLast: true, + }, + singleline: { + delimiter: 'semi', + requireLast: false, + }, + }, + ], + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-misused-new': 'error', + '@typescript-eslint/no-unnecessary-qualifier': 'error', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/prefer-namespace-keyword': 'error', + '@stylistic/quotes': [ + 'error', + 'single', + { + avoidEscape: true, + }, + ], + '@stylistic/semi': ['error', 'never'], + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'always', + types: 'prefer-import', + lib: 'always', + }, + ], + '@stylistic/type-annotation-spacing': 'error', + '@typescript-eslint/unified-signatures': 'error', + 'arrow-spacing': 'error', + 'brace-style': ['error', '1tbs'], + 'block-spacing': ['error', 'always'], + curly: ['error', 'multi-line'], + 'eol-last': 'error', + eqeqeq: ['error', 'smart'], + 'func-call-spacing': ['error', 'never'], + 'id-blacklist': [ + 'error', + 'any', + 'Number', + 'number', + 'String', + 'string', + 'Boolean', + 'boolean', + 'Undefined', + 'undefined', + ], + 'id-match': 'error', + 'comma-dangle': 'off', + 'jsdoc/check-alignment': 'off', + 'jsdoc/check-indentation': 'off', + 'jsdoc/newline-after-description': 'off', + 'new-parens': 'error', + 'no-caller': 'error', + 'no-cond-assign': 'error', + 'no-constant-condition': [ + 'error', + { + checkLoops: false, + }, + ], + 'no-control-regex': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty': 'error', + 'no-empty-character-class': 'error', + 'no-eval': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-fallthrough': 'error', + 'no-inner-declarations': ['error', 'functions'], + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-multi-spaces': 'error', + 'no-multiple-empty-lines': 'error', + 'no-redeclare': 'error', + 'no-regex-spaces': 'error', + 'no-return-await': 'error', + 'no-sparse-arrays': 'error', + 'no-throw-literal': 'error', + 'no-trailing-spaces': [ + 'error', + { + ignoreComments: true, + }, + ], + 'no-unexpected-multiline': 'error', + 'no-unused-labels': 'error', + 'no-var': 'error', + 'object-curly-spacing': ['error', 'always'], + 'one-var': ['error', 'never'], + radix: 'off', + 'space-in-parens': ['error', 'never'], + 'spaced-comment': [ + 'error', + 'always', + { + markers: ['/'], + }, + ], + 'use-isnan': 'error', + 'valid-typeof': 'error', + }, + }, +]); diff --git a/eslint.config.mts b/eslint.config.mts index e21fb18..e575a7e 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -31,7 +31,15 @@ export default defineConfig([ { extends: fixupConfigRules(compat.extends('plugin:import/recommended')), - files: ['source/**/*.ts', 'eslint.config.mts', 'scripts/**/*.ts', 'prisma.config.ts', 'capacitor.config.ts'], + files: [ + 'source/**/*.ts', + 'eslint.config.mts', + 'scripts/**/*.ts', + 'prisma.config.ts', + 'capacitor.config.ts', + 'web2/**/*.ts', + 'web2/**/*.tsx', + ], plugins: { '@stylistic': stylistic, diff --git a/package.json b/package.json index 7729641..035f383 100644 --- a/package.json +++ b/package.json @@ -6,19 +6,21 @@ "packageManager": "yarn@4.17.0", "workspaces": [ ".", - "web" + "web", + "web2" ], "scripts": { "start": "node ./compiled/camera.js", "build": "yarn build:server & yarn build:client", "android:sync": "yarn cap sync android", "android:open": "yarn cap open android", - "dev": "concurrently --kill-others-on-fail --raw \"yarn dev:client\" \"yarn dev:server -- --host 0.0.0.0\"", + "dev": "concurrently --kill-others-on-fail --raw \"yarn dev:client2\" \"yarn dev:server -- --host 0.0.0.0\"", "build:server": "yarn build:server:safe", "build:server:safe": "copyfiles -u 1 \"source/generated/**/*\" \"compiled-temp\" && tsc --project tsconfig.build.json --outDir compiled-temp && tsx --tsconfig tsconfig.build.json scripts/safeReplace.ts", "build:server:force": "copyfiles -u 1 \"source/generated/**/*\" \"compiled\" && tsc --project tsconfig.build.json", "build:client": "dotenvx run -- yarn workspace web build", "dev:client": "dotenvx run -- yarn workspace web vite build --watch", + "dev:client2": "dotenvx run -- yarn workspace web2 vite build --watch", "dev:server": "nodemon --watch source --ext ts --exec \"tsx --tsconfig tsconfig.build.json ./source/camera.ts\"", "db:generate": "yarn prisma generate", "db:push": "yarn prisma db push", diff --git a/prisma.config.js b/prisma.config.js new file mode 100644 index 0000000..dbeeee2 --- /dev/null +++ b/prisma.config.js @@ -0,0 +1,11 @@ +import '@dotenvx/dotenvx/config'; +import { defineConfig, env } from 'prisma/config'; +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations' + }, + datasource: { + url: env('DATABASE_URL'), + }, +}); diff --git a/source/camera.ts b/source/camera.ts index f1c1954..5cd5ce8 100644 --- a/source/camera.ts +++ b/source/camera.ts @@ -856,7 +856,7 @@ const thumbnailLimiter = rateLimit({ }) // --- Static & API Routes --- -app.use(express.static(path.join(__dirname, '..', 'web', 'dist'))) +app.use(express.static(path.join(__dirname, '..', 'web2', 'dist'))) app.use( '/recordings/thumbnails', thumbnailLimiter, @@ -864,11 +864,11 @@ app.use( express.static(path.join(config.recordingsDirectory, 'thumbnails')), ) app.get(/^\/(?!hls|api|recordings|signed|sounds).*/, (req, res) => { - res.sendFile(path.join(__dirname, '..', 'web', 'dist', 'index.html')) + res.sendFile(path.join(__dirname, '..', 'web2', 'dist', 'index.html')) }) app.get(/^\/recordings(\/[^/]+)(\/[^/]+)?$/, (req, res) => { - res.sendFile(path.join(__dirname, '..', 'web', 'dist', 'index.html')) + res.sendFile(path.join(__dirname, '..', 'web2', 'dist', 'index.html')) }) app.get('/api/vapid-public-key', (req, res) => { diff --git a/tsconfig.json b/tsconfig.json index b41532d..bda0125 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,8 @@ "source/**/*.cjs", "source/**/*.mjsx", "source/**/*.cjsx", + "web2/**/*.ts", + "web2/**/*.tsx", "types/**/*.d.ts", "scripts/**/*.ts", "eslint.config.mts" diff --git a/web/src/hooks/useSignedUrl.ts b/web/src/hooks/useSignedUrl.ts index cc6699a..242be17 100644 --- a/web/src/hooks/useSignedUrl.ts +++ b/web/src/hooks/useSignedUrl.ts @@ -16,20 +16,40 @@ interface SignedUrlCache { urls: Record } +let signedUrlCacheExpirationsSorted: Array<{ + key: SignedUrlKey + expiresAt: number +}> = [] + // --- in-memory cache --- let memoryCache: SignedUrlCache | null = null +let cacheCleanInterval: number -function loadCache(): SignedUrlCache { +function getCache(): SignedUrlCache { + if (!cacheCleanInterval) { + cacheCleanInterval = setInterval(cleanupExpiredCacheEntries, 60 * 1000) + } if (memoryCache) return memoryCache + try { - const cached = JSON.parse(localStorage.getItem('signedUrlCache') || 'null') + const cached: SignedUrlCache = JSON.parse( + localStorage.getItem('signedUrlCache') || 'null', + ) + if (cached && cached.urls) { memoryCache = cached + signedUrlCacheExpirationsSorted = Object.entries(cached.urls) + .map(([key, entry]) => ({ + key: key as SignedUrlKey, + expiresAt: entry.expiresAt, + })) + .sort((a, b) => a.expiresAt - b.expiresAt) return memoryCache || { urls: {} } } } catch { // Ignore parse errors } + memoryCache = { urls: {} } return memoryCache } @@ -53,6 +73,26 @@ function getCacheKey( return `${streamId}_${type}_${filename}` } +function cleanupExpiredCacheEntries() { + const cache = getCache() + const now = Math.floor(Date.now() / 1000) + let removed = false + + for (const entry of signedUrlCacheExpirationsSorted) { + if (entry.expiresAt > now + 10) break + if (cache.urls[entry.key]) { + delete cache.urls[entry.key] + removed = true + } + } + + signedUrlCacheExpirationsSorted = signedUrlCacheExpirationsSorted.filter( + (entry) => entry.expiresAt > now + 10 && cache.urls[entry.key], + ) + + if (removed) saveCache() +} + const queuedForFetching: { [streamId: string]: { video: boolean @@ -85,7 +125,7 @@ export function useSignedUrl( useEffect(() => { let isMounted = true - const cache = loadCache() + const cache = getCache() const cacheKey = getCacheKey(filename, type, streamId) const cached = cache.urls[cacheKey] const now = Math.floor(Date.now() / 1000) @@ -108,7 +148,7 @@ export function useSignedUrl( return } - const cache = loadCache() + const cache = getCache() const cacheKey = getCacheKey(filename, type, streamId) const cached = cache.urls[cacheKey] const now = Math.floor(Date.now() / 1000) @@ -120,15 +160,15 @@ export function useSignedUrl( } if (!signedUrlsToFetch[streamId]) { -signedUrlsToFetch[streamId] = { + signedUrlsToFetch[streamId] = { thumbnail: [], video: [], } -} + } if (isMounted) { -signedUrlsToFetch[streamId][type].push(encodeURIComponent(filename)) -} + signedUrlsToFetch[streamId][type].push(encodeURIComponent(filename)) + } if (isMounted && !queuedForFetching[streamId]) { queuedForFetching[streamId] = { @@ -210,7 +250,7 @@ export function clearSignedUrlCache() { // Export function to get cache statistics export function getSignedUrlCacheStats() { - const cache = loadCache() + const cache = getCache() const now = Math.floor(Date.now() / 1000) const expiredUrls = Object.values(cache.urls).filter( @@ -226,7 +266,7 @@ export function getSignedUrlCacheStats() { // Export function to clear cache for specific stream (useful when stream is deleted) export function clearStreamCache(streamId: string) { - const cache = loadCache() + const cache = getCache() const keysToRemove = Object.keys(cache.urls).filter((key) => key.startsWith(`${streamId}_`), ) as Array<`${string}_video_${string}` | `${string}_thumbnail_${string}`> diff --git a/web/tsconfig.json b/web/tsconfig.json index 1ffef60..fe6831f 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,5 +1,10 @@ { - "files": [], + "files": [ + "../eslint.config.mts", + "../prisma.config.ts", + "../capacitor.config.ts" + ], + "include": ["../web2/src"], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } diff --git a/web2/.gitignore b/web2/.gitignore new file mode 100644 index 0000000..f2fa5d2 --- /dev/null +++ b/web2/.gitignore @@ -0,0 +1,134 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +.DS_Store + +llms* diff --git a/web2/README.md b/web2/README.md new file mode 100644 index 0000000..0d43a1d --- /dev/null +++ b/web2/README.md @@ -0,0 +1,5 @@ +# Mantine Vite template + +Get started with the template by clicking `Use this template` button on the top of the page. + +[Documentation](https://mantine.dev/guides/vite/) diff --git a/web2/index.html b/web2/index.html new file mode 100644 index 0000000..60431d0 --- /dev/null +++ b/web2/index.html @@ -0,0 +1,17 @@ + + + + + + + + Gander + + +
+ + + diff --git a/web2/package.json b/web2/package.json new file mode 100644 index 0000000..d02068c --- /dev/null +++ b/web2/package.json @@ -0,0 +1,42 @@ +{ + "name": "web2", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "tsc && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@gfazioli/mantine-video": "1.2.1", + "@mantine/carousel": "9.6.0", + "@mantine/core": "9.6.0", + "@mantine/dates": "9.6.0", + "@mantine/form": "9.6.0", + "@mantine/hooks": "9.6.0", + "@mantine/lightbox": "9.6.0", + "@mantine/modals": "9.6.0", + "@mantine/notifications": "9.6.0", + "@mantine/nprogress": "9.6.0", + "@tabler/icons-react": "3.46.0", + "dayjs": "1.11.23", + "embla-carousel": "8.6.0", + "embla-carousel-react": "8.6.0", + "hls.js": "1.7.1", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-icons": "5.7.0" + }, + "devDependencies": { + "@types/react": "19.2.18", + "@types/react-dom": "19.2.5", + "@vitejs/plugin-react": "6.1.1", + "postcss": "8.5.26", + "postcss-preset-mantine": "1.18.0", + "postcss-simple-vars": "7.0.1", + "typescript": "~6.0.3", + "vite": "8.0.16" + } +} diff --git a/web2/postcss.config.cjs b/web2/postcss.config.cjs new file mode 100644 index 0000000..e817f56 --- /dev/null +++ b/web2/postcss.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + "postcss-preset-mantine": {}, + "postcss-simple-vars": { + variables: { + "mantine-breakpoint-xs": "36em", + "mantine-breakpoint-sm": "48em", + "mantine-breakpoint-md": "62em", + "mantine-breakpoint-lg": "75em", + "mantine-breakpoint-xl": "88em", + }, + }, + }, +}; diff --git a/web2/public/ic_launcher_round.png b/web2/public/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..c82961ae112e120d3a868d3e7708d712a81792a0 GIT binary patch literal 60730 zcmce+bzEEPwl*5HNO5-v7TmSCI|O%%1St-|p?GoE0>$0k-K9l~w73;0MO%u)P1jm` zpMAc2zI(s>&&`jJIp24TXZRSInKMd5O&$Y{6b%3XU??icXu>}G|GZIFmgEZslwNVfS`)fl&hhBI4dI=9cyl zD76K|#?DC;c-GYiq_(pX1?ussa;myWLu~C7eBB^gzG~W*zV?@5oX3s)F+{AZa1Nc|TH)Ls-Q`9~nNzN!Ybw6hz8T8N#C&61Oon_57K z-I|Aw7h-M6O3ls5&BMXP#lb1W#>Fkn%PGt)K>g1P2ovXKWi6~JBlnLm*qJEM77BF{ z=HT%1@?!VmVRv@3;ouSy65`F197)>vvYykIXhAR zK{U5;_JE24VXFRRgCrE{W@qtlWG8oa^S@R+{xGn^T*qN<<<7yy&iTi&l1E5hWbZ8cbI~f=1!jGe{n;+U7X#Z|Khc>{I?_*4>yOuEU>cV zfH*)LVf=sOaQ#~j%-8=W{-f4k5&WZ&x4FxIQ^>{E8S3nA>-@hV|M&KPV}hZps{TLY zJ39Vb7k8+v=RfAbtot9eao6^7fpBO-+?_q#EFrR0c7vEho!zvZogGAh(tmtS4bp@-SwY+&ZcNlx z&X%GwlJahHK9&w_((cqA?hrRooj(hFPA-DpywnzM=1x|kQh!!_J?vz;!PHQQql*J9 zlA`~C50Uw60hXkEY@GaT+}zq+T*9zF9!@q+USUqoze!b{t?aCQ{zp=NVIH3UMhYtt zD|4v%|10soBL?%4gR>h<1WYlWzvQW^3M)FfL(QEmA&N4hK$tVw?d+_Cx%kb6ECqP1 z*&ssPLTvmHUI8{CZa&y4zd6K;A69G-9)W-H%Q#zl{K>XI{C}4(%waH$|B}kXZDqm3 zFT~Hr#l_FXCdkEW#U?0ZVa~?QCB!Ev0O1$p;kEh~n!1}Etb>?4{3q%kt*l^(e0)6C z-29xDFemU?z)GB#o6UlsPms-8kl)J6!b*UbpV#6~nf_ga!je|*|5Uve#Py#e2RrJ2 zcK?bw1lFSdD#`JD5A!zyif)1+g~waDW2Ebe#U8x1zR!xZBw{QL|BV{f+Km z_b)krgK+!@H0;pw@1*|+{XbnKCiN$wVf{_Y8EOl0aQI*0{!#nCVf};rFtIz##uCj(-)_e^$%iV%(jr zp05J{WAdh6d4MdM%1j+&c~osSx%X9u&~%bn?8x>?%7{QW0QIQ|OTpQyod=>MZJ z|7Yd&H|YP5#{6dv{;%=-Ywtg1-M>-^HbDOz{(E%8PX0aoVI5Nx=ms0@HPf5-0Ki_S zqKu@r_tzgiKHX3~or6aczcZfC-UmnT$3FfnBW8q_|fidT{cLOJI1rKEVpVGwjq-TOJ1AS1%mr*7|%N|xYc4f}tC+`QI z;S8RiMYjT;{Rr}p!f{@st;Aki_S4NGR&L=*`1J4c*#r^y z{o6hF&t4r2vQUa}4EUjMJZ@qHF*-jV8lgELSqD3Y zdH0);tP&{#SrEoh*X0(Sdu?CQ28PI9Paw(-?m4x~W=h7<1mMPLy|O=Tiv!uGlgI}X z$mQLfab*X$wRrc*_~9=69u`L6TN5<5@A5SzOiC3`2M2(IRPj5ieRrRiH^eTlToBG@ zQ!*}KIIT`JR0qaeq`_5b$x{*xA_bWFc?a;5RT?%R$;mfEX{c?fd||)CRD5ksG9nN+ zZY4|R1A~S>bgyQZU900AEWMR|N67=-arl{=hn0tJvEI{PChAYpj(KLZsirSR0(fps ze-aAu?|Vl@Ac&5zVUHO1TgqHwIEgS`%D!{*^-H?K2A|+EfCw@MsJpzLvHxK_PO?GuC+cGZ9^2^)-mq)IX}tb~t=NYLcxdFuEN#oT)iA zuj2gGh9nhJO2P7?IKgl@$D%OQf&Av}#nb&1@J@Iupx@?+&E&0+PVKETJn|JK){Gud zoEA7kgo85|_MJ9f%HO)60a#}*g+ko02?#l) z-~^oqc|3lna@zK3V4lrPh#$X*HoLz8oYR~=W(U(g2S!Lfvmy6q_NyCgkM(F0k(MC$ zRdvfn74{Jw$kmgMqN{$A!A5o5{FZAPar+H1BAxu~CT{cKn?N6Ca-7RIpG1g5`ysBU z>0^(B&^@azNU*V6VWid8Te*@~j;XRiLqH>juSKws61B*Xs}dtk0j!Rfm6nM^|Cv^G zgxEr=xSbsBxItDe$!2c}Tz+1C9}o*yxVKE$^XoX~$#U}gVmSZ2AYu;rNo{L=atZ^e zj@pKE{BVb?n>0070ae z_ZjHCz-b)48!N7ISO|#5yi-meU8+bD^D>1gwu~z!E%H6Db#XFhGDGYlFCXGbSYf@D z!8RLHDkF2#)l6Lg1r z>dt=h#mC?K8XQp`IlHvwADN|wjP1jG0 zN_#dqVACqN50Xp;Ho#^9UiSz&1!iO|Fw_sJjne2_>qyAGo*((I-0Hc#j|=ke4+jrS zpBcm+yZWSY)xqn6W64&;M%2@@f(PWus;fsHXqTohQudpPz8#UT$Xi)Vw?8oDJU!SG zckCJKc9(hB;@D7t;shmkr7q1FSj3F;&wVF9SD)v8M4vW_c8W1=3*XDl0+BY@u!29v zX|H^aT;bQ;j-3O0>fnvDtuKbOw(ODm8q&1(w{rvhQ2_fxM$Qp$&<1w9$=uj2sVk2^<>b09>eX7=cX;F9#87&4K{6f3?&6`8kl{n?9jD%$z+QPjLb2&o7! zRt3c-LCSN6a^pJXIeg5iC%)kVy}^BfmBULhZD3(QSatVDQ@O)>6#l#J<)LpT{^?ry zLY_yUnz7&5*{L-dd1F2tJ~XTBogqW|zuDOnJ@G3%ydlda)W==DkJpms!E5N0aY$&a zt)THPq;=EyiPhpSXk;GKvPPpmlkphFB4Pu!bofh2HSF_lxwXTGm(NU_5k|}N`5N-= zj7llJFCQu+{r^miK1u*4&Q(!A8Fq_wRQ_EY!<-;nWYBLZws~z9rjDky?)vfN1UA>} z3;u+!q!WiIw7VV>neXE>FI;9`J!fI{oVGL*QJFj3r*U#m4$cEdnKY?W$ma+uZE_SL z3xvSRJxuP^p{q`XE_t;VOx%LKlk>R&GHH<}GyZ4?qStROTbnf~7Gp-*k!l zZfr3sTSimo{K%K{zNMv5#POvo`xgV(ubjD!8NDT*jJy5VrceA}&3^;|Ao66fbsb!l ziko`vO|YFR$s}pXwE<43lF^Z7Y4g85hhL^L**o==G)mxJ&$OyG*H0tIlg;5luem3> z+2VKvUK`)P&7b}ul#xO&tb6gHWAQs^<((*uNT%dT7~XGdd6$@m z!$Wj)=$kk3cUUKVjKQdxQt-;H=V;`_dM}fCIBFAUGvG8@PjjZa-Jchty0>Am&cFy-&i_oN{VW9v_r{o{ zp2T62FDm3>fV}OP%D`kh58o&BY7^BSC8o+79?o}s=b3)-i9imC(r2^f;gO>+exKIt zMN>AL%?D*YyLifVb@FhjuNi`!%xHxdOqHvig=IEeX$(p)VL9m#+(NiGMLolmQvzbu z)Nc--zJ#wCqhg2;`&rkWvyl7T6Dz(CB;;bH&}VQb$qIRctow?aZ43Iv7M(cbiZF(( zo5P?j;sgqQ)E@D1JEm4Uhwl-wsaRxiW@{>8Hf}IaWoc7$zg;3JwdXU|t#52p(Bpk? zo7yhXbq>D~;*qL|-2G%$Bxegy%C3h97 zDM9K?5S7Z2$#I>4{CLW>;R@I<(Kp$ho%1oQmEXa)gA2a_*kkS=O&blP^K%?}bb-h^ z`yP4eIncVjRq(a+V>CnfbPRh!P9MzY3y0#5aB>Bf_a-UG1Os*vzuJ(SnOe)*#iEyC zFQv{_7%kft$wa?<6DGi9W41p4>&%;r0PNI^&C46JBcgVYMxi9W+<^d=9nDStHiQ<(TS8i0bZY6KF3UYYwkRwjYbmUbb%k zbLD9)irFp*JS7Fju5LQ?4gdC0?&F@El@+{BO#y8v?!Z@#ys(i|m++BqLQ910z!wLJ zYde?lRc?(XMNrDM2$Ub~zeJ{eHhyOMP3u$_XIZ{TFgKLFeSQSgLZ`#|c12~=wSHYT zF4ya-7#+Y8N;6t8Ub;>KfOn&W6uqw-^Lvtpe>mO7G3y&rne(ol5q}|DGm&9)K(-qG z?gwDw$Q@;f*KxE*w{%X=%#gY_n^_>N0aUeIVeo5(wNorgoB2?`C=C&qb*+!njgSGW z$vr`{pcaK~nxA{M)NP_WtPzU@gp_{x#beVk9D6?wj6aQ*iH18^q|!#jr?hq24o*9x z6P4pFfq!uqTV;F|LxKn86rEEZqwNSv%50m&Yf_#O5DqXEM7~_lEeW{cOo4XzALGx6 z2jJ;BZOWLu`bLyKQO%5g_Lk>Ujn=r3jk5k?xR&Ie4(4qS?1Cc?8xZ+?J$mfL5_>v= zR!5MeM_gAyTE4_PW-{u5MgEOP-j}>k&e8Xsp=$kGlBCEtIF>bx&N4r8;Q}~!-JgF3 zx<9^pLKIY?|Dv?)>)$dxk|7=w9&Raexgro{znDCGhFyn)c!E~lR^F_{%_lF+A4N(M zI!Y{DgDZ<%LQtr!ABn97uywY-(t6bs@Sp-MC*( zk>hivlXD@uDJ{k|xhsD#anL2B+rPhJn(^`au#()BRHs~`pXC_vPAyI_S{czXaS|cF z=5<;@B(G>TPv8URqJNaKwY>MyZP#tD30YM4{)$or>v5q!B7fm5iJE~+NPD9K`?ElB zl5dUo^X|^$AZ+A+c+qdvqA6|@S4zaB$u%rAJULr!*ExNeCVGE!6U^VX%JAvPVDy+Y z?|OU$d=cX6!W#9Z8{AM5)FpP#BzAEp;C3@WhRt6nUkVat(_Y&ev=NC}47Xf+{L0Y#v$Vu#uF}+n&jx|F)*ZTQ7`a zzB6bAA5#y1Wi90A${_H)>?&pLvc=mo%lgKT@jp2%mKW7+zD<@Tt4uf6*MDpX_i;$V z`J&;xXIhU-Sm;q8!4DalGKh)99=mu=be4xN`IB5wMshd*NO1e+f&0@2?#u>w_twT? zzw(1oRJY~RAv_7ZFso?1^+v|It4L_Gtz(x<^&3Bk@PLTSWBmFg)uAoJ{1}``I zt!5InwW@VX^$g{=v=4)BzYb|e=?*%w8E)sA+T1T%xxTK`r>C=x%6p@66YPm}K5PPM&WAhRYCI6( z_#D6-!-mjr3OrN?6_usUN1_mvl#dZ8Y}M{6`@cDpbTTH;&Ca${(^ae~OJdtn99In+ z0Za0<@JSi0+`kAMdJ%YnQEi1K*-d7mSU)-FCi!`#U(56tJf^rO71qX+PVyWBa4WT_ z4olx}!L3^m3JHT+5b|+LX*%auswz~L7e~kA#e!)7+EK<=UDeaOJwrY*9IP9{-7OHw z(`*A>%gsFHImCib_VPew4nECsQ#Gdn08(Y4uFT zy>;9l^vO1l!7`Lp6r+=*n&DVTeaB_8mvRSdb+9XcM#P|>@RLvZAhBYdFr0zN+4A8- zx}DxMLbDb3AhH9Bm_Hk|y?BVOO>$9FXvdZf$ zBiG4-8hSH>5fz7RJC1vE%ymDO>%hmU&shXH@eRy~yM_Q1jeJldX!P@Fw9#QTM22^Cb>i!<&$-^3g}W z57IOa`H@g$zy2wE(6Iepy})&}1By1&U@2c8F6}2t)JoUl(~UNeuR*8Sh+G!^@`fm6 zEDa^;ce;MWsJ>r`to)Fx(d-wm$g=q5o4OgnQ2Xhx@y-T>hCf#nW@S+x;K+~HUHSkA zYw`onu*-~kcEF8u;YXuXR1|elZtXWX5x$~gk2hRmO@{_?_*X|vK*(%Q?06_a_{eT5 zw&HN>r`LqrO??;0wSiN9K|1A5a=3Aq`5$PFPI2ST_BzC$G*5f3r$pQqFtiy*r6;wx z>3V)k(sFKn+uQc8<5!d)jUp{l@IW+CjH=|LQv`!!qcN1oWJokf87Y$_9Ecy-md7qk z6m%xFhr8-`?+e2;rtta+XkPo*$$w)FcfVN+Y0`i)5gr_oiv%h18O?~Mn9Ou0C&h4B zN2T5l#mpXI-@;!AiKgxLgcvI%3rZ9%%`Xq!CiB|jxZ&GbN!_(H$6n;qO3UM$G7%OyuR=uid6K>IRdJo%Sv3{6!N2e)@7| zb6%mQwt&uNy$!1zcTQ~FuUJf{i}IddUI$$6Y740B&}t3lJnaF_w@M^{(#BXZ;uQ(N zG=^lEOeLfpVv1`7YEndi5;FOFvA4C#r>a!7&`>S7Zj7Tjz(E^CLNJ2kvQY!mBLw!n zjfMKmJUZ3^5>!_SCAM&}ApOOVeieL@DvIO`0~$W}(s;aIi^w~04Li2EG)d2^~0zq4-D zKgSdNwf`^awd*hTQDtMZkN!QIvjbRff}W;NhVd3FW#Yh4kFfHkVeR8`L8*EC9YhKw zLu2|X{uc}R0#or3(uzZ%YCeF^u?}Q}V z4k7Lu2{fEk$9#c)^`yKR#OS`xc$;=B-}jEY>$N|=t5i;gKM9q&-q&Ix(Qu-^Zy6oe z2xq(T*UwC!z6j~sqY7WkjQ48|@47Od$7uVsm=tqfk%9y8bsR02JyZ-O%TSYxZv>@| zP>T2_bNu1U3w-m{>yC_yUfxZy7t8FBtL2|#qM?VQiy~5OY3Z%tim@ejCy@FWn-@@F z%gQBP>x=qh>!xR}29O%%dLk_l~qp8)>5Q(r-ZLOc_Uao z+a7Dzwbw2`&ALgNAhG(uW>u!)7krXtV1|7m4zx20Ll!z!&KBWL{*cNgt~63%MX+n^ zFzU3-;o<5_z#@Lm{>53aywf(8 z%tndA8&+~8ct~z*=3rqphDUIj8DgE_!Ss$< z|JkqScn*yf?LhVJJ9@^D1YX)vT)mghUDsSsI~oyzUW##|U(n)xev7+_SIHk-m=QI` zm-Vb!JoTIBh@Mh2bUak_3KK)F%C~WIVc7B>%z5P!0NNDZJ z!%&$@_eNYnRt=;vc*+*Sk4C{5xdg&1iiM*H<{ogCS6u^$}f>hCiJF(dv+* zXyurh&H^4>P2KNlMSgy)!bYGl0U*i$78%9)jT@VcktmUaD^GUb84{<}U5*VX1Sco@ z$)@ioYJWNB;nyqBYV!7d44*cP2oADLp|{o`eNv>Q60TSmw1xOWotRhAeYAb zqHcMfEEVk(9mNpUup(tsf#mWh{!v-b{xd7Zp4#1q#B`*Z+Mz0JL~qno8ym?qEDd&O zZ_5B7X&rhbxu!myz!s|o+adX7&KmwOnav!3*{b2U4YJ;_+po)CJWqvk9z~b?ev*6-GB5RqU3lN~66a zyWX^sX*3H2)WlxvFMj~_N1oA}l?>Q^V@4JDRCk^&AlAPWSJpt! zzk<29{xXtNF=brukby)n+$$uJx-vw7A@a30K4B73XCX}+r|3j_;Iro2Lp`&PqJh8Q zY6CxUyq2>m5DT9{(jLfqUcCd%GXH{?v4dJAf~2xhC>Rexk|U_NdP#J$7UA)@k+|Ja zv!kz5ksYxKG6n{yH;Jr*(7?#qxTw5{MN2P=$u4it+)tGi_%)cNwvSDY6f5x2 z9jcFRf%qE+l1clJ$tE!jO>b-(Y4dKzLGWJgmkAs~GA8GT9on@fuiWLHE#}rwYW2_O z=58M=S~m=~-21hj0+ZlT-7u8IKr;FiMXCuHSIh}ELVcGbU5ccuLSYOB#n9lBzLo9}>ms;V9X=(dSz*mz`PcCIoPB+^Fka-rS2#m6=|k&*sp) ziQjf_Nq4?B!I-4z2&Dhu9tmF~YAA1>u_$cpbddNF*c-}>2CYsikH|!vLxxyXM&_^?f;H;%phSeWSV@h z;B~w|_>C_@7TuD0588*7#<6?{xal4A1pM4VUug2|8mDq={l}ub2VkiwO;%Pu7r&gl zb;#WhI@caWL$V@nv1nynH<4+=6=0bSn*pkOC9c#JB(yQvc zcy~ChGs90yJ~F@DGbWi}`ktCho6^tGiS4eynVk`HZx^of!H z3hcI7kn}Q(HZv3dani}=Op>!$e?fQ98zo`GlKEF z93t4kuakE1;}S%*ov)^I=zVzqL)7Zn;Nz))}2MR(M)aLM1)xw-q zlBZ9D8eCYF=|^>5mL?^t#dEjVA;eL*|L|S1{;WrJ;B5Ag*Kf~k#E;BsqR5#~F$uY^ zh)m{7nXa$r#g^REL_$iEBnj6oc=aZRQus_$N2AW3+3sVcs|sDT5+8juZQ(dJsx4%o zZL(Q@`$B(GF7fwG)*u5xv}-6E>(Vz?qG*WCAvM0OZ>5w#mW0447cHKGsE^xy`Z6rB&q`LNUp`mXsz5NUi?F-GaM6C+y#5WYzvrD~|7q*WABw zT3ctXn*OteG@q_5FA(j(t$NC}mii|e%UiZBWYngYq>}IothQ+ullkNvG+*PFp~0pz zNghP{)4GDKzL~9+1>bq-EXg>*NNFtoUDPR<;h3ZdMnHwpEJ<4BY8JbB2-u`l$xz9v z!Ubd#TKc6--#%##7ZUHVfjxpsTclwnT0Gt$i=J%*)f28}Z>HW%m`r!=5!vb~*C=LT z5+G3baLPH|hFzX6>TwnDP>TPiM&)X#NoD+{*mjMxcI5PYfC0ENQ3=ioY_au;(hAWe zrR7|AxJ=tsei)!g;1q2MMa6pmy}$#*nZg3mzQAQ0aoi-BNZRArsq(-y>dBh@Di-{(F zsC(sN7~553#hb|++Yd>GQfz$QZIhj3h#o>vEe(jtg;s#x;~^?BPYhM%-x$LNIG!c*)B4M zFH23TL|t;lw%kDIiqy`@sa}oM%Dl9#x)Nz&GeGdmAwEIW^~X^wk9J82F_sB-8t^mc zG~O*hbBla3R40Td3#1sS8dnW25->17{wVnS>6IdtKEtaAPO95%ESEW+S}QlJ9N(`P zk=aYfY9W_~0nwc7lTS@ZLHDPNfDBUlKDO!kxUj6a6%BRo72&t{%A0fv44HPZQoTwR zz*w!IkugdW{6wS~o;oz*FR>f040GRv=A)P=^p$GTv32;07)I!HK5GxYUwC(HdMkJO zG#nAtmW#t4*%mxtko;AI@&qw%Gw)wFmoRJwflOP%K5mJ$NSe90ManKt7p(@VCRDivWnfxY z8(qJ(E#gXpHwR0VF^7rgUp>eQ2 z)}dy7a(Hujw-MR*7>Q4wpaZaMJW~zsh;)fjVk~c>ehO>LtfESRBZ?Enjo1!f^5@w& zQcY!rN{?8jiAvg!CuNsKYA{B_{0_PtKVNUmn_Xz;3+@@km&ONfKIfs5Sa;4oZU(`wcq`^;nDVif-)hmo$x%I9%aS6Gg>nDh8HwM^Dn#yo$ zm^{H%=!;b8s6xK0Vpm&*VTjJp1%1O+q)DTLstb%yVE8y4mmKh$mNoFwS*VLds~x48 zX3CG1mE7FbE>$pUR(hmM*g*XvK&j#xrTgBD=jEL_5|1fUb+jEov1T$4HpbM&Hz2;p zt@FXGV={zulxEn1ytviR)t`p6RI(0}xKx_dwBsU~aTG~YhTqXuQZwwVndo4?ePHpw zSvE!3C5vPs=x)P@6jk-v-l0!Kjl_)j#L)~2%U3l?;SQW);Qd(P{PaVg;Kc2#L5x#@ zi)|`#YTN!Md2SUMHeJb}ASwzBaonk=(ZNg|f=Ca&M0!`mgWAJRmIlNOPF>=9b+^1IFby5vvC{3_JU5&>MrWhcypSs9>lsW|iN}rm%v4Bh5(Q?K@`>Y6uLtSMBo$d`C1zHeuLzsi+;(E^ ziru#52!JgMEL;qZ6qRg^TURNBKVZ6&=RI^rP}`q9=J}lUp@=-xaRJ`yZ@owbbt9Mg z#V*;yoEHc+IAHiRE#amUo?M+*j%yPVh|!JJFpB;3db? z)4|Il)Vq;p`qyW@v&HPRGrEU{Q{F8TJPaaZ-?~IRwCIa-H^gyg`-NwP?1l~RY{)quu-lGmu-_9{^WbFp8BHK=txw;)MKZ;66b zYk4c$c#A?TCx6x&QCahm+Gz37G&)MXtHLQv3_Y%I&(YMMaPcA1P{^{#qRo602jU-Y zkjzeG|1=%XxpaKN_=*NMBs zhxc+m>TNwLr{t#iy0RrWX%O3>ImNcOzIdK~S{p?gH)JNIlGgP%L%TXC#4WlA;}x3u zmsa{?VA4t55hCrLu@R=%OqZFpd@HO&*My3e#MzuG?Bw6JMPtaF9m{(6d5u30U%NF= zx>$rZHSrN|&V<~58k{Fx7g8ZF>LO80PSk$0Vi;drTs}J)Iv*zJ8qFGi6!-ayVM6Bf z@%3!iwzR`4uiPq~f~kf*5&oWbw#Vpb)-V{qxS$nuCL}wTXF-MWJ(vbyNpybP@BCH| zLE;fl3GgD2#cRcok(?Yko5?rHSz~x3gDqw;W-yo#ZhfjFzMAB!Qw8k~2ZkQH3sO zGT`R)YkP`QFt7fvuVEBUf+%Vq?@RVD>{=M6$g?nv*~8jXb>Iu53P~%j@pY39c87i5 z9eI){9y@$*?y!8qp)@ua`OyvF5mT&Nm2;2-BAF^n3|XCA{jMO|h*0BNCr)FY7cA6K zoKp;8tUt}Bi*8sm>zc|h(R~+ixvY1RFKQ`h9I$5;9J)Nj9CFzN(3X zjo)y+PqlreVr|(#Zv6}MyQ$7)T6|oZog+_OSJ~0{I7?&iuex&b@<+{j29u54O-awR z?pnxQy$sQVwUn}wdSq?^aa?ae`%DP0%aMtv@9*svD&{HotVpja#{B}A8?Np3_}sv= zcf#^5$tiW76>{(7S|&z^98bNsV#rtD3Lm{wt`EodI=Emi!frXuA=8IfsMq0DjE&k& z?3G2Puyfmw%mFrXO*M?h^RAe3k^671*5^I?wK~2xAIa1_Ag@(xF@?BUq|PPbkj5dW zDIqzkyk5Y(&lOe&>&F^UJxC*6-UpQh61^IDF2ABepjt!;M}2xv+c^pbcRwyg2=;@F zDpi5mmT}Z##{OrbN-U}Hcy=m$*37|V;yx4x_Eg-ygq6CCZ?xwKq*FUq>^7Cr4H@MSoQa`svXv_Yp8Dz{+xb&U8<25CeM5Y5MAW^%L#5{T&OT}zfD*vvA=PDFQ$dt#(`;(` z#Y|$!e3zJ+OIY2u*)QhGBza-4VIEnj?7 zY^P)#9c6`$Xixs*>DYjg1;fmY3D^hd-tS6*f80fxbC$7Ub;~vO>6X!!V>Z5?h3oCU z7Ia_r#MwtO6fwCdE}T_DI!t6121e)OF} zavLV}$t??79e`uBka+l2on~gQXOt3rojSYhNblm&TBYHDz~7#0HsZr}s{)PDMc8=x zj`_M(JYr(gD~Bj?a%qM&N+p^!Bn}|!^(X2o&=3{HRCZqysR{CjnAxX)hQHv&F4 z^vz?0=HBi#xs5hL2ej7|g+hF?CWE$v@Cm=sqIf&d5g6uWQcAffPF4&eUOjzZU={Oc zsT7V-CL5+r(f&>VUF2->N&jLxO7%Ed2PQYMf*$aHWtJa~u+V`@_{eC1RO^=IuF^ZU zn&Wc(mPm;8IQzOIGb9>cm0$vi1;U|aIV=pJ#wJTNB99-$ndfreauPR*^`PP^YBh@M zU@w70^*@IWdP$^V#=i{3%jd|$_s}$xNc-NRsb@sGf%zr9YIb{30{B`-hCRzEbt?su ztC?-}%O(3&?}d);bay0lm%MdZZWRE=zaV`m2O=nj#Q>ufK+L(|x&~P#535wL)_IHg zz3AT5vg@}}J7Hk(#{-cawnfA6i3wC#n<-yDhCLu@M^CS)RY*q@y<5QRa#H#0=kwE` zo-`l$*1aaxm+U<6X@9~6=zKOq%N(oWjzd6kQ7ccmz1><5aZ4TlwnxuEbu9RLnZInK zT(hbS-+KofY7kDgd8J2#p#b!^jJ5T}TLwAS$FXF)~3Uko|WVm zxShqoNbX->r$C$gM?`r8?~SbOyk0do!q;iWw$b%2kFig(I|aSIk7m~qmQkSUS|%-7 z%ugbK&wsupou(3d_o4gwYO4^F&9mg!*74J6U;1TgeHmsjXa4Dx9W$gpA2IypXl|}I z#l0tcE%{VsH`c4pr9_#-VF|yepRK&MOTzw71KEear;A(fjSb(^yNWhjXf#F531ShH zl@mp`Dvg6zDj0T?-Jgv%!=Ari<+PY*sac$3svHrT)aYcAQ`t0s$=9vXtkNylWeKgV zBl-HckFW9b{P_EeVN}MEDP$S*A~X7`KbvFNUk~k}uvDFFMHpBdG^$Nb6fxCWC2a;)9tE<~7;I#XXU+8Yuq$ zCt65jwM^xj1|SvB3Lc@^AJY^s8-=cH^`z0gb5thzqT})UyuJAIv`A=SJzWP?@oPCU zH#BWJZMuOj%DFYr)duc0>_K1Ro_*w2$BmDLQP^(h=J66WrZ#RCIZ4)`z6R%|cHz9L z%cSAbqNy$6Cw(Pq7h&IsftOBOS%HRBTd$bm9{i?i#nEV%aKEr)uHg9zB`Pme~Pj@9}22b;P=yTkb`E zL!e!QJpF=VF0ph8?#m=ssk~Ng?+^dcH#+GV835x4iwPZSa zb$KlKd@Of42xd1Sz#hG`Z%duk?S(j=4m^Ny>sgv_?7V{0|DkU6_HI$hfR_cwST{dS zqhj$8Dh#4BuDLG@AqZ#a?^#-&1uHEbX*%=Y_7;y`-JfkYoQlvMn8vDGrm{_Hq3P-p zf~*SGuhFS_6n@Z0$ve-ilHD`tKj{0gSanmjo*SxX^9uUKJbc18PMtz<@(P`z zRe(PcpG2)m?(~w9f<#X4yNq1gseLpXlm9-4`0(Iv8#W&))RI%HNydoZ?YGv9dFaf| zs@@~e$u0@HOIOu^?I*iq)qi$@@@0~{X#u0Q;}R%0A6YUoGM}chl+i}dRGU|t(=O4( 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 + const [_signedUrlCache, setSignedUrlCache] = useLocalStorage<{ + [cacheKey: string]: { + url: string + expiresAt: number + streamId: string + filename: string + type: 'video' | 'thumbnail' + lastAccessed: number + } + }>({ key: 'signedUrlCache', defaultValue: {} }) + const [_signedLiveThumbUrls, setSignedLiveThumbUrls] = useLocalStorage<{ + [streamId: string]: { url: string; expires: number } + }>({ key: 'signedLiveThumbUrls', defaultValue: {} }) + const [_cachedRecordings, setCachedRecordings] = useLocalStorage<{ + [streamId: string]: RecordingType[] + }>({ key: 'cachedRecordings', defaultValue: {} }) + const [_totalRecordings, setTotalRecordings] = useLocalStorage<{ + [streamId: string]: number + }>({ key: 'totalRecordings', defaultValue: {} }) + const [_cachedRecordingRanges, setCachedRecordingRanges] = useLocalStorage<{ + [streamId: string]: Array<{ from: string; to: string }> + }>({ key: 'cachedRecordingRanges', defaultValue: {} }) + const [_cachedPages, setCachedPages] = useLocalStorage<{ + [streamId: string]: number[] + }>({ key: 'cachedPages', defaultValue: {} }) + const [knownSessions, setKnownSessions] = useLocalStorage({ + key: 'knownSessionIds', + defaultValue: [], + }) + const [showSessionMonitor, setShowSessionMonitor] = useState(false) + const [hasCheckedSessions, setHasCheckedSessions] = useState(false) + const [sessions, setSessions] = useState<(Session & TrustedDevice)[]>([]) + + // Helper: Try to refresh token + const tryRefreshToken = async (): Promise => { + debugLog('=== STARTING TOKEN REFRESH ===') + + try { + debugLog('Checking for API key in localStorage') + if (localStorage.getItem('ak')) { + debugLog('API key found in localStorage, skipping token refresh') + setAuthenticated(true) + return true + } + } catch { + debugLog( + 'Error occurred while checking for API key in localStorage', + 'error', + ) + } + + try { + debugLog('Attempting to get refresh token from storage') + + const rtExists = (await cookieStore.get('_rtexists'))?.value + if (!rtExists) { + debugLog( + `No refresh token available for refresh attempt, _rtexists: '${rtExists}'`, + 'warn', + ) + setAuthenticated(false) + return false + } + + debugLog('Refresh token found, preparing refresh request') + + // Check if another tab is already refreshing + const refreshInProgress = localStorage.getItem('tokenRefreshInProgress') + if (refreshInProgress) { + const startTime = parseInt(refreshInProgress) + const timeSinceStart = Date.now() - startTime + + if (timeSinceStart < 5000) { + debugLog( + `Another tab is refreshing (${timeSinceStart}ms ago), waiting...`, + ) + + return new Promise((resolve) => { + let attempts = 0 + const maxAttempts = 50 // 5 seconds max wait + const tokenChannel = new BroadcastChannel('tokenUpdates') + + const handleTokenUpdate = (event: MessageEvent) => { + if (event.data.type === 'TOKEN_UPDATED') { + debugLog( + 'Another tab successfully refreshed token via broadcast', + ) + setAuthenticated(true) + tokenChannel.close() + resolve(true) + } + } + + tokenChannel.addEventListener('message', handleTokenUpdate) + + const checkComplete = () => { + attempts++ + const stillInProgress = localStorage.getItem( + 'tokenRefreshInProgress', + ) + const newToken = localStorage.getItem('jwt') + + if (!stillInProgress && newToken) { + debugLog( + 'Another tab successfully refreshed token via localStorage check', + ) + setAuthenticated(true) + tokenChannel.close() + resolve(true) + } else if (!stillInProgress || attempts >= maxAttempts) { + debugLog( + 'Token refresh failed or timed out in other tab', + 'error', + ) + setAuthenticated(false) + tokenChannel.close() + resolve(false) + } else { + setTimeout(checkComplete, 100) + } + } + setTimeout(checkComplete, 100) + }) + } else { + debugLog( + `Stale refresh in progress flag detected (${timeSinceStart}ms old), clearing and proceeding`, + ) + localStorage.removeItem('tokenRefreshInProgress') + } + } + + // Mark refresh as in progress + localStorage.setItem('tokenRefreshInProgress', Date.now().toString()) + + const deviceInfo = await getDeviceFingerprint() + const data = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ deviceInfo }), + } + + const refreshStartTime = Date.now() + // The current refresh token will be sent in the http only cookie + const res = await Promise.race([ + fetch(`${API_BASE}/api/refresh-token`, data), + new Promise((_, reject) => + setTimeout( + () => reject(new Error('Token refresh request timeout')), + 15000, + ), + ), + ]) + + const requestDuration = Date.now() - refreshStartTime + debugLog( + `Refresh token response received after ${requestDuration}ms, status: ${res.status}`, + ) + + if (res.status === 200) { + const data = await res.json() + if (data && data.token && data.refreshToken) { + debugLog('Token refresh successful, storing new tokens') + + localStorage.setItem('jwt', data.token) + + localStorage.removeItem('tokenRefreshInProgress') + + // Broadcast to all tabs + const tokenChannel = new BroadcastChannel('tokenUpdates') + tokenChannel.postMessage({ + type: 'TOKEN_UPDATED', + token: data.token, + timestamp: Date.now(), + }) + tokenChannel.close() + + setAuthenticated(true) + debugLog('=== TOKEN REFRESH SUCCESSFUL ===') + return true + } else { + debugLog('Token refresh response missing required fields', 'error') + } + } else { + debugLog(`Token refresh failed with status: ${res.status}`, 'error') + try { + const errorText = await res.text() + debugLog(`Error response body: ${errorText}`, 'error') + } catch (_) { + debugLog('Could not read error response body', 'warn') + } + } + } catch (error) { + debugLog(`Token refresh error: ${error}`, 'error') + } + + // Cleanup on failure + debugLog('Token refresh failed, cleaning up', 'error') + localStorage.removeItem('jwt') + localStorage.removeItem('tokenRefreshInProgress') + + setAuthenticated(false) + debugLog('=== TOKEN REFRESH FAILED ===') + return false + } + + // Enhanced logout function - PREVENT INFINITE LOOP + const logout = async (skipBroadcast = false): Promise => { + try { + localStorage.removeItem('ak') + } catch { + /* ignore */ + } + try { + const rtExists = (await cookieStore.get('_rtexists'))?.value + if (!rtExists) { + debugLog( + `No refresh token available for logout attempt, _rtexists: '${rtExists}'`, + 'warn', + ) + setAuthenticated(false) + return + } + + const fingerprint = await getDeviceFingerprint() + + // Notify server of logout - use fetch directly to avoid recursive authFetch calls + await fetch(`${API_BASE}/api/logout`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ clientId: fingerprint.clientId }), + }).catch(console.error) + } catch (error) { + console.error('Error during logout:', error) + } finally { + // Clear all tokens and authentication state + localStorage.removeItem('jwt') + setAuthenticated(false) + + // Only broadcast if not called from a broadcast event (prevent infinite loop) + if (!skipBroadcast) { + console.log('Broadcasting logout to other tabs') + const tokenChannel = new BroadcastChannel('tokenUpdates') + tokenChannel.postMessage({ + type: 'LOGOUT', + timestamp: Date.now(), + }) + tokenChannel.close() + } else { + console.log('Logout called from broadcast, skipping broadcast') + } + } + } + + // 1. FIRST: Set up BroadcastChannel listener (before any token operations) + useEffect(() => { + const tokenChannel = new BroadcastChannel('tokenUpdates') + + const handleTokenUpdate = (event: MessageEvent) => { + console.log('Token update received via broadcast: ' + event.data.type) + + if (event.data.type === 'TOKEN_UPDATED') { + const newToken = event.data.token + if (newToken) { + // Update localStorage if it's different (in case this tab missed it) + const currentToken = localStorage.getItem('jwt') + if (currentToken !== newToken) { + localStorage.setItem('jwt', newToken) + } + + console.log('JWT updated via broadcast, staying authenticated') + setAuthenticated(true) + + // Clear any pending refresh operations in this tab + localStorage.removeItem('tokenRefreshInProgress') + } + } else if (event.data.type === 'LOGOUT') { + console.log('Logout broadcast received - logging out this tab') + // Call logout with skipBroadcast=true to prevent infinite loop + logout(true) + } + } + + tokenChannel.addEventListener('message', handleTokenUpdate) + console.log('BroadcastChannel listener set up') + + return () => { + tokenChannel.removeEventListener('message', handleTokenUpdate) + tokenChannel.close() + } + }, []) // Set up once on mount + + // 2. SECOND: Set up global auth handlers + useEffect(() => { + const logoutWrapper = () => logout(false) // Always broadcast from explicit logouts + setAuthHandlers(logoutWrapper, tryRefreshToken) + console.log('Global auth handlers set up') + }, []) + + // 3. THIRD: Handle initial authentication (after listeners are ready) + useEffect(() => { + const initAuth = async () => { + debugLog('=== STARTING INITIAL AUTH CHECK ===') + + // Add a small delay to ensure BroadcastChannel is fully set up + await new Promise((resolve) => setTimeout(resolve, 50)) + + const token = localStorage.getItem('jwt') + + if (token) { + debugLog('Found existing JWT, assuming authenticated') + setAuthenticated(true) + } else { + debugLog('No JWT found, attempting token refresh') + await tryRefreshToken() + } + } + + debugLog('=== INITIAL AUTH CHECK COMPLETE ===') + + initAuth().catch((error) => { + debugLog(`Critical error in initAuth: ${error}`, 'error') + setAuthenticated(false) + }) + }, []) // Run once on mount + + // Enhanced session checking function + const checkForNewSessions = async () => { + if (hasCheckedSessions || !authenticated) return + + try { + console.log('Checking for new sessions...') + const response = await authFetch(`${API_BASE}/api/user/sessions`) + if (!response.ok) return + + const trustedDevices: TrustedDevice[] = await response.json() + console.log(`Fetched ${trustedDevices.length} trusted devices`) + + // Create sessions list + const sessionsList: (Session & TrustedDevice)[] = trustedDevices.map( + (device) => { + // Use the proper getSessionId function from SessionMonitor + const sessionId = getSessionId(device.ip, device.deviceInfo) + return { + ip: device.ip, + firstSeen: device.firstSeen, + lastSeen: device.lastSeen, + isNew: !knownSessions.includes(sessionId), + geolocated: false, // Mark as not geolocated so SessionMonitor can handle it + isGeolocating: false, + location: undefined, + deviceInfo: device.deviceInfo, + loginCount: device.loginCount, + } + }, + ) + + // Sort sessions with new ones first + const sortedSessions = sessionsList.sort((a, b) => + a.isNew === b.isNew ? 0 : a.isNew ? -1 : 1, + ) + setSessions(sortedSessions) + + // Check if there are new sessions + const newSessionsDetected = sortedSessions.some((s) => s.isNew) + + if (newSessionsDetected) { + console.log('New sessions detected, auto-showing session monitor') + setShowSessionMonitor(true) + } + + setHasCheckedSessions(true) + } catch (error) { + console.error('Error checking for new sessions:', error) + setHasCheckedSessions(true) + } + } + + function checkLocalStorageStateConsistency() { + try { + const currentVersion = import.meta.env.VITE_REACT_APP_VERSION ?? '1.0.0' + let lastUpdate = localStorage.getItem('lastUsedAppVersion') + + if (!lastUpdate) { + localStorage.setItem('lastUsedAppVersion', '1.0.0') + lastUpdate = '1.0.0' + } + + // If new version, reset cached data + if (lastUpdate !== currentVersion) { + // Reset all cached states if the website was updated + setCachedRecordings({}) + setCachedRecordingRanges({}) + setCachedPages({}) + setTotalRecordings({}) + setSignedUrlCache({}) + setSignedLiveThumbUrls({}) + localStorage.setItem('lastUsedAppVersion', currentVersion) + alert('Cache reset due to website update or new deployment.') + } + } catch (err) { + alert('Error checking localStorageState consistency!') + console.error('LocalStorage consistency check failed:', err) + } + } + + useEffect(() => { + checkLocalStorageStateConsistency() + }, []) + + // Check for new sessions when authenticated + useEffect(() => { + if (authenticated === true) { + // Delay session check to allow the app to fully load + setTimeout(checkForNewSessions, 2000) + } + }, [authenticated, hasCheckedSessions, knownSessions]) + + // Add periodic session checking (every 5 minutes) + useEffect(() => { + if (!authenticated) return + + const interval = setInterval( + () => { + // Reset hasCheckedSessions to allow periodic checks + setHasCheckedSessions(false) + }, + 5 * 60 * 1000, + ) // 5 minutes + + return () => clearInterval(interval) + }, [authenticated]) + + // --- Refresh JWT token every minute --- + useEffect(() => { + if (!authenticated) return + const interval = setInterval(tryRefreshToken, 60000) + return () => clearInterval(interval) + }, [authenticated]) + + // Add beforeunload event to clear sensitive data (optional) + useEffect(() => { + const handleBeforeUnload = () => { + // Only clear JWT from memory, keep refresh token for next session + // This is optional - you might want to keep the JWT for better UX + const navigationEntries = performance.getEntriesByType( + 'navigation', + ) as PerformanceNavigationTiming[] + if ( + navigationEntries.length > 0 && + navigationEntries[0].type === 'reload' + ) { + // Only on refresh, not on navigation + localStorage.removeItem('jwt') + } + } + + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) + }, []) + + useEffect(() => { + if (!authenticated) { + setAuthenticated( + localStorage.getItem('ak') !== null ? true : authenticated, + ) + } + }, []) + + const handleLogin = async (token: string, refreshToken: string) => { + // Show user-friendly error message + if ( + !window.isSecureContext && + location.protocol !== 'https:' && + location.hostname !== 'localhost' + ) { + alert( + 'Note: For enhanced security on remote devices, consider using HTTPS. Authentication will work but tokens will use basic encoding.', + ) + } + + try { + localStorage.setItem('jwt', token) + setAuthenticated(true) + } catch (error) { + console.error('Error storing tokens:', error) + + // Still try to authenticate even if secure storage fails + localStorage.setItem('jwt', token) + // Store refresh token as base64 fallback + // localStorage.setItem('_rt', btoa(refreshToken)) + setAuthenticated(true) + } + } + + // If authenticated is null, we'll show a loading overlay further down + if (authenticated === false) { + return ( + + + + ) + } + + return ( + + {authenticated ? ( + + ) : ( + + )} + + ) +} diff --git a/web2/src/Component/SessionsLogoutButton.module.css b/web2/src/Component/SessionsLogoutButton.module.css new file mode 100644 index 0000000..55a5172 --- /dev/null +++ b/web2/src/Component/SessionsLogoutButton.module.css @@ -0,0 +1,11 @@ +.button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.menuControl { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border: 0; + border-left: 1px solid var(--mantine-color-body); +} diff --git a/web2/src/Component/SessionsLogoutButton.tsx b/web2/src/Component/SessionsLogoutButton.tsx new file mode 100644 index 0000000..ad84bdb --- /dev/null +++ b/web2/src/Component/SessionsLogoutButton.tsx @@ -0,0 +1,44 @@ +import { FiChevronDown, FiLogOut, FiUsers } from 'react-icons/fi' +import { ActionIcon, Button, Group, Menu, useMantineTheme } from '@mantine/core' +import classes from './SessionsLogoutButton.module.css' + +export function SessionsLogoutButton( + props?: React.ComponentPropsWithoutRef & { + logout: (skipBroadcast?: boolean) => Promise + }, +) { + const theme = useMantineTheme() + + return ( + + + + + + + + + + props?.logout()} + leftSection={} + > + Logout + + + + + ) +} diff --git a/web2/src/Component/StreamVideo.tsx b/web2/src/Component/StreamVideo.tsx new file mode 100644 index 0000000..f79f401 --- /dev/null +++ b/web2/src/Component/StreamVideo.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef, useState } from 'react' +import HLS from 'hls.js' +import { Video } from '@gfazioli/mantine-video' +import { API_BASE, fetchWithRetry, authFetch } from '../main' + +export default function StreamVideo(props: { streamId: string }) { + const [streamUrl, setStreamUrl] = useState() + const videoRef = useRef(null) + useEffect(() => { + ;(async () => { + let url = `${API_BASE}/api/signed-stream-url/${props.streamId}` + + // Fetch signed URL from API + try { + const response = await fetchWithRetry(() => authFetch(url)) + url = `${API_BASE}${(await response.json()).url}` + } catch { + console.error('Failed to fetch signed stream URL') + return + } + + setStreamUrl(url) + })() + }, [props.streamId]) + + useEffect(() => { + const video = videoRef.current + if (!video || !streamUrl) return + + const videoElement = video.getElementsByTagName('video')[0] + + // eslint-disable-next-line import/no-named-as-default-member + if (HLS.isSupported()) { + const hls = new HLS() + hls.loadSource(streamUrl) + hls.attachMedia(videoElement) + + return () => hls.destroy() + } + + if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { + videoElement.src = streamUrl + } + + return () => { + videoElement.removeAttribute('src') + videoElement.load() + } + }, [streamUrl]) + + return