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
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: CI

# src/api is generated from LensBridgeBackend/openapi.yaml. The kiosk reads one
# endpoint, but it runs unattended on a wall -- a silently changed payload shape
# shows up as a blank board, so the contract is checked here instead.
# src/api/schema.d.ts is generated from LensBridgeBackend/openapi.yaml. The kiosk
# reads one payload shape (MusallahBoardPayload, served by its agent), but it
# runs unattended on a wall -- a silently changed payload shape shows up as a
# blank board, so the contract is checked here instead.

on:
push:
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Release

# Publishes the board app for the device agents (agent/docs/architecture.md,
# sections 4, 9.4 and 15). Pushing a tag vX.Y.Z builds the SPA, signs it into
# musallahboard-app-X.Y.Z.mbu with the release key, and attaches that plus
# app-channel.json to the GitHub release. Boards poll
# releases/latest/download/app-channel.json, so publishing the release is what
# rolls the fleet forward; the package signature, not this workflow, is what
# makes that safe.

on:
push:
tags: ['v*']

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm

- run: npm ci

# The channel file advertises package.json's version under this tag's
# download URL. A tag that disagrees would publish a pointer to a file
# that is not there.
- name: Check the tag matches package.json
run: |
version="$(node -p "require('./package.json').version")"
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
echo "tag ${GITHUB_REF_NAME} does not match package.json version ${version}" >&2
exit 1
fi

- run: npm test

- name: Build and sign the app package
env:
MB_RELEASE_SIGNING_KEY: ${{ secrets.MB_RELEASE_SIGNING_KEY }}
run: npm run package

- uses: softprops/action-gh-release@v2
with:
files: |
release/*.mbu
release/app-channel.json
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
node_modules/*
dist/*
# npm run package: signed board app packages and the channel pointer
release/
.env
.DS_Store
.vscode/*
logs/*
npm-debug.log*
new/*

# wrangler files
.wrangler
.dev.vars*
!.dev.vars.example
!.env.example

# design-sync: staged converter scripts, build output, and machine state.
Expand Down
9 changes: 9 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MusallahBoard Production Plan

> Superseded: the board app is no longer hosted; it runs only served by the device agent (see README and agent/docs/architecture.md).

Path from the current PoC (hosted Vite site + `setup.sh`-hardened Pi kiosk) to a
production fleet with per-device identity, remote control from the admin
portal, granular per-board configuration, and offline resilience.
Expand Down Expand Up @@ -90,6 +92,13 @@ elevated privileges, so they don't touch the agent.

### 1. Service worker (offline)

> **Superseded.** Offline operation is no longer a service-worker concern:
> every board is now served from its own disk by the device agent, with
> signed content and app packages (local-first, no online/offline mode). See
> `docs/architecture.md` in the [agent repo](https://github.com/LensBridge/agent).
> The notes below, and the related non-goals and tech choices in this plan,
> describe the earlier approach.

In the existing Vite app, using `vite-plugin-pwa` (Workbox under the hood).

- Precache app shell (HTML/CSS/JS/fonts/logo).
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,44 @@ curl -fsSL https://raw.githubusercontent.com/LensBridge/agent/main/setup.sh | su

This will install the agent and all of its dependencies, setup the configuration for the window manager, and configure the system. Upon reboot, the board will display a "Waiting for Enrollment" screen until you enroll it in your Minbar tenancy.

## Running on a board

Every board runs this app from its own disk. The [device agent](https://github.com/LensBridge/agent) serves it at `http://127.0.0.1:8080/` together with the day's content, and the kiosk always loads that address, online or not. The network only changes how fresh the content is. The agent's `docs/architecture.md` is the contract for all of this.

The page is same-origin with the agent: it fetches today's payload from `/api/musallah/payload`, learns its device id from `/api/local/status`, and `/api/local/events` tells it when new content (re-fetch in place) or a new app release (reload) is installed. With no content installed yet it shows a "Waiting for content" screen saying whether it is downloading or needs a USB stick or a laptop on its ethernet port.

Alt+Shift+F shows diagnostics: app and agent versions, installed content and sync status.

### Developing

`npm run dev` serves the app on port 3000 and proxies `/api` (including the `/api/local/events` stream) and `/media` to a device agent, `http://127.0.0.1:8080` by default. To develop against a real board's content, forward its agent port first:

```bash
ssh -L 8080:127.0.0.1:8080 <board>
npm run dev
```

Set `VITE_DEV_AGENT` to proxy to an agent somewhere else instead (`VITE_DEV_AGENT=http://127.0.0.1:18080 npm run dev`).

`npm test` runs the unit tests and `npm run typecheck` checks the API layer. The payload types in `src/api/schema.d.ts` are generated from the backend's `openapi.yaml` (`MusallahBoardPayload`); regenerate them with `npm run api:generate`, with LensBridgeBackend checked out beside this repository or `OPENAPI_SPEC` pointing at the spec.

### Releasing the board app

Boards only install app packages (`.mbu`) signed with the release key, so the key is created once and kept out of the repo:

```bash
# in the agent repo
mbpack keygen
# or with Node alone: prints the seed (keep secret) and the public key (give to the agent)
node -e "const c=require('crypto');const s=c.randomBytes(32);const k=c.createPrivateKey({key:Buffer.concat([Buffer.from('302e020100300506032b657004220420','hex'),s]),format:'der',type:'pkcs8'});const p=c.createPublicKey(k).export({format:'der',type:'spki'}).subarray(-32);console.log('seed: ',s.toString('base64'));console.log('public:',p.toString('base64'));console.log('keyId: ',c.createHash('sha256').update(p).digest().subarray(0,8).toString('hex'))"
```

Store the seed as the repository secret `MB_RELEASE_SIGNING_KEY`, and compile the public key into the agent (or add it with `musallahboard-agent trust add release <public key>`).

To release, bump `version` in `package.json` (`MAJOR.MINOR.PATCH`), commit, and push a matching tag (`v2.1.0`). The `Release` workflow tests, builds and signs `musallahboard-app-<version>.mbu`, and attaches it and `app-channel.json` to the GitHub release; online boards pick it up from the channel within a few hours, offline ones from a USB stick or `mbpush`.

Locally, `MB_RELEASE_SIGNING_KEY=<seed> npm run package` writes both files to `release/`. `MB_RELEASE_BASE_URL` changes where `app-channel.json` says the package can be downloaded.

## Epilogue

This README is vastly incomplete due to me focusing on development. Docs and setup instructions will lag behind actual development work for another ~month-ish or so. Rest assured, I do plan on writing high-quality docs from scratch (no AI :P) once the system is in a more stable state. For now, please feel free to open an issue if you have any questions or need help with setup. I will do my best to respond in a timely manner.
Loading
Loading