Platform rights as a spine of their own
Who may operate the platform has been three different answers in three different places — a system_user_access grant table, an owner check copied into each plat...
Move the callback marker beside the contact name, add a Callback filter
The marker sits with the name because that is who the call is to, rather than in the id column where it pushed the ids out of line. The filter offers All / Sch...
Upload your own sound effects, at all three layers
The system could only ever play the thirty-nine tones it shipped with. Now any layer can add its own file — the platform's catalogue, an organization's, a membe...
List, rename, retire and stream uploaded sound effects
Bytes arrive through ConversionService (which owns ffmpeg and the scanner); everything afterwards is here, where the session and the scoping rules already live....
Transcode uploaded sound effects to Opus
Extends the ffmpeg wrapper the VoIP recording pipeline already had — this image installs ffmpeg with libopus for it, so putting a second ffmpeg in another image...
Uploaded sound effects, and the storage domain that counts them
EVENT_SOUND_ASSETS: a sound file somebody uploaded, ready to be assigned. Scope mirrors EVENT_SOUND_PROFILES exactly — same three layers, same SCOPE_ID conventi...
Let the sender edit the meeting invite email subject
The invitation subject was fixed at "<host> from <org> invited you to a meeting" and built inside the send route, so nobody could say what the meeting was actua...
Gate the personal-loan proxy on the PLOS rights
KamoLOS enforces nothing — no rights check anywhere in its source — so this proxy is the authorization boundary for the module, the position denyUnless occupies...
A kind for the meeting that is about to start
MEETING_REMINDER, so MediaService can raise the start-time nudge as a notification instead of a popup only it knows about. Its own kind rather than CALENDAR_EV...
Subscribe to the course count instead of asking for it every minute
NavTop sums two counts into one obligation badge, and only the legal half was live. A member assigned a course — or excused from one by HR — kept the old number...
Relay the outstanding-course count onto the member's own topic
The twin of the legal relay, because NavTop sums the two counts into one badge and a pair of halves that behaved differently would give the member chrome that w...
The outstanding-course badge moves when the course does, not a minute later
The NavTop obligation badge sums two counts — packages to sign and courses to take — and only one of them was live. The legal half has pushed since its NATS blo...
Publish every change of state, not only arrivals
A member has this open in more than one tab about as often as not, and often on a phone as well. Each of those holds its own socket and draws its own bell, and ...
The bell tells the truth in every tab, not just the one you clicked in
The nav-top bell was live for arrivals and nothing else. Reads, dismissals, snoozes and deletions were applied to whichever tab the member clicked in and announ...
Drop the three chat-channel rights from the frontend
Mirrors the shared-lib deletion: the enum mirror, its registry array, both hierarchy edges (ACCESS_CHAT -> CREATE_CHANNELS -> MANAGE_CHANNELS and ACCESS_CHAT ->...
Purge stored rows for the deleted chat-channel rights
Ids 74/75/76 no longer resolve. RoleRightsSyncService would sweep them on some future SecurityService boot anyway — silently, at an unpredictable moment, with n...
Delete the three chat-channel rights — 196
CREATE_CHANNELS (74), MANAGE_CHANNELS (75) and MODERATE_CHAT (76) gated nothing and could not be implemented as written: chat is real but the platform has no ch...
Move notes from account ownership onto membership
Adds notes.member_id and attaches each note to its owner's earliest membership — the organisation the account has belonged to longest, which in practice is wher...
One event-sound system, three layers, wired to what actually fires
There were two sound systems and neither knew about the other: four mp3s behind useChatSounds, and four synthesised tones behind notificationSound with a five-o...
Read and write the three layers of the event-sound stack
GET returns all three layers in one round trip plus what the caller may edit — every settings page needs the whole stack, because an org administrator editing t...
One table for the three layers of the event-sound stack
Platform defaults, an organization's overrides and a member's own are the same shape, so they are the same table: a scope, the id that scope is about, and the s...
Give the deck its slide back, without the lurch
Dropping `layout` to stop the card lurching onto the edge of the screen also took the sibling slide with it: dismissing a card left the rest snapping into its p...
Stagger the cascade, add branch bulk actions, explain the rule
The cascade highlight now travels the chain instead of flashing every affected row at once. enabledAncestors already arrives nearest-first, so the index IS the ...
Show and enforce the permission hierarchy in all five editors
Five editors granted 199 rights as five flat lists of checkboxes, and none of them showed that one right can be a prerequisite for another. Granting a child wit...
Normalise member overrides against the hierarchy
Member direct rights now go through RightsHierarchy.normalizeLevel before they are persisted: a granted child pulls its ancestors up to GRANTED at the member le...
Backfill ancestors so closure cannot remove access
MemberRightsAppliedService now denies any right whose parent chain is not granted. Every row written before the hierarchy existed knows nothing about ancestors,...
Parent/child hierarchy for all 199 rights
RoleRightType has declared a 'parent' field and a public getParent() for years. Every constant passed null and nothing ever called it. This finishes that scaffo...
Repaint the reminder in the org's colour, open its options in place
The card is the organisation's primary colour now, with every mark on it drawn in --on-primary rather than a literal white — a white-label org that picks a pale...
Legal & Compliance settings tab with the org default legal text
A new tab on /settings/features/hrs holding Default Legal Text, linked from the settings menu and the settings launchpad. Creating a package on /hr/legal-compli...
Rich text in the sticky and the previews, and a mode you can switch
A note now says whether it is rich text or plain text, and both the editor and the floating sticky can switch it. The floating note in rich mode embeds the ver...
Carry a note's rich-text / plain-text mode through the API
Create honours the requested mode and defaults to rich, matching the editor a note is created in. Update is null-guarded like every other field on that endpoint...
Add the is_rich_text column, defaulted and backfilled to rich
Whether a note is edited as rich text or plain text cannot be read back out of its content: a rich note nobody has formatted yet is byte-for-byte a plain one. W...
A note records whether it is rich text or plain text
The mode cannot be derived from the content. A rich note the member has not formatted yet serializes byte-for-byte identically to a plain one — both are a Lexic...
One page for the notifications and the controls over them
The notifications page wore a header of its own — an eyebrow, a title and three pill buttons inside a bordered card — while every neighbouring page (the account...
GET/PATCH the org's default legal text
/api/docs/legal/settings, behind the same guarded() funnel as every other mapping on this controller. The read NEVER creates a row: it resolves to LegalDefault...
Seed every organization a default legal text
One hr_legal_settings row per org, unique on org_id, carrying **************** INSERT-ONLY, never an UPDATE. An org that has edited its own wording owns it; a ...
Org-level default legal text
LegalSettings is one row per organization carrying the wording a NEW legal package starts with. It is a DEFAULT, never a live reference: creating a package copi...
The history becomes a page, and the bell rings
The bell opened a popover, which is the right shape for "what just happened" and the wrong one for "what did that reminder say on Tuesday". A panel hanging off ...
A paged, filterable history
The centre could answer one question: the most recent hundred, unread or not, with no way to ask for what had already been read. So anything a member had seen w...
Let the history be queried by specification
The browsable history filters by kind and by whether a notification has been read, and those combine freely — which as JPQL is a query per combination, or one q...
Rebuild /notes as a paper wall with a split-pane editor
The page was the last screen still on raw Tailwind blue-600/gray-50 while everything around it moved to the house system, and every one of its ~100 `dark:` vari...
Pin and unpin a note from its floating sticky
The sticky could recolour, rename, minimize, maximize and close a note but not pin it — so pinning meant going back to /notes for the one thing the note is alre...
One quiet-hours window, governing both notification channels
There were two. The pair near the top of the page silenced the in-app cards; the pair at the bottom silenced the missed-message emails. Nothing on screen said t...
Two meanings of 100% — a reading width, and the whole pane
The percentage reset to one baseline: the page grown to the pane but capped at 920px, which on a wide monitor leaves the sheet floating in a lot of empty desk. ...
Refresh the NavTop clock the moment the editor saves
Voiding the punch a member is currently clocked in under already leaves them OFF server-side: RosterService.clockState reads the latest LIVE punch and skips voi...
Zoom the document, and grab it to move it
A floating capsule in the bottom-right of the document pane: −, the current percentage, +. The READOUT IS THE RESET — a separate reset button would be a third c...
Pinned notes on the launchpad, colour rotation, and a proper send-off
Three things. Pinned notes reach the home launchpad. Every note pinned on /notes gets a chip there in its own sticky colour, with a short preview; clicking one...
Notification settings across the width, with a working model
It was one 760px column of switch rows, which reads as a form to fill in rather than something to tune — and on a wide screen left two thirds of the page empty ...
Notifications become a tab on the member's own profile
They were a page of their own at /settings/notifications, which put a preference about one person somewhere other than the page about that person — and left the...
Like what you see shipping?
Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.