← HR Cluster

Changelog

Operational and technical updates to the HR Cluster platform.

Types of changes
Added New functionality or modules.
Updated Improvements or changes to existing functionality.
Core Major architectural, platform-wide, or operational changes.
Deprecated Features scheduled for removal.
Removed Features or functionality removed.
Fixed Bug fixes and issue resolution.
Security Security improvements or vulnerability fixes.

2026

— continued

Supplier portal • Each proposal row says what the delivery is

In the supplier portal, each row under Your proposals now states the delivery it concerns: the delivery's description, as the delivery now stands once approved or as you proposed it while it waits, and the operator's delivery instructions, with No description registered or No instructions registered when nothing has been written. Until now a row named only the proposal, so a supplier with several open proposals had to open each to see which delivery it was about. A proposal for a new delivery that is still waiting states no instructions, because the delivery does not exist yet for an operator to have written any. The lines read exactly as they do on the list of your existing deliveries. Help updated in all four languages.

Technical focus: The proposal rows print the SAME description and instructions lines as the existing-deliveries list on the portal, with the description resolved from the delivery for an approved proposal and from the proposed payload otherwise (falling back to the delivery's own), and each absence stated; the guard pins the portal as stating instructions in three places, so a fourth surface on the portal must print the same lines rather than its own.

Billing • The Contact us button always opens a working address

On Settings and Billing, the Contact us and Request activation buttons beside a plan or an add-on open the address HR Cluster has set for it. When that address had been stored without its https prefix, the button opened a page under your own workspace instead of the address it named, and landed on nothing. The button now opens the stored address only when it is a real web address or an email address, and otherwise opens an email to HR Cluster support, so it always leads somewhere that answers. Nothing about your plan, its price or its limits changed.

Technical focus: ONE rule, billing_contact_url_valid() in the billing core, decides what a plan or add-on contact URL may be (an absolute http or https address with a dotted host, or a mailto with a real address). The platform-admin editors refuse anything else on save, and the three customer button sites ask the same rule before rendering a STORED value and fall back to the support mailto, so a row written before the rule existed is judged at render time rather than trusted.

Deliveries • The request and approval emails describe the delivery

The email that asks a driver or a supplier contact to complete a delivery now states the delivery's description and its location beside the expected arrival and the instructions, and the email that tells them the delivery was approved states those and the gate control the delivery carries. Each is always stated: when nothing is registered the email says No description registered, No location registered or Not registered rather than leaving the line out, so an absence is visible. A rejection email carries none of them. The emails that announce a proposal now say Proposed arrival only when the proposal asks for a different time; an update that leaves the arrival alone reads Arrival, and a proposed new delivery always reads Proposed arrival. Help updated in all four languages.

Technical focus: Every delivery email reads the description, the location and the gate control through ONE helper over the delivery as it now stands, so a fifth email cannot describe it differently, and each value carries its own stated absence. Whether an email says Proposed arrival is decided by ONE pure rule over the proposal's canonical window form, the same form the review dialog compares, so the email and the reviewer agree on whether the time changed.

Phone numbers • Country codes for Lithuania, Latvia, Estonia, Croatia, Slovenia, Slovakia, Bulgaria, Serbia and Bosnia

The country-code picker on every phone field now offers Lithuania, Latvia, Estonia, Croatia, Slovenia, Slovakia, Bulgaria, Serbia and Bosnia and Herzegovina, and every phone field offers the same list: a person's profile in the workspace and on their own page, the complete-your-profile step, the public registration forms, the public signup link, the missing-information request, a supplier contact, and a delivery's driver and contact on the delivery window, the public delivery registration link, the information-request page and the supplier portal. Until now those fields kept their own lists: the public registration, signup and missing-information forms offered thirteen countries and lacked Romania among others, and now offer the full list, Norway first. A number already stored with one of the new codes was shown under +47 with the country code left in the local part; it now opens under its own code. Nothing changes for numbers on the codes that were already offered.

Technical focus: ONE country-code list, phone_codes() in the core phone-input helper, is read by all seven phone pickers; the suppliers helper and the six other copies now delegate to it, so a code added once reaches every field in the same change, and a guard sweeps the tree for a second list. A stored number is re-split against that list on open, which is why a code missing from a field's old list fell back to the Norwegian default with the digits left in the local part.

Supplier portal • A delivery always states its instructions

In the supplier portal, a delivery's instructions were shown on the delivery list row and in the planned-details block only when some had been written; with none, the line was simply missing, so a supplier could not tell whether there were no instructions or the line had been left out. Both places now always show the Instructions line, reading No instructions registered when nothing has been written, as plain information rather than a warning. That is the same rule the production plan, the checklist, the supplier plan, the Deliveries list, the PDFs, Plan Live and the delivery emails already follow, so a supplier and the office now read a delivery's instructions the same way. Help updated in all four languages.

Technical focus: The supplier portal view renders the instructions line unconditionally on the list row and in the planned block, printing the same stated absence every other reader prints, styled as information and never as a missing value; the guard pins both rows as always stated, and the portal was rendered through the real front controller to prove both. With this, every reader of the instructions field states the absence rather than omitting the line.

Production Plan • Set dangerous goods and gate control from the plan

The Dangerous goods (ADR) and Gate control actions added to the Deliveries list earlier today are now on the production plan's selection bar as well. They appear while every selected row is a delivery or a return, and disappear as soon as any other row is ticked. They change the deliveries those rows belong to, exactly as on the Deliveries list: only the chosen field changes, and if one delivery cannot take the change, none of them are changed. Help updated in all four languages.

Technical focus: The plan fetches the panel from the Deliveries page and posts its item ids to the same endpoint, which maps them to their deliveries and refuses the whole set if any row is not a delivery or return. One renderer and one handler serve both surfaces.

Deliveries • The approval email always states the instructions

The email that tells a supplier or a public registrant their delivery was approved carried the delivery instructions only when some had been written; with none, the row was simply missing, so the reader could not tell whether there were no instructions or the row had been left out. An approval email now always shows the Delivery instructions row, reading No instructions registered when nothing has been written, in the recipient's language, the same rule the information-request email, the production plan, the Deliveries list, the PDFs and Plan Live already follow. A rejection email still carries no instructions row: the delivery it concerns was not approved, so there is nothing for the driver to act on. Help updated in all four languages.

Technical focus: The decision template renders the instructions row unconditionally on the approved branch, printing the same stated absence every other reader prints, resolved in the recipient's language; the rejected branch is the ONE deliberate omission and the guard pins both, mutation-tested against the old conditional. With this, every reader of the instructions field states the absence rather than omitting the line.

Morning briefing • A reply reaches the Ops mailbox

The morning briefing email, the automated 06:00 message with the day's production plan documents and each person's checklist, is sent by the Production Plan, an Ops module, but a reply to it was addressed to the Workforce crew mailbox instead of the Ops one. Anyone who answered the briefing, to say a task was done or to ask about a delivery, reached the wrong inbox. Replies to the briefing now go to the Ops mailbox, the same place replies to every other Ops email go. Nothing about the briefing's content, timing or recipients changed.

Technical focus: The briefing sender names its platform (ops) and its account explicitly when it calls the mail layer, which is what resolves the Reply-To; a sender that leaves the platform unset gets the Workforce default, and an Ops module must never rely on that. The briefing guard asserts both arguments on the call, comment-stripped.

Deliveries • Set dangerous goods and gate control on several deliveries at once

The selection bar on the Deliveries list has two new actions beside Responsible, Department, Location, Supplier and Label: Dangerous goods (ADR) and Gate control. Select the deliveries, choose one, and set whether they carry dangerous goods with an ADR class, or which gate control applies (the delivery's own controls, no control, or a chosen set of checks). Only that field changes on each selected delivery, and if one of them cannot take the change, none of them are changed. Leaving the dangerous goods description blank keeps each delivery's own description. Help updated in all four languages.

Technical focus: The bulk action writes through the ONE delivery writer with only the chosen field's keys, so its presence rule leaves every other field of each delivery untouched; the whole selection runs in one transaction and a single refusal rolls it back. The panels reuse the delivery modal's own dangerous goods block and control group, so the bulk choices cannot differ from the modal's.

Deliveries • The information request email always shows the instructions

The email you send with Request information on a delivery now always includes a Delivery instructions row. When you have written instructions for the delivery, the row shows them; when you have not, it says No instructions registered, so the person delivering knows there are none instead of wondering whether they were left out. Until now the row appeared only when instructions had been written. The row is shown in the recipient's language. Help updated in all four languages.

Technical focus: The information-request email now follows the same always-stated rule as every other surface that shows delivery instructions (the plan, the Deliveries list, the PDFs and Plan Live): the row is emitted unconditionally, with the shared No instructions registered string when the delivery has none. The approval email to a supplier is unchanged and still shows the row only when instructions have been written.

14 changes · 3 added · 7 updated · 4 fixed

Vehicles 4 · Forms 4 · Clothing & sizes 4 · Production Plan 3 · UI 2 · Infrastructure 1 · +10 more

Plan Live • A live link shows the delivery instructions

A Plan Live link at the Standard or Full detail level now prints a delivery's instructions under the delivery reference, as the first line of the delivery block, exactly as the production plan does: Instructions followed by what you wrote, or No instructions registered when nothing has been written, so a crew reading the plan on a phone at the gate sees the same instructions the office sees. A Minimal link still shows only the bare delivery reference and no delivery lines. Help updated in all four languages.

Technical focus: Plan Live rebuilds the delivery block at the LINK's own level through the ONE delivery block formatter; it now hands the formatter the delivery's instructions after the minimal return, so the line lists at standard and full and never at minimal. The attach payload carries the raw instructions for this one reader (every other reader prints the pre-built lines), and the public projection grew no key: the line rides inside the existing lines list the page already renders generically.

Page titles • A name with an ampersand or an apostrophe reads correctly in the tab

On a person's page, a registration form's page and a message's page in the workspace, and on the public incident report, registration and contract signing pages, a name containing an ampersand or an apostrophe was shown in the browser tab and the page heading as its HTML code: Oslo Pride & Friends read Oslo Pride & Friends, and O'Brien read O'Brien. The name was stored correctly; only the title was encoded twice. Every one of those titles now reads as written. Nothing else on the pages changed.

Technical focus: The page title variables are plain text and the ONE page head escapes them once; a page must assign the raw value and never pre-escape it, which is what twelve sites did. A static guard now sweeps every page in the tree for a pre-escaped title assignment, with the heads pinned as the positive control, so a new page cannot reintroduce the double encoding.

Deliveries • Instructions travel in the plan CSV, and their absence is stated

The delivery instructions published earlier today now go with the delivery in the production plan CSV: the export writes them in a new column, Leveringsinstruksjoner, and the import reads it back, so a plan exported, edited in a spreadsheet and imported again keeps every delivery's instructions. The column comes last, after the five existing delivery columns, so an older sheet without it imports exactly as before. Rows that share a delivery reference must agree on the instructions, like the other delivery-owned columns, and the import preview shows them. The Instructions line is also always printed under the delivery reference, on the production plan, the checklist, the supplier plan, their PDFs, the morning briefing and now the Deliveries list row too: when nothing has been written it says No instructions registered, so a missing instruction is visible instead of silent, the same way a missing description already is. The list row updates in place when you save a delivery. Help updated in all four languages.

Technical focus: The CSV column is appended LAST in the export header and the import alias table, so every older column keeps its position and a sheet without it reads as before; rows sharing a Leveringsref must agree on it (group_field_mismatch), the rule the other delivery-owned fields already follow. The ONE delivery block formatter takes the instructions as ?string: NULL prints no line (Plan Live calls it so), an empty string states the absence, never as a warning; every plan-family reader and the Deliveries list row inherit the line with no renderer of their own.

Deliveries • Write delivery instructions for the driver

A delivery now carries Delivery instructions, written under its description in the delivery window on both the create and the edit form: which gate to use, who to call on arrival, where to park. Only you write them; a supplier or a public registrant cannot. The people who deliver read them where they already look: in the supplier portal, on the page an information request opens, and in the information-request email and the approval email. They are not shown on the public registration link, where the delivery does not exist yet. The production plan, the checklist and the supplier plan print them as Instructions on a line of their own directly under the delivery reference, on screen, in the PDFs and in the morning briefing, so a driver's instructions travel with the delivery everywhere it is listed. Help updated in all four languages.

Technical focus: The column is operator-only by construction: it is not part of the delivery proposal vocabulary, so no supplier or public submission can set it, and the edit handler writes it by presence so a form from the previous deploy does not clear it. Every plan-family reader prints it through the ONE delivery block formatter, delivery_block_lines(), as the first line under the reference; the screen, the row re-render mirror, the plan, supplier and checklist PDFs and the briefing inherit it with no renderer of their own, and Plan Live calls the formatter without it.

Registration forms • Choose which sizes a form asks for, and require an answer

A registration form that asks for clothing sizes can now say which items it asks for: all the items the account collects, or a selection you tick, and for each item whether an answer is required. On a required item the registrant must pick a size; Not needed is not offered and a blank answer is refused with the item named. When a person has given their sex, the sizes offered follow it, and a size from another fit is refused. This applies on the form's own link and on a signup link's registration step alike. A form set to Selected items with nothing ticked asks for no sizes, and switching back to All items clears the selection. Forms already asking for sizes keep asking for every item, optional, exactly as before. Help updated in all four languages.

Technical focus: Two stores, one setting: the form's scope (all | selected) is the mode and a junction of ticked items with a required flag per row is the set under selected, with one required switch under all; choosing all clears the set. ONE writer saves the configuration, ONE partial renders the offer (withholding Not needed on a required item) and ONE validator refuses a blank, a Not needed or an unknown id on a required item and a size of the other sex's fit, run by both public pages before anything is written.

Schedule • Each time row on Add shift is its own shift

When you add several shifts at once, each row under Shift times now carries its own role, role label and minimum age, so one Add shift can create a bar shift, a security shift with an age requirement and a rigging shift in one go instead of three passes. The dates, the location, the meeting point, the project, the signup setting and the notes stay on the form and apply to every row. Every row is checked before anything is created, so if one row is refused nothing is written and the form stays open with the reason shown. Items follow the role: with the items list untouched, each shift gets its own role's standard items, while an edited list still applies to every shift. On a wide screen the rows are a grid; below tablet width each row is a labelled card, and on a phone From and To share the first line. Help updated in all four languages.

Technical focus: The per-row fields travel as slots[i][title|shift_role_id|role_label|min_age]; the batch fields stay top-level. Every row is validated before the first INSERT, so a refused row writes nothing. A form from the previous deploy, posting a top-level title and min_age, still creates the shift it describes; a later change to the handler must keep both shapes. An untouched items list resolves per shift from that row's role defaults, an edited list is applied to every shift.

Competence • The filters sit above the figures, as on Food and Clothing

On the Competence overview the summary figures sat above the filter bar, while Food & allergies and Clothing & sizes show the filter bar first and the figures beneath it. Competence now reads the same way: scope and status filters at the top, the figures for the people they select below. The Type filter also took half the row from the search box; it is now its natural width and the search takes the remaining space, exactly as on the other two overviews. On a phone the toolbar sits above the figures and nothing runs off the screen. Who is listed and what the figures count are unchanged.

Technical focus: The three Workforce overviews (Competence, Food & allergies, Clothing & sizes) share ONE member scope and ONE toolbar primitive; the page order is toolbar first, then the KPI grid, and a fourth overview should follow it rather than choose. The Type and Status selects opt out of the toolbar's shared flex-grow by a rule keyed on the form's id, the shape the Clothing toolbar already used, so the search keeps the slack.

Registration forms • The clothing sizes section renders correctly

On the public registration forms, the Clothing sizes section published earlier today did not look like the same section on a person's own page: the Not needed choice showed as a thin sliver instead of a tick box, and the size chips were drawn small and muted, in the style of a field label. The page's own label styling was overriding the section. The section now renders as intended on the form link and on a signup link's registration step: a proper tick box for Not needed and full-size chips in the ordinary text colour. Nothing changed for people who had already registered; their answers were stored correctly.

Technical focus: The chip primitive's layout rules are scoped under its own fieldset so they outrank a host page's generic label rule (a .form-group label selector on the public forms) instead of relying on being bare; the chip states its own margin and the box its own display. A host page that wraps the partial in a labelled group must not need a page-local override, and the guard pins the scoping with the app-wide rule as the positive control.

Sign-in • The cookie banner no longer covers the Sign in button

On a short screen, a phone with the keyboard open, a phone held in landscape, or a small laptop, the cookie consent banner sat on top of the Sign in button, so tapping the button landed on the banner instead. The page now leaves room for the banner while it is showing: the sign-in box centres above it and a field you move into is scrolled clear of it. Once you have answered the banner the room is released. The same applies on every page that shows the banner.

Technical focus: While the banner is visible, the cookie script measures it and publishes its height to the page (a class on the root plus a length variable); the stylesheet reserves that height as bottom padding and scroll padding on the body, so centring and scroll-into-view stop above it. Cleared with consent. A page-local layout must not fix its own bottom padding over this.

Deliveries • The list names a delivery the way the plan does

On the Deliveries list, a row printed the bare delivery reference under its title, while the Production Plan prints the reference together with the delivery's description, D-0006: Tent material, and states No description registered when there is none. The list now reads the same way, so a delivery looks the same whether you meet it on the plan or on the Deliveries page, and a delivery with no description says so instead of looking finished. The line updates in place when you save a delivery.

Technical focus: The reference line is built by the same formatter the plan uses (reference plus description, with the stated absence), and the delivery save response carries the rendered line so the in-place row swap cannot drop it. A third surface that prints a delivery reference should call the formatter rather than join the two fields itself.

Registration forms • Ask for clothing sizes when people register

A registration form can now ask for clothing sizes. The Clothing sizes toggle sits with the form's other field toggles and is offered only while the Clothing & sizes module is on and at least one item is collected; a form that has it switched on keeps that setting if the module is turned off and on again. With it on, both public registration surfaces, a form's own link and a signup link's registration and reactivation steps, show a Clothing sizes section listing the items asked of the group the person is registering into, each with its sizes and a Not needed choice. Answering is optional: an item left blank simply shows as missing on the Clothing & sizes overview afterwards, where a size request can be sent. Nothing is written to the person's sizes until the registration is approved; the review dialog lists the sizes they picked, and for a returning member the answers follow the same Update the member with the submitted details choice as the rest of the submission, so unticking it keeps the sizes already on record. A size that was removed from an item between registration and approval is skipped and named in the approval message rather than stored. Help updated in all four languages.

Technical focus: Answers are STAGED on the submission (form_submission_clothing, a typed pair per item, a size accepted only from the offered item's own active sizes) through ONE writer, forms_stage_clothing(), and reach clothing_user_sizes only inside create_account_user_from_submission() at approval, after the membership and PIN writes, gated on the same keep_profile choice as the profile fields, as the person's own answer (updated_by NULL). Both public surfaces derive the offer through forms_clothing_offer(): the form toggle AND the module AND at least one active item in scope for the registrant's group. The toggle is written by its own UPDATE only when it was offered, never defaulted, which is what keeps a stored 1 through a module cycle.

Departments • Send a clothing & sizes request to a whole department

A department's bulk actions now include Send clothing & sizes update request, the same row a group gained on 23 September. It reaches the department's own members who are asked for at least one item, have an email address and have not opted out of email, and skips anyone who already has an open request; the preview counts exactly who that is before you confirm. As with every department action, a sub-department's members are not included: a department holds its own member list, and you send from the sub-department to reach them. Each person receives a link to fill in or review their own sizes without logging in, the emails go out in the background, and a request is not sent again while an earlier one is still open. The row appears when the Clothing & sizes module is on and needs access to it. Help updated in all four languages.

Technical focus: ONE eligibility filter for every container, clothing_bulk_eligible_members() in the clothing module: the group and department loaders hand it their own member sets (the department loader is DIRECT members, the module's no-roll-up rule) and it decides who is askable, so the two cards cannot disagree. Same shared bulk-request engine and live-request rule as the group row; a third container is a loader over the same filter.

Workspace • Row highlighting fills the card on every details table and list

The edge-to-edge row highlight published on 23 September for the details tables on a person, a project or a contract now applies everywhere the same kind of table appears. Key/value tables and list rows inside cards and dialogs across the workspace, among them the module access card on every settings page, the supplier categories, the schedule roles, the assignee rows in the shift window, a signup link's details, an item's history, a read-only delivery and the import reconciliation, no longer show a highlight that stopped short of the card's edges. On a phone the highlight covers the whole stacked row. Three tables where a highlight meant nothing, the vehicle specification toggles on a supplier, the comparison shown when a removed member re-registers, and the power analytics report, no longer highlight at all. Nothing moved: labels and values start exactly where they did, and no page scrolls sideways.

Technical focus: ONE bleed mechanism in main.css: the container that owns the padding sets --bleed (a card sets its card padding, a dialog body its own, and the value is inherited), and the bleed rule reads it, so a new padded container sets one variable and never a second copy of the rule. Structural selectors cover the common placements; table.table-bleed is the explicit opt-in for a table one unpadded wrapper deep. The phone stack moves the inset onto the row.

Public API • The changelog feed returns every module tag the page shows

The public changelog feed at /api/public/changelog carried its own copy of the module vocabulary, and it had fallen behind the changelog page: an entry tagged with any of fifteen newer areas (Accreditation, Suppliers, Vehicles, Risk, Clothing & sizes, Departments, Production Plan, Projects, Items, Power, Users, Statistics, Settings, Import and Mobile) showed its tag on the page but came back from the feed with that tag missing, so an integrator filtering on one of them saw nothing. The feed now returns exactly the tags the page shows for every entry, past and future, and a tag stored under an older spelling resolves the same way in both. The response shape and every existing field are unchanged; clients that cached the earlier response are told to fetch it again. In the platform admin, the Clothing & sizes tag is now labelled in every language.

Technical focus: ONE registry, core/changelog_taxonomy.php (changelog_module_labels / changelog_module_keys / changelog_entry_modules), read by the page, the admin form and the API. It is its own file because /api/public/* runs on bootstrap_public.php, which loads no helpers: helpers.php requires it and the API requires it directly. A new module key is added there once, plus changelog.module.<key> in the four lang files; scripts/check_changelog_module_taxonomy.php fails on any consumer that grows a literal copy again. API ETag seed v4.

26 changes · 5 added · 9 updated · 12 fixed

Clothing & sizes 9 · Contracts 8 · Workforce 5 · UI 3 · Scheduling 2 · Messaging 2 · +4 more

Email delivery • Welcome and approval emails are in the log

The welcome email sent when a person is created from a registration, and the approval email sent when a pending registration is approved, were delivered but not recorded in the Email delivery log under Settings, so their status could not be checked there and a temporary delivery failure was not retried. This mattered most for the welcome, which is the only email that carries a person's first set-password link: with no record, an operator could not tell never set a password from never received the email. Both are now recorded like every other email the platform sends, with the recipient, and a soft bounce is retried in the usual way; the approval email is also filed against the registration it decided, beside the rejection email that already was.

Technical focus: The mail layer logs an email only when it can attribute it to an account, and it fell back to the session, which every current caller happened to have; both senders now pass the account and the recipient explicitly, so a queued approval or a CLI backfill is recorded too. The welcome keeps no related record (its two callers are different entities); the approval mirrors the rejection exactly, so one signup_registration filter shows both sides of a decision.