feat(info-screen): migrate the infoscherm to aurora - #283
Merged
Conversation
LarsvanDartel
marked this pull request as draft
August 13, 2026 20:02
Contributor
Author
LarsvanDartel
marked this pull request as ready for review
August 14, 2026 15:27
Wand3rerz
reviewed
Sep 2, 2026
Wand3rerz
left a comment
Contributor
There was a problem hiding this comment.
These are some things I noticed when skimming the PR, it might not be exactly as I state it but might be worth a look.
LarsvanDartel
force-pushed
the
feat/info-screen
branch
from
September 2, 2026 11:10
05383ae to
42ae9fc
Compare
Contributor
|
This also resolves #194 |
LarsvanDartel
force-pushed
the
feat/info-screen
branch
2 times, most recently
from
September 2, 2026 12:26
aac9aeb to
8d49c20
Compare
Replaces the legacy PHP info screen with a config-driven narrowcasting screen: a per-screen widget layout, edited in the backoffice and pushed to the client over the /screen namespace. Core: - Widget catalog as the single source of truth for the layout editor and the client, with per-widget settings, container widgets and modal overlays. - Per-screen layouts and reusable presets, sanitised server-side so a bad payload cannot produce an out-of-bounds grid. - Widgets can be switched off centrally (DISABLED_WIDGETS); conference rooms and the caller widgets are disabled pending a real data source. - Services widget now reads a real Uptime Kuma or Gatus instance, detected from the API the configured URL answers on, and reports Uptime Kuma's own statuses (up/down/pending/maintenance/unknown). - Keyholder registry syncs from LDAP: the union of three configured group DNs, resolved transitively, identified by objectGUID. Names may be overridden in the backoffice and survive later syncs; synced rows cannot be deleted and can be reverted to the directory's value. Client: - Widget renderer driven by the layout, on a fixed 1920x1080 grid. - Self-fetching widgets share a cache keyed on their settings, so a widget rotating back into view in a carousel repaints immediately instead of refetching from scratch. Adds the migration for the info-screen tables. Two columns deviate from the usual JSON-as-varchar convention: layout placements are TEXT (a full layout exceeds varchar(255)), and simple-array columns carry no default (MySQL rejects defaults on TEXT). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The clock floors elapsed time (at 15:59:55.4 it reads 15:59:55) while the countdowns floored their remaining time (4600ms became 00:04), so the two disagreed by one whenever there was a sub-second remainder, and a countdown reached zero a full second before the clock reached the target. Remaining time is now rounded up: with 4.6 seconds to go you are still inside the fifth-from-last second, so the countdown reads 00:05 and hits zero exactly when the clock arrives. The total is rounded once and only then split into hours/minutes/seconds, so the parts always carry (59.4s is 01:00, never 00:60). Affected the beer overlay, the beer panel's "hrs and mnts to go" and the clock's own 1 July countdown; the shared formatting now lives in countdown.ts. Also replaces three independent setInterval(1000) tickers with a shared useSecondTick hook that schedules each tick for the next whole second. Each interval previously fired relative to whenever its component mounted, so two widgets showing the same moment repainted at different offsets and could disagree briefly even with the rounding fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`pnpm format` runs `prettier --check .` over the whole repo in CI, and 22
info-screen files were not formatted. Whitespace only, no behaviour change.
The pre-commit hook did not catch these: lint-staged still matches
`./src/**/*.{ts,vue}`, which stopped matching anything when the apps moved to
`apps/*/src` for the monorepo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The glob still matched `./src/**/*.{ts,vue}`, a path from before the apps moved
to `apps/*/src` for the monorepo. It has matched nothing since, so `npx
lint-staged` silently formatted nothing on every commit — which is how 22
unformatted files reached CI.
It now matches every file type `prettier --check .` covers in CI, so the hook
enforces exactly what CI enforces, and it includes `tsx` (never covered, so the
React client was skipped even before the move).
Runs prettier only. The previous entry also called `npm run lint`, which as a
lint-staged command expands to `turbo run lint <staged files>` and would treat
the filenames as task names. Linting stays in CI: there is no root eslint config
(each app has its own flat config), so `eslint --fix` from the repo root would
not resolve them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The virtual desktop was modelled as one PC per session: every remote session
arrived under its own id and became its own pc_status row with a single user. So
rows appeared and disappeared as people logged in and out, the backoffice table
listed every session separately, and a maintenance/disable override only ever
applied to whichever session happened to be there.
It is now a single PC, `vdesktop`, that many people are logged into at once. The
poster contract is unchanged — it still reports one entry per session — and the
core folds every non-physical entry into that one row, de-duplicating users and
dropping lockedAt (which describes a session, not a shared machine). The row
persists like a physical PC instead of being deleted when idle, so it can be put
into maintenance or disabled as a whole. Leftover per-session rows are still
cleaned up once stale.
Every PC now reports `users: [{ username, symbol }]` rather than a single
username/symbol: a physical PC has zero or one, the vdesktop has many, and each
user keeps its own keyholder symbol. The map lists the vdesktop users with those
symbols and replaces the overflow with "+N more" instead of silently dropping
everyone past the fifth.
The info-screen migration is amended in place rather than adding an alter, since
it has not been released.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Anything beyond the third is counted by the existing "+N more" line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The catalog is in definition order, which is arbitrary to someone hunting for a widget in the list. Widgets now carry a category — information, the room, content, containers — and the picker groups by it, sorted by name within each group. The add-to-container picker is sorted the same way. The category is presentation only; nothing in the core or the client behaves differently per category. It overlaps with the existing `container` flag, which keeps the behaviour (holds children, no nesting), so the palette no longer has to special-case containers. A category the backoffice does not recognise still shows, listed last, so adding one in the core cannot make widgets disappear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The daily reset was persisted: getRoomStatus mutated the row, cleared the responsibles and beer time, and stamped lastResetAt. That only works if something remembers to run it, and it made a GET write to the database. Derive it instead. State whose updatedAt predates the current reset boundary is reported as a closed room with no responsibles and no beer time, so there is no reset that can fail to run and no write on read. The lastResetAt column goes with it. setRoomStatus now bumps updatedAt explicitly. TypeORM issues no UPDATE when no column differs, and since the row is no longer cleared, opening the room again with the same responsible and beer time produces exactly that no-diff save -- which would leave the state reading as stale forever. Same workaround as PcUsageService.upsert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GEWISDB now exposes the keyholder registry over its API (GEWIS/radix#68), so aurora no longer needs a bind account and three group DNs to find out who holds a key. Read /keyholders and /boards instead, using the same Bearer + X-Api-Version conventions the poster service already follows, and take the registry as the union of the two so a board member without a key is still listed. The policies carry over unchanged: an empty response is refused rather than emptying the registry, a name edited in the backoffice is never overwritten, and a manually added row with a matching name is adopted instead of deleted and recreated. Identity moves from objectGUID to the membership number. Three fields stay backoffice-owned, because the API has no notion of them and writing them would discard real work: usernames (seeded with the member's full name on a new row, since the API knows no logins), isCandidateBoard, and photoUrl. The edit dialog and updateKeyholder both allow those on a synced row and refuse the two flags the API owns. Note for deployment: GEWIS_KEY needs keyholders:read and boards:read in addition to the photo permissions it already carries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PC usage was keyed on the AD login name, which meant the keyholder
registry had to carry a list of logins per person just so a report could
be matched back to a symbol. Report the GEWIS membership number instead:
it is the same identity the keyholder sync already uses, so the match is
a number comparison and the login never leaves the reporting agent.
PcStatus.usernames (a comma-separated list) becomes PcStatus.users, a
JSON list of {memberId, name} — a physical seat holds at most one, the
virtual desktop one per session. The name is reported alongside purely to
be displayed; accounts that belong to no member send a name with a null
memberId and simply get no symbol. Keyholder.usernames is dropped
entirely, along with the seeding hack that filled it with the member's
full name.
The registry becomes read-only apart from the two things the GEWIS
records do not cover. Adding and removing keyholders now happens where
the keys are actually granted, so the create and delete endpoints are
gone; only the photo and the candidate-board flag can still be set. The
name follows the API too, which retires the override/revert machinery
(syncedName, nameIsOverridden, the revert endpoint and its button) that
existed solely to protect an edited name.
The sync also clears isCandidateBoard when it sets isBoard: a candidate
board that gets installed becomes the board, and nobody would think to
untick the box, so the row would claim both until the next election.
Adds tools/pc-usage-agent: a PowerShell agent that collects every room PC
and the shared virtual desktop and posts them in one request, reading the
membership number from the employeeNumber attribute.
Also fixes the backoffice's Member column, which read a field the API did
not expose and so rendered an em dash for every row, and makes the
keyholder list scrollable and searchable now that it is filled by a sync
rather than by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jsonTransformer.from parsed unconditionally, so a single column that did not hold JSON threw inside TypeORM's result transformer and failed the entire query -- every other row with it. Log the value and read it as absent instead: the columns are nullable and their readers already cope with null, so one bad row costs one row. Found when a SQLite synchronize treated the pc_status usernames -> users change as a rename and carried comma-separated logins into a JSON column, which made every PC-usage read fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PC-usage card in the backoffice showed a snapshot that never refreshed, and the two things it could do no longer need doing: the reporting agent marks a machine as under maintenance from its AD group, and a PC that stops reporting goes offline on its own after the staleness window. Deleting a PC was never meaningful either, since the next report recreates it. So the card goes, and the keyholder list takes its place beside the room status, filling the card height and scrolling inside it. With the card gone, PcStatus.overrideState and the PcOverride enum have no writer and no reader, and the override, delete and includeDisabled endpoints have no caller, so those go too. getAll is left deriving one thing -- staleness -- rather than layering an override on top of it. Also renames the keyholder Member column to Member ID, drops the explainer block from the edit dialog, and pads the search field so its text clears the icon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Still 2 hrs and 32 mnts to go" abbreviated two words that did not need it, and read badly at the edges: "2 hrs and 0 mnts", "1 hrs and 1 mnts", and "0 hrs and 0 mnts" in the last minute. Say it in words instead, dropping a zero part and using the singular where it belongs. The phrasing lives in countdown.ts next to the other remaining-time helpers, so it is unit tested rather than buried in the panel, and it keeps the module's round-up behaviour: 90 seconds left reads as two minutes, matching the countdown ticking beside it. Also registers PrimeVue's IconField and InputIcon, which the keyholder search box used without them ever being registered, so they rendered as unstyled unknown elements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stub-services.spec.ts tested only ConferenceRoomsService; the name was a leftover from when it was meant to cover several stubbed services, and nothing else ever landed in it. Every other spec in the directory is named after the file it covers, so name this one too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… its own useCachedResource seeds from the screen-wide broadcast whenever its cache is cold, which is right for a widget following that broadcast and wrong for one pointed somewhere else: a services widget on another status page showed the GEWIS services for a beat before swapping, and the same went for a trains widget on another station and a weather widget on other coordinates. Briefly wrong-but-plausible is worse than briefly empty, because nothing on screen says which one you are looking at. So the broadcast is only passed as the starting value while the widget's settings still match what the core broadcasts; otherwise the widget starts empty and fills in when its own response lands. Also turns an unrecognisable status page into a 502 naming the URL rather than a bare 500: the message already explained what was wrong, but a plain Error threw it away, so a mistyped page looked like a server fault. Sorts the backoffice keyholder list board, then candidate board, then the rest, by name within each group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A full name rarely fits a seat on the workstation map. Derive the given name, adding as much surname as it takes to tell two people apart, and let the backoffice override it per keyholder. The agent sends given names for everyone else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every matched row went into the update plan, so a steady-state run reported the whole registry as updated and saved each row for nothing. Compare the fields the sync owns first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7GFLi8MLxJUkjKbMEoD7H
LarsvanDartel
force-pushed
the
feat/info-screen
branch
from
September 4, 2026 11:11
a4e9bb5 to
6639bc7
Compare
…n its name The save dialog opens pre-filled with the selected configuration's name, so reusing one is the normal case, not a mistake. Overwrite it, reject a duplicate name with a 409 rather than a constraint error, and give the catch-all 500 a body so the toast is no longer blank. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7GFLi8MLxJUkjKbMEoD7H
DTSTART;TZID=Europe/Amsterdam was parsed in the server's zone, so every event in a UTC container landed an hour or two late — enough to move an evening event to the wrong day. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7GFLi8MLxJUkjKbMEoD7H
A booking is today's when it overlaps today, not when it starts today: at 00:30 a 23:00-01:30 booking read as free. The timeline also headed itself with the count for every room while showing a selected few. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7GFLi8MLxJUkjKbMEoD7H
The name was the key, and the sync overwrites a keyholder's name whenever the GEWIS records change it — the match then failed silently and the photo and board flags blanked out. The backoffice search accepts either the name or the member ID. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7GFLi8MLxJUkjKbMEoD7H
Only six named entities were handled, so "Pippi’s" reached the ticker unchanged. Resolve named and numeric references in one pass, including the Windows-1252 block feeds mislabel as Unicode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7GFLi8MLxJUkjKbMEoD7H
…roke The search field shares a component with the table, so each letter rebuilt the filtered array and re-rendered all 39 rows. Settle the term first and key the rows by id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7GFLi8MLxJUkjKbMEoD7H
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.






Description
Migrates the legacy PHP infoscherm (
infoscherm.gewis.nl) into an aurora screen handler, so it is configurable from the backoffice.How it works
The screen is modular and configurable. From the backoffice it is possible to set information on the info screen as well as modify the widget layout and settings. The layout is a grid, each widget has a predetermined minimal and maximal size and can be placed anywhere on the grid. The background of the info screen is configurable. The style of each widget is configurable, as well as the widget-specific settings for each placed widget.
Widget coverage
Everything the legacy screen showed is ported: clock, weather, train departures, news ticker, agenda, workstation map, room responsibles, beer time, coffee status, and Spotify.
Additional widgets were added, such as the GEWIS services, text/image/logo widgets and two widget containers (carousel and status stack).
The groundwork for two widgets has been added but by lack of a good data source, they are disabled for now: conference room availability, and the caller widgets.
Keyholder sync
Keyholders are synced from the new Gewis API
Related issues/external references
Closes #275, #194
Types of changes