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
3 changes: 2 additions & 1 deletion app/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ 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
48 changes: 47 additions & 1 deletion app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,53 @@ 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.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
41 changes: 39 additions & 2 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ 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 lockdownMode = await getSetting('lockdownMode')

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

export const meta: MetaFunction = ({matches}) => {
Expand All @@ -38,7 +39,7 @@ export const meta: MetaFunction = ({matches}) => {
}

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

Expand Down Expand Up @@ -119,6 +120,42 @@ 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>
</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})
}
34 changes: 34 additions & 0 deletions app/routes/button-api.get-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {type ActionFunctionArgs} from '@remix-run/node'

import {getPrisma} from '~/lib/prisma.server'
import {getSettings} from '~/lib/settings.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.findFirst({
where: {key, enrolled: true}
})

if (!button) {
return Response.json({error: 'invalid key'}, {status: 403})
}

const {lockdownMode} = await getSettings(['lockdownMode'])

return Response.json({
name: button.name,
id: button.id,
ledPin: button.ledPin,
buttonPin: button.buttonPin,
holdDuration: button.holdDuration,
cancelDuration: button.cancelDuration,
lockdown: lockdownMode === '1'
})
}
34 changes: 34 additions & 0 deletions app/routes/button-api.log.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {type ActionFunctionArgs} from '@remix-run/node'

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

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

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

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

const prisma = getPrisma()

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

if (!button) {
return Response.json({error: 'invalid key'}, {status: 403})
}

await prisma.actionButtonLog.create({
data: {message, actionButtonId: button.id}
})

return Response.json({status: 'ok'})
}
37 changes: 37 additions & 0 deletions app/routes/button-api.ping.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {type ActionFunctionArgs} from '@remix-run/node'

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

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

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

const prisma = getPrisma()
const redis = getRedis()

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

if (!button) {
return Response.json({error: 'invalid key'}, {status: 403})
}

await prisma.actionButton.update({
where: {id: button.id},
data: {lastCheckIn: new Date()}
})

if (version) {
void redis.set(`osb-button-version-${button.id}`, version)
}

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