From 8008f11abc4c9097f6a106adb59470148338e7f7 Mon Sep 17 00:00:00 2001 From: AML - A Laycock Date: Thu, 30 Jul 2026 14:04:08 +0100 Subject: [PATCH] feat: improve the control point api --- app/lib/constants.ts | 3 +- app/lib/settings.server.ts | 2 ++ app/locales/en.ts | 3 ++ app/routes/about.tsx | 47 +++++++++++++++++++++++++-- app/routes/control-point.config.tsx | 21 +++++++++--- app/routes/control-point.trigger.tsx | 2 +- app/routes/settings.tsx | 48 +++++++++++++++++++++++----- 7 files changed, 110 insertions(+), 16 deletions(-) diff --git a/app/lib/constants.ts b/app/lib/constants.ts index 7963fb8..118f487 100644 --- a/app/lib/constants.ts +++ b/app/lib/constants.ts @@ -5,7 +5,8 @@ export const RequiredVersions = { tts: '2.0.0', piper: '1.3.0', sounder: '2.4.0', - button: '1.0.0' + button: '1.0.0', + controlPoint: '1.0.0' } export const DOCS_URL = `https://openschoolbell.co.uk` diff --git a/app/lib/settings.server.ts b/app/lib/settings.server.ts index d8ecdb4..7874a26 100644 --- a/app/lib/settings.server.ts +++ b/app/lib/settings.server.ts @@ -9,6 +9,7 @@ type SettingKey = | 'ttsSpeed' | 'enrollUrl' | 'controlPointKey' + | 'controlPointDefaultZone' | 'ttsLastSeen' | 'workerLastSeen' | 'lockdownEntrySequence' @@ -22,6 +23,7 @@ export const DEFAULT_SETTINGS: {[setting in SettingKey]: string} = { ttsSpeed: '1', enrollUrl: 'http://controller:3000', controlPointKey: '', + controlPointDefaultZone: '', ttsLastSeen: '"1970-01-01T23:00:00.000Z"', workerLastSeen: '"1970-01-01T23:00:00.000Z"', lockdownEntrySequence: '[]', diff --git a/app/locales/en.ts b/app/locales/en.ts index 7e1b01f..861ea53 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -222,6 +222,9 @@ export const en = { 'settings.controlPointKey.label': 'Control Point Key', 'settings.controlPointKey.helper': 'The key used by the Control Point App to communicate with the controller.', + 'settings.controlPointDefaultZone.label': 'Control Point Zone', + 'settings.controlPointDefaultZone.helper': + 'Which zone should the control point user interface default to?', 'settings.password.label': 'Change password', 'settings.password.helper': 'Leave fields empty to keep the current password.', diff --git a/app/routes/about.tsx b/app/routes/about.tsx index e0a4632..7066156 100644 --- a/app/routes/about.tsx +++ b/app/routes/about.tsx @@ -131,6 +131,25 @@ export const loader = async ({request}: LoaderFunctionArgs) => { .catch(() => resolve('error')) }) + const controlPointLatest = await new Promise(resolve => { + fetch( + 'https://api.github.com/repos/Open-School-Bell/control-point/releases?per_page=1', + { + headers: { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + } + ) + .then(response => { + response + .json() + .then(data => resolve(data[0].tag_name)) + .catch(() => resolve('error')) + }) + .catch(() => resolve('error')) + }) + const prisma = getPrisma() const redis = getRedis() @@ -160,6 +179,8 @@ export const loader = async ({request}: LoaderFunctionArgs) => { buttonVersions[id] = version ? version : '0.0.0' }) + const controlPointVersion = await redis.get('osb-control-point-version') + const license = ( await readFile(path.join(process.cwd(), 'LICENSE')) ).toString() @@ -174,7 +195,9 @@ export const loader = async ({request}: LoaderFunctionArgs) => { buttonLatest, ttsLatest, controllerLatest, - license + license, + controlPointVersion, + controlPointLatest } } @@ -194,7 +217,9 @@ const About = () => { buttonLatest, ttsLatest, controllerLatest, - license + license, + controlPointVersion, + controlPointLatest } = useLoaderData() const {t} = useTranslation() @@ -284,6 +309,24 @@ const About = () => { ) })} + {controlPointVersion ? ( + + Control Point + {controlPointVersion} + + {controlPointLatest.replace('v', '')} + + + {RequiredVersions.controlPoint} + + + ) : ( + '' + )} diff --git a/app/routes/control-point.config.tsx b/app/routes/control-point.config.tsx index 95513f2..33daf83 100644 --- a/app/routes/control-point.config.tsx +++ b/app/routes/control-point.config.tsx @@ -1,10 +1,16 @@ import {type LoaderFunctionArgs} from '@remix-run/node' -import {getSetting} from '~/lib/settings.server' +import {getSettings} from '~/lib/settings.server' import {getPrisma} from '~/lib/prisma.server' +import {getRedis} from '~/lib/redis.server.mjs' export const loader = async ({request}: LoaderFunctionArgs) => { - const controlPointKey = await getSetting('controlPointKey') + const {controlPointKey, controlPointDefaultZone} = await getSettings([ + 'controlPointKey', + 'controlPointDefaultZone' + ]) + + const redis = getRedis() if (controlPointKey === '') { return Response.json({ @@ -14,13 +20,18 @@ export const loader = async ({request}: LoaderFunctionArgs) => { } const authHeader = request.headers.get('Auth') + const versionHeader = request.headers.get('Version') if (!authHeader) { return Response.json({result: 'error', error: 'No key provided.'}) } if (authHeader !== controlPointKey) { - return Response.json({result: 'error', error: 'Invalid Ket provided.'}) + return Response.json({result: 'error', error: 'Invalid Key provided.'}) + } + + if (versionHeader) { + void redis.set(`osb-control-point-version`, versionHeader) } const prisma = getPrisma() @@ -30,6 +41,8 @@ export const loader = async ({request}: LoaderFunctionArgs) => { return Response.json({ zones: zones.map(({id, name}) => { return {id, name} - }) + }), + defaultZone: + controlPointDefaultZone !== '' ? controlPointDefaultZone : zones[0].id }) } diff --git a/app/routes/control-point.trigger.tsx b/app/routes/control-point.trigger.tsx index 617b443..018bebc 100644 --- a/app/routes/control-point.trigger.tsx +++ b/app/routes/control-point.trigger.tsx @@ -22,7 +22,7 @@ export const action = async ({request}: ActionFunctionArgs) => { } if (authHeader !== controlPointKey) { - return Response.json({result: 'error', error: 'Invalid Ket provided.'}) + return Response.json({result: 'error', error: 'Invalid Key provided.'}) } const data = (await request.json()) as {pin: string; zone: string} diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx index cd9b7d7..2314fae 100644 --- a/app/routes/settings.tsx +++ b/app/routes/settings.tsx @@ -4,7 +4,7 @@ import { type MetaFunction, redirect } from '@remix-run/node' -import {useLoaderData} from '@remix-run/react' +import {Form, useLoaderData} from '@remix-run/react' import {invariant} from '@arcath/utils' import {getSettings, setSetting} from '~/lib/settings.server' @@ -14,6 +14,7 @@ import {Page, FormElement} from '~/lib/ui' import {useTranslation} from '~/lib/i18n' import {translate} from '~/lib/i18n.shared' import {getRootI18n} from '~/lib/i18n.meta' +import {getPrisma} from '~/lib/prisma.server' export const meta: MetaFunction = ({matches}) => { const {messages} = getRootI18n(matches) @@ -27,16 +28,27 @@ export const loader = async ({request}: LoaderFunctionArgs) => { return redirect('/login') } - const {ttsSpeed, enrollUrl, controlPointKey} = await getSettings([ - 'ttsSpeed', - 'enrollUrl', - 'controlPointKey' - ]) + const prisma = getPrisma() + + const {ttsSpeed, enrollUrl, controlPointKey, controlPointDefaultZone} = + await getSettings([ + 'ttsSpeed', + 'enrollUrl', + 'controlPointKey', + 'controlPointDefaultZone' + ]) + + const zones = await prisma.zone.findMany({ + select: {id: true, name: true}, + orderBy: {name: 'asc'} + }) return { ttsSpeed, enrollUrl, - controlPointKey + controlPointKey, + controlPointDefaultZone, + zones } } @@ -52,14 +64,19 @@ export const action = async ({request}: ActionFunctionArgs) => { | undefined) ? (formData.get('controlPointKey') as string | undefined) : '' + const controlPointDefaultZone = formData.get('controlPointDefaultZone') as + | string + | undefined invariant(enrollUrl) invariant(ttsSpeed) invariant(controlPointKey) + invariant(controlPointDefaultZone) await setSetting('enrollUrl', enrollUrl) await setSetting('ttsSpeed', ttsSpeed) await setSetting('controlPointKey', controlPointKey) + await setSetting('controlPointDefaultZone', controlPointDefaultZone) if (password && checkPassword && password === checkPassword) { await setSetting('password', password) @@ -69,7 +86,8 @@ export const action = async ({request}: ActionFunctionArgs) => { } const Settings = () => { - const {ttsSpeed, enrollUrl, controlPointKey} = useLoaderData() + const {ttsSpeed, enrollUrl, controlPointKey, controlPointDefaultZone, zones} = + useLoaderData() const {t} = useTranslation() return ( @@ -108,6 +126,20 @@ const Settings = () => { defaultValue={controlPointKey} /> + + +