Skip to content

feat(info-screen): migrate the infoscherm to aurora - #283

Merged
Wand3rerz merged 24 commits into
developfrom
feat/info-screen
Sep 4, 2026
Merged

feat(info-screen): migrate the infoscherm to aurora#283
Wand3rerz merged 24 commits into
developfrom
feat/info-screen

Conversation

@LarsvanDartel

@LarsvanDartel LarsvanDartel commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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

  • New feature (non-breaking change which adds functionality)

@LarsvanDartel
LarsvanDartel marked this pull request as draft August 13, 2026 20:02
@LarsvanDartel

Copy link
Copy Markdown
Contributor Author

A new look for the info screen (just an example)
Screenshot 2026-08-13 at 22-17-48 Aurora

Beer time countdown
Screenshot 2026-08-13 at 22-18-16 Aurora

Room status screen in backoffice
Screenshot 2026-08-13 at 22-19-09 Aurora

Manage layout page
Screenshot 2026-08-13 at 22-19-24 Aurora

Example settings, can add custom news feeds etc
Screenshot 2026-08-13 at 22-25-58 Aurora

Another example config
Screenshot 2026-08-13 at 22-24-33 Aurora

@LarsvanDartel
LarsvanDartel marked this pull request as ready for review August 14, 2026 15:27

@Wand3rerz Wand3rerz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/aurora-core/src/modules/handlers/screen/info/ldap-keyholder-sync-service.ts Outdated
@Wand3rerz

Copy link
Copy Markdown
Contributor

This also resolves #194

@LarsvanDartel
LarsvanDartel force-pushed the feat/info-screen branch 2 times, most recently from aac9aeb to 8d49c20 Compare September 2, 2026 12:26
@Wand3rerz Wand3rerz linked an issue Sep 3, 2026 that may be closed by this pull request
LarsvanDartel and others added 18 commits September 4, 2026 12:56
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
…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
LarsvanDartel and others added 5 commits September 4, 2026 13:56
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&#8217;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
@Wand3rerz
Wand3rerz merged commit 1a55a99 into develop Sep 4, 2026
2 checks passed
@Wand3rerz
Wand3rerz deleted the feat/info-screen branch September 4, 2026 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Migrate old infoscherm to aurora [Feature]: Room responsible functionality

2 participants