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
5 changes: 3 additions & 2 deletions app/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export const VERSION = '1.4.0'
export const VERSION = '1.5.0'

export const RequiredVersions = {
controller: VERSION,
tts: '2.0.0',
piper: '1.3.0',
sounder: '2.1.0'
sounder: '2.1.0',
button: '1.0.0'
}

export const DOCS_URL = `https://openschoolbell.co.uk`
Expand Down
32 changes: 32 additions & 0 deletions app/lib/hooks/use-live-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {useRevalidator} from '@remix-run/react'
import {useEffect, useRef} from 'react'

function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef<() => void>()

// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])

// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current!()
}
if (delay !== null) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [delay])
}

export const useLivePageData = (interval: number = 5000) => {
const revalidator = useRevalidator()

useInterval(() => {
if (revalidator.state === 'idle') {
revalidator.revalidate()
}
}, interval)
}
50 changes: 49 additions & 1 deletion app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const en = {
'Are you sure you want to disable lockdown?',
'dashboard.lockdown.button.enable': 'Enable',
'dashboard.lockdown.button.disable': 'Disable',
'dashboard.log': 'Log',
'about.title': 'About',
'about.table.component': 'Component',
'about.table.version': 'Version',
Expand Down Expand Up @@ -343,7 +344,54 @@ export const en = {
'zones.edit.pageTitle': 'Edit zone {{name}}',
'zones.detail.metaFallback': 'Zone',
'zones.detail.soundersTitle': 'Sounders',
'zones.detail.editButton': 'Edit zone'
'zones.detail.editButton': 'Edit zone',
'buttons.titleWithCount': 'Buttons ({{count}})',
'buttons.table.device': 'Button',
'buttons.table.action': 'Action',
'nav.buttons': 'Buttons',
'buttons.addButton': 'Add Button',
'buttons.metaTitle': 'Buttons',
'buttons.add.pageTitle': 'Add Button',
'buttons.form.name.label': 'Name',
'buttons.form.name.helper': 'Descriptive name of the button.',
'buttons.form.ip.label': 'IP address',
'buttons.form.ip.helper':
'IP address where the controller can reach the button.',
'buttons.form.action.label': 'Action',
'buttons.form.action.helper': 'The action to be triggered by this button',
'buttons.form.zone.label': 'Zone',
'buttons.form.zone.helper': 'The zone to use when triggering the action.',
'buttons.form.ledPin.label': 'LED Pin',
'buttons.form.ledPin.helper': 'The GPIO output pin connected to the LED.',
'buttons.form.buttonPin.label': 'Button Pin',
'buttons.form.buttonPin.helper':
'The GPIO input pin connected to the Button.',
'buttons.form.holdDuration.label': 'Hold Duration',
'buttons.form.holdDuration.helper':
'How long in seconds should the button be held to trigger the action.',
'buttons.form.cancelDuration.label': 'Cancel Duration',
'buttons.form.cancelDuration.helper':
'How long in seconds does the user have to cancel the trigger.',
'buttons.add.submit': 'Add Button',
'buttons.edit.submit': 'Update Button',
'buttons.deleteConfirmation':
'Are you sure you want to delete the button {{name}}?',
'buttons.detail.metaFallback': 'Button',
'buttons.detail.infoTitle': 'About',
'buttons.detail.ipLabel': 'IP',
'buttons.detail.keyLabel': 'Key',
'buttons.detail.logTitle': 'Log',
'buttons.detail.editButton': 'Edit button',
'buttons.detail.logButton': 'View full log',
'buttons.detail.ledPinLabel': 'LED Pin',
'buttons.detail.buttonPinLabel': 'Button Pin',
'buttons.detail.holdLabel': 'Hold Duration',
'buttons.detail.cancelLabel': 'Cancel Duration',
'buttons.detail.actionLabel': 'Action',
'buttons.detail.zoneLabel': 'Zone',
'buttons.edit.metaTitle': 'Edit {{name}}',
'buttons.edit.pageTitle': 'Edit button {{name}}',
'dashboard.buttons': 'Buttons'
} as const

export type EnMessages = typeof en
4 changes: 4 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import LockClosedIcon from '@heroicons/react/24/outline/LockClosedIcon'
import MusicIcon from '@heroicons/react/24/outline/MusicalNoteIcon'
import CodeIcon from '@heroicons/react/24/outline/CodeBracketIcon'
import LogIcon from '@heroicons/react/24/outline/ClipboardDocumentCheckIcon'
import ButtonIcon from '@heroicons/react/24/outline/ArrowDownOnSquareIcon'

import './tailwind.css'

Expand Down Expand Up @@ -112,6 +113,9 @@ const AppContent = () => {
<SidebarLink to="/actions">
<ArrowIcon className="w-6 mr-2" /> <span>{t('nav.actions')}</span>
</SidebarLink>
<SidebarLink to="/buttons">
<ButtonIcon className="w-6 mr-2" /> <span>{t('nav.buttons')}</span>
</SidebarLink>
<SidebarLink to="/webhooks">
<CodeIcon className="w-6 mr-2" /> <span>{t('nav.webhooks')}</span>
</SidebarLink>
Expand Down
80 changes: 73 additions & 7 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
redirect
} from '@remix-run/node'
import {Link, useLoaderData} from '@remix-run/react'
import {formatDistance} from 'date-fns'
import {formatDistance, format} from 'date-fns'
import {enUS, pl} from 'date-fns/locale'

import {getPrisma} from '~/lib/prisma.server'
Expand All @@ -15,6 +15,8 @@ import {Page} from '~/lib/ui'
import {useTranslation} from '~/lib/i18n'
import {translate} from '~/lib/i18n.shared'
import {getRootI18n} from '~/lib/i18n.meta'
import {useLivePageData} from '~/lib/hooks/use-live-data'
import {translateLogMessage} from './log'

export const loader = async ({request}: LoaderFunctionArgs) => {
const result = await checkSession(request)
Expand All @@ -26,10 +28,12 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
const prisma = getPrisma()

const sounders = await prisma.sounder.findMany({orderBy: {name: 'asc'}})
const buttons = await prisma.actionButton.findMany({orderBy: {name: 'asc'}})
const logs = await prisma.log.findMany({orderBy: {time: 'desc'}, take: 10})

const lockdownMode = await getSetting('lockdownMode')

return {sounders, lockdownMode}
return {sounders, lockdownMode, buttons, logs}
}

export const meta: MetaFunction = ({matches}) => {
Expand All @@ -38,12 +42,13 @@ export const meta: MetaFunction = ({matches}) => {
}

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

return (
<Page title={t('dashboard.pageTitle')}>
<Page title={t('dashboard.pageTitle')} wide>
<div className="grid grid-cols-2 gap-4">
<div className="box">
<h2>{t('dashboard.devices')}</h2>
Expand Down Expand Up @@ -82,9 +87,9 @@ export default function Index() {
</table>
</div>
<div
className={`box ${lockdownMode === '0' ? 'bg-green-300' : 'bg-red-300'}`}
className={`box text-center ${lockdownMode === '0' ? 'bg-green-300' : 'bg-red-300'}`}
>
<p>
<p className="mt-2">
{t('dashboard.lockdown.message', {
status: t(
lockdownMode === '0'
Expand All @@ -110,7 +115,9 @@ export default function Index() {
}
}}
>
<button className="bg-gray-300 p-2 rounded-xl shadow-sm cursor-pointer">
<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'
Expand All @@ -119,6 +126,65 @@ export default function Index() {
</button>
</form>
</div>
<div className="box">
<h2>{t('dashboard.buttons')}</h2>
<table className="box-table">
<thead>
<tr>
<th className="p-2">{t('dashboard.table.name')}</th>
<th className="p-2">{t('dashboard.table.status')}</th>
<th className="p-2">{t('dashboard.table.lastSeen')}</th>
</tr>
</thead>
<tbody>
{buttons.map(({id, name, lastCheckIn}) => {
return (
<tr key={id}>
<td>
<Link to={`/buttons/${id}`}>{name}</Link>
</td>
<td className="text-center">
{new Date().getTime() / 1000 -
lastCheckIn.getTime() / 1000 <
65
? '🟢'
: '🔴'}
</td>
<td>
{formatDistance(lastCheckIn, new Date(), {
addSuffix: true,
locale: dateLocale
})}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
<div className="box">
<h2>{t('dashboard.log')}</h2>
<table className="box-table">
<thead>
<tr>
<th>{t('log.columns.time')}</th>
<th>{t('log.columns.message')}</th>
</tr>
</thead>
<tbody>
{logs.map(({id, message, time}) => {
return (
<tr key={id}>
<td className="text-center">
{format(time, 'dd/MM/yy HH:mm')}
</td>
<td>{translateLogMessage(message, t)}</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</Page>
)
Expand Down
56 changes: 56 additions & 0 deletions app/routes/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
.catch(() => resolve('error'))
})

const buttonLatest = await new Promise<string>(resolve => {
fetch(
'https://api.github.com/repos/Open-School-Bell/action-button/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 All @@ -128,6 +147,19 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
sounderVersions[id] = version ? version : '0.0.0'
})

const buttons = await prisma.actionButton.findMany({
select: {id: true, name: true},
orderBy: {name: 'asc'}
})

const buttonVersions: {[buttonId: string]: string} = {}

await asyncForEach(buttons, async ({id}) => {
const version = await redis.get(`osb-button-version-${id}`)

buttonVersions[id] = version ? version : '0.0.0'
})

const license = (
await readFile(path.join(process.cwd(), 'LICENSE'))
).toString()
Expand All @@ -137,6 +169,9 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
sounders,
sounderVersions,
sounderLatest,
buttons,
buttonVersions,
buttonLatest,
ttsLatest,
controllerLatest,
license
Expand All @@ -154,6 +189,9 @@ const About = () => {
sounders,
sounderVersions,
sounderLatest,
buttons,
buttonVersions,
buttonLatest,
ttsLatest,
controllerLatest,
license
Expand Down Expand Up @@ -228,6 +266,24 @@ const About = () => {
</tr>
)
})}
{buttons.map(({id, name}) => {
return (
<tr key={id}>
<td>{`Button: ${name}`}</td>
<td className="text-center">{buttonVersions[id]}</td>
<td
className={`text-center ${semver.gt(buttonLatest, buttonVersions[id]) ? 'bg-red-300' : ''}`}
>
{buttonLatest.replace('v', '')}
</td>
<td
className={`text-center ${semver.gt(RequiredVersions.button, buttonVersions[id]) ? 'bg-red-300' : ''}`}
>
{RequiredVersions.button}
</td>
</tr>
)
})}
</tbody>
</table>

Expand Down
24 changes: 24 additions & 0 deletions app/routes/button-api.enroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {type ActionFunctionArgs} from '@remix-run/node'

import {getPrisma} from '~/lib/prisma.server'

export const action = async ({request}: ActionFunctionArgs) => {
const {key} = (await request.json()) as {key?: string}

if (!key || typeof key !== 'string') {
return Response.json({error: 'missing key'}, {status: 400})
}

const prisma = getPrisma()

const button = await prisma.actionButton.findFirstOrThrow({
where: {key, enrolled: false}
})

await prisma.actionButton.update({
where: {id: button.id},
data: {enrolled: true}
})

return Response.json({id: button.id, name: button.name})
}
Loading
Loading