Conversation
* feat(info-screen): config-driven info screen with LDAP keyholder sync
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>
* fix(info-screen): countdowns ran one second ahead of the clock
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>
* style(info-screen): apply prettier to the info screen sources
`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>
* build: fix the lint-staged glob so the pre-commit hook runs again
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>
* feat(info-screen): make the vdesktop one PC with many users
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>
* feat(info-screen): limit the vdesktop list to three names
Anything beyond the third is counted by the existing "+N more" line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(info-screen): group the add-widget picker by what a widget is for
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>
* feat(info-screen): fix reset method
* feat(info-screen): derive the daily room reset from updatedAt
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>
* feat(info-screen): sync keyholders from the GEWIS API instead of LDAP
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>
* feat(info-screen): identify PC users by membership number
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>
* fix(core): do not fail a whole query on one non-JSON column value
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>
* refactor(info-screen): drop the manual PC controls
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>
* feat(info-screen): spell out the time left until beer time
"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>
* test(info-screen): name the conference-rooms spec after its subject
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>
* fix(info-screen): do not paint the broadcast in a widget that fetches 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>
* feat(info-screen): show keyholders under a short display name
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>
* fix(info-screen): only count keyholders the sync actually changed
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
* fix(info-screen): replace a layout configuration instead of failing on 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
* fix(info-screen): read iCal timestamps in the zone the feed names
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
* fix(info-screen): keep overnight bookings and scope the timeline summary
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
* fix(info-screen): identify the room responsibles by membership number
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
* fix(info-screen): decode every HTML entity in a news headline
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
* perf(info-screen): stop the keyholder table rebuilding on every keystroke
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
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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
Related issues/external references
Types of changes