Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions app/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
export const VERSION = '1.7.0'
export const VERSION = '1.7.1'

export const RequiredVersions = {
controller: VERSION,
tts: '2.0.0',
piper: '1.3.0',
tts: '2.0.2',
piper: '1.6.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`
Expand Down
7 changes: 7 additions & 0 deletions app/lib/log.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {getPrisma} from './prisma.server'

export const log = async (message: string) => {
const prisma = getPrisma()

await prisma.log.create({data: {message}})
}
6 changes: 5 additions & 1 deletion app/lib/settings.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ type SettingKey =
| 'ttsSpeed'
| 'enrollUrl'
| 'controlPointKey'
| 'controlPointDefaultZone'
| 'ttsLastSeen'
| 'workerLastSeen'
| 'lockdownEntrySequence'
| 'lockdownExitSequence'
| 'siteName'

export const DEFAULT_SETTINGS: {[setting in SettingKey]: string} = {
lockdownRepeat: '5',
Expand All @@ -22,10 +24,12 @@ 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: '[]',
lockdownExitSequence: '[]'
lockdownExitSequence: '[]',
siteName: 'Open School Bell'
}

export const getSetting = async (setting: SettingKey) => {
Expand Down
13 changes: 11 additions & 2 deletions app/lib/trigger-action.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ import {type Action} from '@prisma/client'

import {broadcast} from './broadcast.server'
import {toggleLockdown} from './lockdown.server'
import {log} from './log.server'

export const triggerAction = async (
action: Action,
zone: string | null | undefined,
source: string,
callbacks: {onMissingZone: (suppliedZone: string | null | undefined) => void}
) => {
await log(`🎬 Triggering action ${action.name} from ${source}`)

export const triggerAction = async (action: Action, zone: string) => {
switch (action.action) {
case 'broadcast':
if (!zone || typeof zone !== 'string' || zone.trim() === '') {
return Response.json({error: 'missing zone'}, {status: 400})
callbacks.onMissingZone(zone)
return
}

await broadcast(zone, action.data)
Expand Down
6 changes: 6 additions & 0 deletions app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,17 @@ 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.',
'settings.password.placeholderNew': 'New password',
'settings.password.placeholderConfirm': 'Repeat new password',
'settings.siteName.label': 'Site Name',
'settings.siteName.helper':
'The site names to display at the top of the application.',
'schedule.metaTitle': 'Schedule',
'schedule.pageTitle': 'Schedule',
'schedule.defaultOption': 'Default',
Expand Down
9 changes: 7 additions & 2 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
useRouteLoaderData
} from '@remix-run/react'
import {
json,

Check warning on line 11 in app/root.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'json' is defined but never used. Allowed unused vars must match /^ignored/u
type LinksFunction,
type LoaderFunctionArgs
} from '@remix-run/node'
Expand Down Expand Up @@ -38,6 +38,7 @@
import {SidebarLink, NavSep} from './lib/ui'
import {I18nProvider, useTranslation} from './lib/i18n'
import {initTranslations, type InitTranslationsReturn} from './lib/i18n.server'
import {getSetting} from './lib/settings.server'

const FALLBACK_LOCALE: SupportedLocale = 'en'
const FALLBACK_TRANSLATIONS: InitTranslationsReturn = {
Expand All @@ -49,7 +50,10 @@

export const loader = async ({request}: LoaderFunctionArgs) => {
const {locale, messages} = initTranslations(request)
return json({locale, messages})

const siteName = await getSetting('siteName')

return {locale, messages, siteName}
}

export function Layout({children}: {children: React.ReactNode}) {
Expand All @@ -74,13 +78,14 @@
}

const AppContent = () => {
const {siteName} = useLoaderData<typeof loader>()
const {t} = useTranslation()

return (
<div className="grid grid-cols-app min-h-screen grid-rows-app">
<div className="p-2 flex items-center">
<img src="/logo.png" className="w-16 mr-4" />
<span>{t('app.title')}</span>
<span>{siteName}</span>
</div>
<div className="row-span-3 border-gray-300 border rounded-xl my-2 mr-2 shadow-sm bg-white p-2">
<Outlet />
Expand Down
107 changes: 63 additions & 44 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
const buttons = await prisma.actionButton.findMany({orderBy: {name: 'asc'}})
const logs = await prisma.log.findMany({orderBy: {time: 'desc'}, take: 10})

const {lockdownMode, workerLastSeen, ttsLastSeen} = await getSettings([
'lockdownMode',
'workerLastSeen',
'ttsLastSeen'
])
const {lockdownMode, workerLastSeen, ttsLastSeen, siteName} =
await getSettings([
'lockdownMode',
'workerLastSeen',
'ttsLastSeen',
'siteName'
])

return {sounders, lockdownMode, buttons, logs, workerLastSeen, ttsLastSeen}
return {
sounders,
lockdownMode,
buttons,
logs,
workerLastSeen,
ttsLastSeen,
siteName
}
}

export const meta: MetaFunction = ({matches}) => {
Expand All @@ -46,14 +56,21 @@ export const meta: MetaFunction = ({matches}) => {
}

export default function Index() {
const {sounders, lockdownMode, buttons, logs, workerLastSeen, ttsLastSeen} =
useLoaderData<typeof loader>()
const {
sounders,
lockdownMode,
buttons,
logs,
workerLastSeen,
ttsLastSeen,
siteName
} = useLoaderData<typeof loader>()
const {t, locale} = useTranslation()
const dateLocale = locale === 'pl' ? pl : enUS
useLivePageData()

return (
<Page title={t('dashboard.pageTitle')} wide>
<Page title={`${t('dashboard.pageTitle')} - ${siteName}`} wide>
<div className="grid grid-cols-2 gap-4">
<div className="box">
<h2>{t('dashboard.devices')}</h2>
Expand Down Expand Up @@ -92,44 +109,46 @@ export default function Index() {
</table>
</div>
<div
className={`box text-center ${lockdownMode === '0' ? 'bg-green-300' : 'bg-red-300'}`}
className={`box text-center relative shadow ring-1 ring-white/50 ${lockdownMode === '0' ? 'bg-green-300' : 'bg-red-300'}`}
>
<p className="mt-2">
{t('dashboard.lockdown.message', {
status: t(
lockdownMode === '0'
? 'dashboard.lockdown.status.disabled'
: 'dashboard.lockdown.status.enabled'
)
})}
</p>
<form
action="/lockdown/trigger"
method="post"
onSubmit={e => {
if (
!confirm(
t(
lockdownMode === '0'
? 'dashboard.lockdown.confirmEnable'
: 'dashboard.lockdown.confirmDisable'
)
<div className="absolute top-[50%] mt-[-44px] left-[50%] ml-[-12.25rem] w-96">
<p className="mt-2">
{t('dashboard.lockdown.message', {
status: t(
lockdownMode === '0'
? 'dashboard.lockdown.status.disabled'
: 'dashboard.lockdown.status.enabled'
)
) {
e.preventDefault()
}
}}
>
<button
className={`bg-gray-300 p-2 rounded-xl shadow-sm cursor-pointer mt-4 ${lockdownMode === '1' ? 'bg-green-300' : 'bg-red-300'}`}
})}
</p>
<form
action="/lockdown/trigger"
method="post"
onSubmit={e => {
if (
!confirm(
t(
lockdownMode === '0'
? 'dashboard.lockdown.confirmEnable'
: 'dashboard.lockdown.confirmDisable'
)
)
) {
e.preventDefault()
}
}}
>
{t(
lockdownMode === '0'
? 'dashboard.lockdown.button.enable'
: 'dashboard.lockdown.button.disable'
)}
</button>
</form>
<button
className={`bg-gray-300 p-2 rounded-xl shadow-sm cursor-pointer mt-4 ${lockdownMode === '1' ? 'bg-green-300' : 'bg-red-300'}`}
>
{t(
lockdownMode === '0'
? 'dashboard.lockdown.button.enable'
: 'dashboard.lockdown.button.disable'
)}
</button>
</form>
</div>
</div>
<div className="box">
<h2>{t('dashboard.buttons')}</h2>
Expand Down
47 changes: 45 additions & 2 deletions app/routes/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
.catch(() => resolve('error'))
})

const controlPointLatest = await new Promise<string>(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()

Expand Down Expand Up @@ -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()
Expand All @@ -174,7 +195,9 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
buttonLatest,
ttsLatest,
controllerLatest,
license
license,
controlPointVersion,
controlPointLatest
}
}

Expand All @@ -194,7 +217,9 @@ const About = () => {
buttonLatest,
ttsLatest,
controllerLatest,
license
license,
controlPointVersion,
controlPointLatest
} = useLoaderData<typeof loader>()
const {t} = useTranslation()

Expand Down Expand Up @@ -284,6 +309,24 @@ const About = () => {
</tr>
)
})}
{controlPointVersion ? (
<tr>
<td>Control Point</td>
<td className="text-center">{controlPointVersion}</td>
<td
className={`text-center ${semver.gt(controlPointLatest, controlPointVersion) ? 'bg-red-300' : ''}`}
>
{controlPointLatest.replace('v', '')}
</td>
<td
className={`text-center ${semver.gt(RequiredVersions.controlPoint, controlPointVersion) ? 'bg-red-300' : ''}`}
>
{RequiredVersions.controlPoint}
</td>
</tr>
) : (
''
)}
</tbody>
</table>

Expand Down
33 changes: 13 additions & 20 deletions app/routes/button-api.trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {type ActionFunctionArgs} from '@remix-run/node'

import {getPrisma} from '~/lib/prisma.server'
import {broadcast} from '~/lib/broadcast.server'
import {toggleLockdown} from '~/lib/lockdown.server'
import {triggerAction} from '~/lib/trigger-action.server'

export const action = async ({request}: ActionFunctionArgs) => {
const {key} = (await request.json()) as {
Expand All @@ -26,23 +25,17 @@ export const action = async ({request}: ActionFunctionArgs) => {

const zone = button.zoneId

switch (button.action.action) {
case 'broadcast':
if (!zone || typeof zone !== 'string' || zone.trim() === '') {
return Response.json({error: 'missing zone'}, {status: 400})
}

if (button.action.audioId) {
const zoneId = zone.trim()
await broadcast(zoneId, JSON.stringify([button.action.audioId]))
}
break
case 'lockdown':
await toggleLockdown()
break
default:
break
}
let response: {result: 'ok' | 'error'; error?: string} = {result: 'ok'}
let status = 200

await triggerAction(button.action, zone, `Button ${button.name}`, {
onMissingZone: () => {
response = {result: 'error', error: 'missing zone'}
status = 400

return
}
})

return Response.json({ping: 'pong'})
return Response.json(response, {status})
}
Loading
Loading