Releases: thatskiff33/AlpineClubBookingsNZ
Release list
v0.13.2
AlpineClubBookingsNZ v0.13.2
Release target: v0.13.2 on 2026-07-23. This patch public reference release
brings all changes landed after v0.13.1 into one supported tag. It carries
five operator-relevant workstreams, one of which needs deliberate handling at
deploy time:
- The club-theme orphan-column expand→contract pair (#2187). The theme
rebuild dropped from seven hand-picked brand colours to three seeds, leaving
four formerClubThemecolumns dead to code. This release ships both the
additive expand migration that lets the new runtime stop naming them and
the destructive contract migration that drops them — so the contract half
needs the blue/green handling described below and is legal only once the new
runtime has drained the previous colour. - Config-bundle format version 2 (#2187). Configuration bundles now export
at format version 2; a bundle exported by an older app (version 1) is refused
on import with a clear message rather than importing stale colour columns. - Guided provider setup wizards for Xero, Stripe, and Google sign-in
(#2080/#2081/#2082/#2087). Provider credentials, already DB-only since
v0.13.1 (#2079), are now captured through step-by-step in-app wizards. - The opt-in AI help assistant (#2094). A new, default-off, spend-metered
module that answers members' free-text questions grounded strictly in each
page's curated help. - Backup configuration re-homed to the encrypted store (#2095). The runtime
now reads backup settings only from the in-app credential store; the
legacyBACKUP_*environment variables are no longer honoured, and an
unmigrated legacy config makes the nightly backup report FAILURE until the
operator re-enters credentials in the new backup settings UI.
The canonical detailed entries remain in CHANGELOG.md; this page is the
release/operator overview.
⚠️ TheClubThemeorphan-column drop is destructive and must not run while a
pre-#2187 colour is still draining. The four dropped columns
(brandCharcoal/brandRidge/brandMist/brandSnow) were named in the
SQL of every colour up to and includingv0.13.1— Prisma projects every
scalar of a model in an unnarrowedSELECTand in a mutation's implicit
RETURNING. Dropped whilev0.13.1is still the draining colour, the contract
migration breaks the old colour's theme reads and writes. Thev0.13.2runtime
names none of them, which is what makes the drop legal — but only after
v0.13.2has replaced and drainedv0.13.1. Either defer the contract drop
until then (see below) or run it in a later quiet window with the breaking-
migration override. If the previous colour is still live, do not run the
drop.
Highlights
Club theming: three-seed substrate and the orphan-column drain (#2187)
- Site Style now derives the whole theme from three seed colours instead of
seven hand-picked ones. A vendored Radix colour generator turns one required
accent plus two optional seeds into the full light/dark 12-step palette, with
cross-colour text contrast guaranteed by construction, and the whole member and
admin app — plus the public website — now paints from that generated palette. 20260722140000_expand_club_theme_orphan_column_defaults— EXPAND
(additive, blue/green safe). Adds a DBDEFAULTto each of the four former
columns so the new runtime canINSERTaClubThemerow without naming them,
while the draining previous colour (which still writes explicit values) is
unaffected. No data is rewritten and no column is dropped.20260722160000_contract_drop_club_theme_orphan_columns— CONTRACT
(destructive). DropsbrandCharcoal/brandRidge/brandMist/
brandSnow. The four surfaces are derived from the generated substrate at
render time (deriveBrandShims), so there is zero resolvable data loss, but the
drop is irreversible without a restore. It carries a full rationale row in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv
(previous_expand_release = 20260722140000_expand_club_theme_orphan_column_defaults,
old_code_compatible = yes) and refuses to run without the breaking-migration
acknowledgement.- Configuration bundles move to format version 2. A bundle exported by an
older app (version 1) is refused on import with a clear message rather than
importing stale colour columns.
Guided provider setup wizards (#2080, #2081, #2082, #2087)
- Xero (#2080/#2081). A reusable wizard shell walks a Full Admin through app
creation, credential capture, and the OAuth connect, then completes setup with
verified webhooks, item-code mappings, and member import, showing an amber
badge on the setup page while webhook verification still needs attention. - Stripe (#2082). Captures its keys into the encrypted store and reads the
publishable key at runtime, so changing keys takes effect immediately rather
than needing a fresh build. - Google sign-in (#2087). Captures its OAuth credentials in-app, runs on a
request-scoped NextAuth config, and keeps the module locked until a real Google
round-trip verifies, so an unconfigured provider can never render a visibly
broken sign-in path. - In all three, legacy provider env vars are detected, warned about, and ignored
— re-enter the values in the wizard, then remove the env vars.
AI help assistant and the chat-style help widget (#2094)
- The in-app help is now a chat-style widget on every surface (#2209, #2210).
A single curated help corpus feeds a widget that answers page-specific
questions and shows a full page guide, replacing the older scattered help UIs. - The AI help assistant module is opt-in and off by default (#2211, #2212).
With it on, authenticated members can ask free-text questions that a paid AI
model (Anthropic Claude Haiku 4.5) answers grounded strictly in the curated
corpus. The club supplies its own Anthropic API key, held only in the
encrypted vault (never an env var). A hard monthly spend cap (default
NZ$10, integer cents) stops AI answers for the rest of the month once
reached; the budget gate fails closed, reserving a conservative worst-case
cost before each call and refusing to spend if it can no longer record usage.
Enabling the module without a key is harmless — the ask box degrades to a
structured fallback and curated page help still works. Members are told the
answer may be wrong and that their question is sent to Anthropic (United
States); the question text itself is never stored.
Backup configuration re-homed to the encrypted store (#2095)
- Backup settings move into the encrypted credential store and out of the
environment. The S3 access key and secret, destination bucket and region,
retention window, restore-validation shadow database, and the on/off switch are
managed from a new Admin → Integrations → Database Backups page
(/admin/backups), with a Run backup now action that runs as a background
job behind a database-level cross-process lock. The S3 secret and the
restore-validation connection string are write-only and never echoed back. - The legacy
BACKUP_*environment variables are no longer read. An install
that configured backups through the old env vars upgrades to an empty store, so
until the operator re-enters the backup settings the nightly backup reports a
loud FAILURE rather than silently skipping — a stopped disaster-recovery path
must alert, not disappear. OnlyBACKUP_CRON_SCHEDULE(cron-leader timing —
when the nightly job fires) stays in the environment.
Hardening and fixes
- Config-transfer column audit (#2178). A reverse-drift guard fails the build
if a club-settings column is neither exported nor explicitly excluded,
complementing the singleton-table audit (#2200). - Season-subscription charge-coverage reconcile (#2179). The guard that flips
a staleNOT_REQUIREDseed subscription row now checks for an active
charge-coverage claim (both at read and, relationally, at write) instead of a
null-check on a to-many relation that was always true, so the reconcile stands
aside when live coverage exists and proceeds when it does not. - Dependency advisories (#2196, #2224). Cleared newly published advisories in
hono,fast-uri,dompurify, andsharp, and moved Next.js to 16.2.11 to
clear a middleware-bypass and a Server-Actions denial-of-service advisory.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains six migrations: five additive/expand migrations plus one
destructivecontractmigration.- Expand (no operator action):
20260722120000_add_integration_wizard_progress(the wizard cursor for the
guided setup shell, #2080),20260722130000_add_xero_webhook_validation_receipt
(the Xero webhook validation receipt sink, #2081),
20260722140000_expand_club_theme_orphan_column_defaults(the theme
orphan-column defaults, #2187),20260722150000_add_backup_run(the backup-run
ledger, #2095), and20260723120000_add_ai_assistant(the AI assistant models,
#2211). Each is additive and blue/green safe. - Contract (needs handling):
20260722160000_contract_drop_club_theme_orphan_columnsdrops the four dead
ClubThemecolumns. The blue/green validator refuses it without
ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1plus a non-empty
BLUE_GREEN_MIGRATION_OVERRIDE_REASON. Because the paired expand migration
ships in this same tag, the contract drop is legal only after the
v0.13.2runtime has replaced and drainedv0.13.1. Either defer it —
mark it applied without running it (prisma migrate resolve --applied 20260722160000_contract_drop_club_theme_orphan_columns) until the expand-
carrying...
- Expand (no operator action):
v0.13.1
AlpineClubBookingsNZ v0.13.1
Release target: v0.13.1 on 2026-07-22. This patch public reference release
brings all changes landed after v0.13.0 into one supported tag. It carries two
lead items, both requiring operator action:
- Release B of the #2129/#2130 contract series — two destructive
contract
migrations that finish the expand/migrate/contract work E4 (#1930) and E8
(#1934) began and that thev0.13.0runtime-prep (Release A) made
blue/green-legal. They make this release breaking: unlikev0.13.0(whose
four migrations were all expand / metadata-only), both drop schema and are
irreversible — the deploy requiresALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1
and a non-emptyBLUE_GREEN_MIGRATION_OVERRIDE_REASON, and it is legal only
on top of a deployed, drained, soakedv0.13.0. - Encrypted DB-only provider credentials (#2079) — a Critical security/ops
change. Xero credential resolution is hard-cut from envXERO_*to an
encryptedIntegrationCredentialstore. On upgrade, an existing
Xero-connected install enters a documented "needs re-entry" state and Xero work
pauses until a Full Admin re-enters credentials in-app and reconnects OAuth.
Its migration is additive/expand, so the release ships two contract
migrations plus one expand migration.
The canonical detailed entries remain in CHANGELOG.md; this page is the
release/operator overview.
⚠️ Do not deploy this release untilv0.13.0(Release A) has been the live,
drained production colour long enough to soak. These migrations drop a table
and three columns that the v0.12.2 colour still named in its SQL. Shipped
against a draining v0.12.2, they cause anonymous public 500s on every page
carrying{{hut-fees}}(42P01 relation "SeasonRate" does not exist), admin
seasons pages 500, Xero item-code saves 500 (42703 column "isMember" does not exist), and age-tier saves plus the boot-time config self-heal failing on
every blue container start — none of it recoverable by rolling the app back.
Thev0.13.0colour names none of them, which is what makes this drop legal
now. If the live colour isv0.12.2or earlier, stop.
Highlights
Release B — legacy contract drops (#2129 step 2, #2130 STEP 2)
20260721120000_contract_drop_season_rate—DROP TABLE "SeasonRate". The
frozen member/non-member boolean-keyed nightly-rate table is dropped. Its rows
were copied forward intoMembershipTypeSeasonRateby the E4 re-key
(20260717140000_pricing_rekey_by_membership_type), and nightly pricing, Xero
hut-fee item codes, and the public{{hut-fees}}embed have all read the new
table since #2129 step 1, so nothing user-visible changes. Release A removed the
last application-runtime reader (the{{hut-fees}}embed); this PR removes the
only survivors, which were seed-only and outsidesrc/(theinclude: { rates: true }read andrates: { create: … }write in
e2e/setup/seed-second-lodge.ts, andcreateMissingSeasonRatesplus its two
call sites inprisma/seed.ts). Because the E4 fan-out that copied those rows
forward was conditional on the install having aMEMBER_RATE-behaviour
membership type and aNON_MEMBER-keyed type, the migration opens with a
pre-drop coverage guard: a read-only anti-join that countsSeasonRaterows
with noMembershipTypeSeasonRatecounterpart for the same season and age tier
and raises, aborting the transaction before theDROP TABLE, if any exist. A
fork whose types never matched keeps its only copy of that pricing instead of
losing it.docs/UPGRADING.mdpublishes the same check as a read-only operator
pre-flight query — run it before you start.20260721130000_contract_drop_ismember_and_agetier_xero_columns. Deletes
the orphaned legacyHUT_FEEitem-code rows that carry nomembershipTypeId
(not resolvable for pricing by the current runtime — production held 16; the
only paths that still touch them count or collect item codes in aggregate and
name no dropped column), drops the old(category, ageTier, seasonType, isMember)unique index, dropsXeroItemCodeMapping.isMember, and drops
AgeTierSetting.xeroContactGroupId/xeroContactGroupName(their data moved
intoXeroContactGroupRuleat E8,20260716140000_xero_member_grouping). The
still-live partial indexXeroItemCodeMapping_hutfee_flat_uniqueis untouched.
This is legal only becausev0.12.2narrowed the reads (#2133 STEP 1) and
Release A narrowed the writes (#2130 STEP 1.5) on both models, so the draining
colour names none of these columns in aSELECTor an implicitRETURNING.
Both migrations carry full rationale rows in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv
(previous_expand_release = 20260717140000_pricing_rekey_by_membership_type,
old_code_compatible = yes), and both refuse to run without the breaking-
migration acknowledgement. The #2130 select guard
(doomed-column-select-guard.test.ts) is kept even though its original columns
are gone — narrow selects remain the rule for both models.
Admin theming
- The admin area now follows the club's saved site colours in light mode
(#2144). A sweep of 1,410 class occurrences across the admin tree moved
hard-coded light-grey ("slate") Tailwind colours onto the same semantic theme
tokens the finance dashboard has used since #2137, so a club with a strongly
non-default palette now sees it applied consistently across admin. Dark mode
is visually unchanged for ~98% of occurrences (90.6% land on the exact token
the existing.darkneutral remap already assigned — a provable dark-mode
no-op — and a further 7.3% land on a token that renders identically today);
~2% genuinely change dark rendering, chiefly small deliberate dark-mode fixes on
admin surfaces the remap never covered plus four occurrences on the public
hut-leader instructions page (which renders underwebsite-theme). In light
mode there are two deliberate changes: the five grey text tints collapse onto
the single AA-clampedtext-muted-foregroundtone (an accessibility
improvement), and recessed panels usebg-mutedwhile cards usebg-card
(the finance precedent) so insets stay visibly recessed. A widened
source-contract test now gates the whole admin tree (plus finance) against raw
neutral classes, with a nine-entry per-file allowlist covering the deliberate
literal-colour exclusions (print pages, signage letterboxes, code-preview panes,
status chips, the member-import active-step border).
Encrypted DB-only provider credentials (#2079)
- New encrypted credential store. The additive migration
20260721210000_add_integration_credentialcreates one standalone
IntegrationCredentialtable holding AES-256-GCM ciphertext under a key derived
by real HKDF-SHA256 from the canonicalgetAuthSecret()resolver (fixed
documented salt, versioned info labels), with a fresh random IV per encrypt and
GCM AAD (provider:key:labelVersion) binding every ciphertext to its row. A
secretSourcefield makes a silentAUTH_SECRET↔NEXTAUTH_SECRETflip
diagnosable (still decrypts, flagged), while a changed secret value fails
cleanly into "needs re-entry" rather than crashing. - Hard cutover of Xero resolution to the store.
getOperationalXeroConfig(),getOperationalXeroEncryptionKey(), and the new
getOperationalXeroWebhookKey()resolve from the store; envXERO_CLIENT_ID/
XERO_CLIENT_SECRET/XERO_REDIRECT_URI/XERO_ENCRYPTION_KEY/
XERO_WEBHOOK_KEYare no longer read for operation — legacy values are
detected and flagged for removal in setup readiness, never silently honoured and
never silently ignored. The webhook route stays fail-closed (missing/unreadable
key rejects every delivery), and the OAuth redirect URI now derives from
NEXTAUTH_URL. - Capture, cache, and gating. A 45 s cross-process credential cache
(invalidated on write; negatives and DB errors never cached beyond the TTL) lets
the cron-leader observe a web-slot write without a restart. An AUTH_SECRET
strength gate hard-blocks credential capture on a weak secret (< 32 chars, or a
placeholder — a blocklist that catches the 41-char.env.exampleliteral),
shows a passive amber readiness warning from day one, and imposes no boot-time
enforcement. Writes go through a write-only, Full-Admin-only API (values never
returned, metadata-only audit; area admins keep a metadata-only status GET), with
an interim Xero Credentials entry section on/admin/xero/setup.
Verify-reset: a client-credential write drops stored OAuth tokens; a
webhook-key write re-arms verification without dropping tokens. - Truthful reconnect after upgrade. Tokens previously wrapped under the dropped
XERO_ENCRYPTION_KEYbecome unreadable by design (no silent key import); a typed
XeroTokenDecryptErrormaps to the reconnect state so the admin status panel,
setup readiness, and finance-report messaging show "reconnect Xero" instead of a
false "Connected". The credential entity is excluded from configuration export,
the blue/green deploy script no longer hard-requires the droppedXERO_*vars
(warn-only), docker-compose no longer plumbs them, and the.envexamples were
rewritten.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains three migrations: two destructive
contractmigrations
(Release B) plus one additive/expand migration (#2079). The two Release B
contract migrations are bothold_code_compatible=yesand ledgered with
previous_expand_release = 20260717140000_pricing_rekey_by_membership_type;
20260721210000_add_integration_credentialis a purely additive standalone
table (no ledger row required — it matches no hot-table or breaking rege...
v0.13.0
AlpineClubBookingsNZ v0.13.0
Release target: v0.13.0 on 2026-07-21. This minor public reference release
brings all changes landed after v0.12.2 into one supported tag. It touches
subscription billing — the annual-subscription billing epic (#2151) changes how
the annual membership fee is deduped, voided, re-billed, and suppressed, and it
drops the old role-based subscription exemption in favour of DB-backed membership
types. An operator reading only this page must know the money paths change
this release; read the 0.13.0 changelog section and docs/UPGRADING.md before
deploying.
Unlike v0.12.2 (which carried two breaking contract migrations), this release
is not breaking: its four migrations are all expand / metadata-only and all
old_code_compatible=yes, so the blue/green validator passes with no
ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS override and a normal deploy window is
enough. The canonical detailed entries remain in CHANGELOG.md; this page is the
release/operator overview.
Highlights
Annual-subscription billing epic (#2151)
- Double-billing fix with void / re-bill (#2147). The billing preview and the
in-transaction confirm now treat a seasonMemberSubscriptionas already billed
when itsstatus = PAIDor it carries a non-nullxeroInvoiceId
(manually-marked-paid members with no invoice stay skipped), and coverage-based
dedup counts only active (unreleased) claims. When the Xero sync observes a
charge's invoice VOIDED/DELETED it atomically releases the coverage claim
(releasedAtset, row kept for audit), marks the chargeVOIDED(row kept),
bumpsMemberSubscription.voidGeneration, and clears the subscription's invoice
link toNOT_INVOICED— the member becomes re-billable, and a post-void confirm
derives a new idempotency key that folds invoidGeneration(byte-identical
for never-voided subscriptions). A collapsed-by-default "Already invoiced"
section in the preview shows the count and each suppressed member's Xero invoice
number and status. Documented semantics change: a voided invoice previously read
asUNPAID(booking lockout) and now reads asNOT_INVOICED(re-billable). - Billing-exception resolution provenance (#2148). A new nullable
MembershipBillingException.resolvedViacolumn, backed by a
MembershipBillingExceptionResolutionenum (CONFIRM | PREVIEW_RECONCILE),
records how an exception reachedRESOLVEDso a preview-reconciliation
resolution is distinguishable from a confirm-run one. Existing and legacy
resolved rows, and every open row, stayNULL— the "resolved before this
column existed / not yet resolved" state. - Membership type is the sole subscription authority (#2149) — behaviour
change. The role-based subscription exemption is removed from every
derivation: membership type (subscriptionBehavior, plus age tier where the
type isBASED_ON_AGE_TIER) is now the only authority on whether a member owes
a subscription, and the loginRoleenum is a pure permission concept again. A
fee-paying member who happens to holdrole=ADMINnow shows their real
subscription status everywhere. Five divergent derivations (booking gate,
profile/subscription-status API, admin members list and its SQL filter variants,
subscriptions list, CSV export, and the Xero-sync status check) are consolidated
onto shared primitives. A data-only migration seeds two built-in types so the
dropped exemption keeps a DB-backedNOT_REQUIREDfallback: ADMIN
(NOT_REQUIRED,BLOCK_BOOKING) and LODGE (NOT_REQUIRED,MEMBER_RATE,
so kiosk accounts still book on behalf of members). - Operator "already invoiced" family marker and refined family suppression
(#2161 / #2167). ForPER_FAMILYbilling, a live legacy invoice suppresses a
family's charge only when its holder's own resolved billing basis is
PER_FAMILY; aPER_MEMBER-billed member's personal invoice no longer blocks
the family fee (that member stays skipped per-member). To close the ambiguous
legacy shapes that refinement re-opens, a newFamilyGroupSeasonInvoiceMarker
table lets an operator mark a family "already invoiced":finance:edit-gated
MARK/UNMARK actions on the billing route, an active marker suppressing the family
regardless of basis, unmark releasing the row (releasedAt) while keeping it for
audit, and a partial unique index enforcing one active marker per
(familyGroupId, seasonYear). Suppression is fail-closed: it lifts only on a
provenPER_MEMBERholder basis, with unresolvable bases carrying an
"Unresolved basis" badge in the audit panel.
Xero and finance surfaces
VOIDEDcharges are fenced everywhere they could otherwise re-enqueue or
re-invoice — enqueue/RETRY_CHARGE, invoice creation, the outbox failure handler,
and the panel (no Retry button) — so a voided-then-retained charge cannot cause a
second Xero write.- Four dependency security advisories cleared (#2173).
npm auditbegan
flagging one moderate and three high-severity advisories against two transitive
packages (anaxios/brace-expansiondenial-of-service and prototype-pollution
set), turning the requiredverifyjob red onmainand every PR. The exact
overridespins were bumped (axios1.16.1→1.18.1,brace-expansion
5.0.6→5.0.7);xero-nodestill resolves^1.7.7and did not move, and no
application code changed. The hardened axios form-serialization and body-limit
paths are not on the Xero call path.
Admin UX
- View-only banner rollout (#2142 → #2160 → #2168). The per-button "you cannot
change this" explanation that reached almost nobody (a greyed-out button is
skipped by the keyboard and shows no hover tooltip) is replaced by a short
section-level banner shown once at the top of a read-only area — the pattern
Booking Policies adopted in #2142, now rolled across most of the admin tree
(#2160) and the member detail page as one page-level banner (#2168). About seven
of every eight gated buttons are now explained by a banner instead of
individually. Nothing about who can do what changed — every button is gated
exactly as before. - Shared
useSectionEditStatehook (#2136). The canonical admin
settings-section pattern (load read-only, per-section Edit reveals Save/Cancel,
nothing auto-persists on toggle, Cancel reverts to the saved snapshot, Save
persists once) is centralised into one hook, so Cancel restores every field and
Save re-seeds both draft and snapshot from the save callback's return. Refactor
only, adopted by the group discount, password policy, email sign-in link, and
Google sign-in sections. - Quote-timing cards open with Edit (#2166), and the indicative-pricing toggle
is staged behind Save (#2162). On Booking Policies → Public Booking Requests,
the Quote Response Window / School Attendee Confirmation cards and the "Show
indicative pricing" checkbox no longer write on the first keystroke or click —
each now has its own Edit → change → Save → Cancel cycle, matching the rest of
the area, and each save re-reads the stored settings and sends back only the
fields it owns so it cannot clobber another admin's concurrent change.
Theming and print
- Categorical teal on tokens, and a themed
/financebody (#2137). The last
literal-Tailwind categorical teal (waitlist-offered chip, audit-logfamily
badge, family-groupGROUP_CREATEbadge) now reaches its hue through the
--hue-*tokens (value-identical), the audit category badge map is de-duplicated
into one shared module, and the/financedashboard body now uses semantic
surface tokens so it follows a strongly non-default club theme in light mode as
well as dark. - Print / PDF dark mode no longer produces a blank page (#2146). Downloading a
PDF or printing/financeor/admin/reportswhile browsing in dark mode used
to render near-white ink on the forced-white print background. Print and PDF now
always render the light colour scheme regardless of the browsing theme, across
every printable surface (reports, chore roster, induction sign-off, lodge
instructions). - Secondary text actually looks secondary (#2145), and bed-allocation labels
no longer clip (#2150): the muted-foreground token is corrected so secondary
text reads as secondary (falling back to identical-to-normal only where contrast
requires it), and the bed-allocation board's label column gets its own 14rem
width with two-line wrapping and atitlefallback for long bed names.
Config transfer and hut fees
- Config-transfer legacy-bundle compatibility window closed (#2131). One
release after the E13 contraction, the importer rejects the legacy boolean-keyed
bundle shapes (isMembercolumns, the pre-#1931ENTRANCE_FEEcategory) at
dry-run with a clear row-named error, and aHUT_FEErow with a blank
membershipTypeKeyis now a blocking error. v0.12.2 was the last release that
could import the legacy shape — re-export archived bundles before upgrading
(seedocs/UPGRADING.md). - Public
{{hut-fees}}embed re-sourced onto the authoritative rates (#2129,
step 1). The embed was the last application-runtime reader of the frozen
member/non-memberSeasonRatetable; it now readsMembershipTypeSeasonRate—
the same rows that price a booking — and renders a real per-lodge × season table
with one column per active, publicly listed membership type. No schema change
this release;SeasonRatestill has one reader and two writers in seed code
outsidesrc/, so its DROP is deferred to a later release. - Blue/green runtime-prep for the legacy Xero column drops (#2130). This is
step-1 prep only, with no schema change and no migration: every mutation on
XeroItemCodeMappingandAgeTierSettingis narrowed with an explicitselect
(com...
v0.12.2
AlpineClubBookingsNZ v0.12.2
Release target: v0.12.2 on 2026-07-20. This patch public reference release
brings all changes landed after v0.12.1 into one supported tag. As with
v0.12.1, the version is a deliberate patch bump chosen by the owner even
though the range carries feature work — most additions are opt-in and flagged
off by default. Unlike v0.12.1, this release is not purely additive: it
lands the first destructive contract migration since the expand/migrate/contract
series began (legacy-structure contraction E13, the blue/green-safe subset of
#1939), a second breaking column-drop migration (Xero member-grouping
multi-select age tiers), and one changed default behaviour (admin post-login
landing). Both breaking migrations require the operator acknowledgement
ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1. The canonical detailed entries remain
in CHANGELOG.md; this page is the release/operator overview.
Highlights
- Production Xero lock-date 503 fix. Retroactive (past-dated) booking
creation returned a 503 "Could not verify the Xero lock dates" whenever the
connected Xero organisation actually had lock dates set — the exact case
the guard exists for. xero-node's serializer coerces an MS-JSON/Date(...)/
payload into a JSDate, soparseXeroLockDate'svalue.slice(0,10)threw
and the guard failed closed; reconnecting could never fix it. The parser now
acceptsstring | Date. - Xero reliability follow-ups. A lock-date error taxonomy classifies the
guard's 503 asreconnect_required | rate_limited | transientwith
cause-specific, admin-only reason copy (member responses byte-identical); a
click-only live connection-health probe (GET /api/admin/xero/status?probe=1)
replaces the token-row-presence "Connected" chip; the finance-sync date parser
is nowDate-aware so SDK-coerced due-dates no longer drift into the
no-due-date aging bucket; and the Xero contact-create gate is shrunk to require
only name + email, with sparse members creating clean payloads. - Age-exempt (N/A) membership-type lifecycle. Membership types whose
allowed-age-tiers list "N/A (no age)" are now the single source for genuinely
age-exempt members, enforced through one shared helper at every ageTier write
site. On top of it: bulk membership-type assignment (up to 100 members) from
the members page with preview tokens, per-member audits, and a batched Xero
reconcile; Xero Setup import into membership types (or types + age tiers) that
never overwrites an existing current-season assignment and fully reports what
it skips; and an opt-in mode that lets subscription paid-detection look through
to per-type + tier fee item codes (default off = today's behaviour). - Xero member-grouping multi-select age tiers. A grouping rule can now target
any subset of age tiers (empty = all tiers) with specificity-based overlap
resolution and a fingerprint serializer proven byte-identical to the old one
for the migrated cases, plus a "Refresh from Xero" button and a "Last synced"
header. Its migration drops the old scalarageTiercolumn (breaking — see
below). - Admin post-login landing (behaviour change). After sign-in a member with
admin access and no set preference now lands on their admin area instead of the
member dashboard, applied by the application redirect resolver; a nullable
Member.postLoginLandingenum backs a profile toggle. No open-redirect
surface; a demoted admin with a stale preference still lands safely on
/dashboard. - Admin and booking UX. A Ctrl/Cmd-K admin feature-search palette; a
bookings-officer key-card row on the dashboard; a fixed admin booking-calendar
overflow (auto-expanding rows, "+N more" chip, greyed finished days); the
/financeand/lodgeshells now inherit the club theme;/admin/security
gains per-section Edit→Save gating, a persistable magic-link TTL, and a
stale-clobber fix; the member booking-edit panel now renders the required
minors-without-adult justification field (machine-readable error code); and an
in-progress stay's minimum-stay rule is evaluated against the whole contiguous
stay, surfaced as an advisory warning. - Legacy schema contraction E13 (destructive). Drops the dead
EntranceFee
andAgeTierXeroAcceptedContactGrouptables and an orphaned account-mapping
row — the blue/green-safe subset of #1939, re-verified as zero-reader against
thev0.12.1tag; the remainder is deferred to #2129/#2130/#2131. The #2130
runtime-prep also ships here (#2133): explicitselects stop the deployed
client naming the deferred columns, making next release's contract drop
blue/green-legal. - Docs and tests. The member-facing user guide is mirrored one-way to the
GitHub wiki by a push-triggered workflow (npm run docs:wiki-sync), and the
E2E seed fixtures were made relative/never-expiring so the suite stops going
stale at date boundaries.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains four migrations: two expand/additive and two breaking
contractmigrations. Both breaking migrations require the operator
acknowledgementALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1for this deploy — the
blue/green validator refuses a breaking migration without it. All four carry
old_code_compatible=yeswhere they have a ledger row.20260719170000_xero_grouping_age_tiers_multiselect(breakingcontract,
ledgered) backfills the scalarXeroContactGroupRule.ageTierinto a new
ageTiersarray (X → [X],null → []= "all tiers") and then drops the
scalar column.old_code_compatible=yesbut window-bounded and
admin-only: between migrate and cutover the old colour's
grouping/membership-admin reads still nameageTierand error with
column-does-not-exist. The live grouping sync fails closed (a re-grouping
edit errors and is retried post-cutover — no partial write, no money, no
booking capacity). Deploy with grouping/membership-admin traffic idle and cut
over promptly. No member is re-grouped in Xero by the migration.20260720120000_contract_drop_entrance_fee_and_agetier_xero_group
(destructivecontract, ledgered) drops the deadEntranceFeeand
AgeTierXeroAcceptedContactGrouptables and deletes the orphaned
entranceFeeAmountCentsaccount-mapping row.old_code_compatible=yes: an
independent drop-proof review re-verified zero readers against the
v0.12.1tag (the colour draining during this deploy) with no FK/cascade
trap, so the draining old colour keeps working. The acknowledgement is
required only because aDROPis breaking by class. Deliberately
kept/deferred (still read by the current runtime): theEntranceFeeCategory
enum,SeasonRate(the live public{{hut-fees}}embed),MembershipTypeAgeTier,
and theXeroItemCodeMapping.isMember/AgeTierSetting.xeroContactGroup*
columns — follow-ups #2129/#2130/#2131.20260719150000_add_post_login_landing(expand, ledgered) adds a
PostLoginLandingenum plus a nullableMember.postLoginLandingcolumn with
no default — a metadata-only catalog change even on the hotMembertable.
No existing member's landing changes on migrate; the changed admin default is
applied entirely by the application redirect resolver.20260719180000_add_use_fee_schedule_item_codes(expand, ledger-exempt)
adds a single flagged-off boolean on the cold single-row
MembershipLockoutSettingstable — additive with a constant default, so it
carries no ledger row under the same policy as v0.12.1's
add_login_security_setting.
- The one behaviour change is admin post-login landing: from the first login
after cutover, a member with any accessible admin area (including read-only
admins and finance-only viewers) lands on their admin area instead of
/dashboard, unless they set the new profile preference. It is applied by the
application, not by stored data. - Opt-in, no behaviour change until enabled: the new "use membership fee item
codes" subscription paid-detection mode (its migration only adds the flag), and
the age-exempt (N/A) membership types (config-only, no migration — effective
only when an admin sets a type's allowed age tiers to include or restrict to
N/A). - No migration makes a Xero, Stripe, or SES call, and no member is re-grouped in
Xero by any migration.
Upgrade and verification
Follow docs/UPGRADING.md from v0.12.1 to v0.12.2. In summary: take and
restore-test a fresh backup (mandatory this release — the E13 DROP TABLEs are
irreversible without it), review the four pending migrations against the safety
ledger, set ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1 for the deploy, run the
grouping-drop deploy in a quiet admin window and cut over promptly, and complete
the post-upgrade actions.
After cutover, verify at minimum:
- Xero lock dates — creating a retroactive (past-dated) booking no longer
returns the 503 when the org has lock dates set; the admin Xero page shows the
real connection-health chip (and the Reconnect CTA when a token is revoked). - Member-grouping rules — each former single-tier rule shows that one tier
and each former "Any age" rule shows "all tiers"; "Refresh from Xero" triggers
no unexpected full regroup. - Post-login landing — admins (including read-only and finance-only) land on
their admin area on the first sign-in after cutover unless they set the new
profile preference; plain and demoted members still land on/dashboard. - Membership tooling — the members-page bulk "Set Membership Type" flow and
the Xero Setup import mapping modes work and never overwrite an existing
current-season assignment; the opt-in fee item-code mode is off unless enabled. - *...
AlpineClubBookingsNZ v0.12.1
AlpineClubBookingsNZ v0.12.1
Release target: v0.12.1 on 2026-07-19. This patch public reference release
brings all changes landed after v0.12.0 into one supported tag. The version
is a deliberate patch bump chosen by the owner even though the range carries
feature work, because every addition is additive and flagged off by default —
nothing changes for an existing club until an admin opts in. The canonical
detailed entries remain in CHANGELOG.md; this page is the release/operator
overview.
Highlights
- Optional sign-in methods behind a new admin Login & Security page
(/admin/security): a per-club password-complexity policy (minimum length
8–64, default 12; four character-class toggles, default off; fixed 128
maximum) enforced only at password-set time, plus two module-flagged
sign-in additions that never replace password login — email magic-link
(single-use hashed token, club-set TTL) and Google OAuth (profile-initiated
account linking only, resolving by pinned Google subject id, never by email
match or auto-provisioning). Both new modules default off; an
un-configured club is byte-identical to today. - Per-age-tier membership billing: membership types gain a Required based on
age tier subscription behaviour that defers to the existing per-tier
subscriptionRequiredForBookingflag (billing liability fixed by age at the
start of the financial year), and annual membership fees gain the same
flat vs per-tier shape the joining fee already carried (existing rows are
the flat fallback; per-family fees stay flat-only). The membership-type
editor adds an explicit "N/A (no age)" allowed-tier option. - Fees editor UX: the annual-fee editor replaces free-text Xero Account/Item
inputs with searchable pickers (ACTIVE revenue accounts / sales-capable
items) fed by the existing admin-gated proxy endpoints, falling back to
manual entry on Xero disconnection, surfacing the resolved default account,
and showing the fee-level proration rule — with no billing-math change. - Lobby Display polish: a six-board template pack (four built-ins + a
two-board import bundle) so every display module is exercised, a guided
visual builder at/admin/display/builder(ADR-004) over the unchanged
save contract with the textarea editors retained as Advanced mode and no
schema change, and night-columns rescoped as an honest permanent 3-night
board. The Lobby Display module remains off by default. - A full documentation library: a docs foundation (style guide, audience-first
hub, curated architecture diagrams, a coverage matrix, an advisory link-check
CI workflow, and a screenshot harness), 65 operator guides in
docs/guides/across four batches, and a fifth batch of member-facing
journey guides indocs/user-guide/, each written against the running
seeded app — closing the coverage matrix to zero gaps. - A screenshot-forward
README.mdmarketing front page with reproducible hero
art and a booking-demo GIF harness, its former deep operational content
relocated into the docs it duplicated. No runtime app code changes. - Fixes and hardening: membership-types editor close-on-save + dirty-guarded
header X,/admin/displaydrill-down back-links (with a repo-wide
normalisation), a dollar-quote-aware fix to the blue/green migration
validator's session-clock gate, and agithub/codeql-actionbump to 4.37.0.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains five migrations: all expand/additive, no contract
migration. Four have explicit safety-ledger rows in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv, allold_code_compatible=yes:20260718130000_add_magic_linkaddsClubModuleSettings.magicLink
(flagged off, constant default) and a new emptyMagicLinkTokentable
with anON DELETE CASCADEFK toMember. The ADD COLUMN is catalog-only
on the cold single-row settings table; the FK on a brand-new empty child
table scans nothing. Additive; safe at any traffic level.20260719120000_add_google_oauthaddsClubModuleSettings.googleLogin
(flagged off, constant default) and a nullableMember.googleSubwith a
unique index. Both ADD COLUMNs are catalog-only (thegoogleSubcolumn
takes no default, so even on the hotMembertable it is metadata-only);
the unique index builds over an all-NULL column (NULLs never collide) and
briefly blocksMemberwrites — switch toCREATE UNIQUE INDEX CONCURRENTLYifMemberis very large. No backfill, no provider call.20260719130000_add_based_on_age_tier_subscription_behaviorregisters the
BASED_ON_AGE_TIERvalue on theMembershipTypeSubscriptionBehaviorenum
(ALTER TYPE ... ADD VALUE IF NOT EXISTS). Purely additive; no existing
row changes and no old colour reads the new label before cutover.20260719140000_annual_fee_age_tieradds a nullableMembershipAnnualFee.ageTier
(no default, no backfill — every existing row stays the flatNULL-tier
row and resolves identically) plus index/constraint reshaping on the cold
MembershipAnnualFeeconfig table: the uniqueness re-keys from
(membershipTypeId, effectiveFrom)to(membershipTypeId, ageTier, effectiveFrom)with a partial unique flat index, the effective-lookup
index widens to includeageTier, and the single per-type GiST no-overlap
EXCLUDE is split into two partial constraints (flat-vs-flat and same-tier
overlaps still conflict; flat-vs-tier and cross-tier coexist). A raw-SQL
CHECKforbids per-tier PER_FAMILY rows. Catalog-only ADD COLUMN plus
brief-lock index/EXCLUDE swaps on a cold table; no DML.
- The fifth migration,
20260718120000_add_login_security_setting, creates the
empty single-rowLoginSecuritySettingconfig table (password policy plus the
magic-link TTL) with no FK and no hot-table or breaking SQL, so it needs no
ledger row — the same policy under which several v0.12.0 additive migrations
carried no row. An absent row falls through to the code defaults (minimum
length 12, classes off, TTL 15), so an un-configured club behaves exactly as
before. - One old-colour operator caveat. Because the old colour's annual-fee
resolver does not filter by age tier, a per-age-tier annual-fee row is not
invisible to it — once such a row falls in an active window the old colour can
select it for a member of any tier (first match byeffectiveFromdesc) and
mis-price them at the wrong tier's amount. Do not create per-age-tier
annual-fee rows until the cutover completes. Flat annual fees and existing
rows are unaffected; per-tier joining fees already shipped in v0.12.0. - The two new sign-in modules default off, so magic-link and Google sign-in
stay disabled through the migrate→cutover window until an admin enables the
module (and, for Google, a member links their account). The
password-complexity policy applies only at password-set time and never
re-validates an existing password. - No migration makes a Xero, Stripe, or SES call, and no member is re-grouped
in Xero. The annual-fee Xero pickers read only the existing admin-gated
proxy endpoints server-side; no Xero secret reaches the browser bundle.
Upgrade and verification
Follow docs/UPGRADING.md from v0.12.0 to v0.12.1. In summary:
restore-test a fresh backup, review the five pending migrations against the
safety ledger, deploy in a normal window (no contract window is required this
release), and — critically — do not author any per-age-tier annual-fee rows
until cutover completes.
After cutover, verify at minimum:
- Login & Security — the admin security page shows the intended password
policy (an un-configured club still accepts and rejects passwords exactly as
before), and existing members can still sign in with their password. - Optional sign-in modules — magic-link and Google OAuth remain disabled
under Admin > Modules unless the club intends to use them; if enabling
Google, per-clubGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETare configured
perCONFIGURATION.mdand a member can link and then sign in. - Per-age-tier billing — for any membership type set to Required based on
age tier, the age-tier settings drive which tiers are billed, and exempt
tiers get no subscription invoice;REQUIRED/NOT_REQUIREDtypes are
unchanged. - Annual fees — admin fee configuration and the public annual-fees embed
show the same amounts as before; per-tier annual-fee rows are added only
after cutover. - Fees editor — the annual-fee Account/Item pickers list the expected Xero
codes (or fall back to manual entry with the amber notice if Xero is
disconnected), and the surfaced default account is correct. - Lobby Display — the module stays off unless intended; if enabled, the
template pack and the guided builder at/admin/display/builderwork, and
guest phones stay hidden unless both the member and the lodge opt in. - Docs and README — the operator/member guides and the new README render
correctly; these are documentation-only and carry no runtime risk.
This release does not publish an npm package (package.json remains private).
The supported artefacts are the Git tag/GitHub release and the repository's
container-image release workflow.
AlpineClubBookingsNZ v0.12.0
Release target: v0.12.0 on 2026-07-18. This minor public reference release
brings all changes landed after v0.11.0 into one supported tag. The canonical
detailed entries remain in CHANGELOG.md; this page is the release/operator
overview.
Highlights
- Lobby Display: a new flagged-off module for per-lodge lobby screens with
admin-authored layouts and templates (room cards, night columns, status
board), a lodge notice, guest name-granularity control, registered display
devices, and a two-sided (member and lodge) opt-in before any guest
phone number is shown — and only ever for adult members; youth and child
phone numbers are never shown. Off by default; nothing changes until
enabled. - Exclusive whole-lodge holds: a school/group booking request can ask for sole
occupancy, and an admin can place a whole-lodge hold that blocks all other
bookings for those nights, enforced through capacity checks, bed allocation,
status guards, and advisory locks. - Multi-lodge operation is core: the
multiLodgemodule flag is removed,
lodge routes are un-gated, and the admin home becomes a lodge hub. The
vestigialClubModuleSettings.multiLodgecolumn is retired but not yet
dropped (the drop is deferred to a future contract migration). Single-lodge
clubs see no operational change. - Authoritative fee schedules and membership billing: database-held season
rates keyed by membership type, per-type/per-tier joining fees, annual-fee
invoice components, durable subscription billing, family billing modes,
manual mark-paid provenance, and public fee presentation behind a
double-opt-in embed. Day-one amounts are backfilled from existing
configuration and the legacy tables are retained. - Database-first club identity and configuration: club identity, lodge
address, capacity, age tiers, and email settings resolve database-first with
config-file fallback, a boot-time self-heal backfills missing values without
overwriting admin edits, andCONFIG_BUNDLE_IMPORT_PATHcan auto-import a
configuration bundle at boot into an empty database for disaster recovery
or cloning. Age tiers can run as a contiguous subset (fewer than four). - Rule-based Xero member grouping with a server-persisted dry-run that must be
fresh before any bulk re-sync, plus Xero/finance hardening: settlement apply
retry, write durability, credit deallocation, entrance-fee dedup with a
single-active-link index, transactional season billing, and a webhook
processing lease so redelivered events reprocess safely. - Booking settlement and lifecycle correctness: NZ date-only enforcement,
corrected lock topology, closed cancel and double-charge races, truly
terminal request/hold states, single-sourced deferred-payment state, and
completion at the end of the checkout day (enabling priced, capacity-checked
checkout-day extensions). Split-booking settlement, payment amounts, and
admin presentation are corrected. - View-only admin access roles are enforced across admin editors, routes, and
action buttons: read the data, mutate nothing. - Member lifecycle administration: surfaced deletion requests, member merge,
duplicate-capture auto-refund template and history event, book-on-behalf for
non-members, and CSV import of already-cancelled members. - The legacy standalone committee directory and its admin CRUD are removed in
favour of the member-linked roles/assignments system from v0.11.0 — this
release's one contract migration. - Performance, load, and accessibility: admin bookings pagination, pool
sizing, a k6 load harness, load-stability fixes, accessible form errors and
UI states. CI adds Semgrep static analysis; the attack surface is
documented.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains 25 migrations: 24 expand/additive and one contract.
Fifteen have explicit safety-ledger rows in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsvbecause they touch hot-table-matching
SQL, contain data operations, or need a documented deployment plan. (One
further ledger change is a documentation-only correction to the pre-v0.11.0
20260708230000_add_member_credit_note_allocationrow.) The ten without
rows were checked directly against the validator and ledger policy and do
not require rows:20260714130000_add_oswald_club_theme_fontadds theOSWALDvalue to the
ClubThemeFontenum (ALTER TYPE ... ADD VALUE IF NOT EXISTS). Purely
additive; no table, row, or hot-table SQL is involved.20260715130000_xero_entrance_fee_single_active_linkcreates one partial
unique index onXeroObjectLinkguaranteeing at most one ACTIVE
entrance-fee invoice link per member — a last-line DB guard behind the
existing mint idempotency key. Recorded in
prisma/partial-unique-indexes.tsv; no data change and no destructive
SQL.20260716120000_add_booking_request_versionadds
BookingRequest.version INTEGER NOT NULL DEFAULT 0for the held-request
conversion fences. A constant-default ADD COLUMN is catalog-only on
PostgreSQL 11+, and old colours neither read nor write the column.20260717120000_add_admin_member_delete_request_prefadds
NotificationPreference.adminMemberDeleteRequestdefaultingtrue, so
existing admins keep receiving delete-request alerts. Additive only.20260717160000_add_club_identity_settingscreates the empty,
all-nullableClubIdentitySettingssingleton. An absent or empty row is
fully functional through the DB →club.json→ hard-default fallback
chain; no seed values are written by SQL.20260717160100_add_lodge_addressadds nullableLodge.addressand
backfills the previously hardcoded contact-page address onto the existing
default lodge only — a single-row update on a tiny cold config table.20260717160200_add_book_now_settingsadds constant-default Book Now
columns to thePublicContentSettingssingleton plus a SET NULL FK to
PageContent. The defaults preserve today's behaviour exactly (button
shown, booking-flow target).20260717180000_genericise_starter_lodge_copyis a data-only UPDATE of
the starter privacy/terms/FAQ page copy, guarded on an exact match of the
full previous shipped HTML, so any page an admin has edited is left
completely untouched.20260717210000_xero_member_grouping_dry_runcreates the cold
XeroMemberGroupingDryRunprovenance table that enforces dry-run
freshness before a bulk re-sync. DB-only and idempotent; zero Xero calls.20260717220000_add_club_identity_facebook_urladds nullable
ClubIdentitySettings.facebookUrl. A NULL value falls through to
club.json, and the boot-time config self-heal backfills it without
overwriting an admin edit.
- The one contract migration,
20260714140000_drop_committee_member, drops
the legacy standalone committee directory table. Its expand predecessor,
20260629130000_add_committee_roles_assignments, shipped inv0.11.0
(deployed 2026-07-13) and backfilled roles/assignments while the table still
existed, so the drop loses no data beyond the retired directory itself — no
assignment or contact data lives only in the dropped table — and the
required expand release is fully drained. The old colour's admin committee
CRUD routes still read the table, so between migrate and cutover those
admin-only routes error with relation-does-not-exist; public committee and
contact surfaces read from assignments on both colours and are unaffected.
Deploy with old-colour admin traffic idle or drained, cut over promptly,
and use
ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1with a non-empty override reason
acknowledging this window. - Two expand migrations carry window-bounded old-colour caveats documented in
their ledger rows.20260717170000_joining_fee_modelre-keys the
entrance-fee Xero item-code mappings fromENTRANCE_FEEtoJOINING_FEE;
once that runs, the old colour resolves both the item code and the
amount of a new entrance-fee invoice from the legacy flat mappings, so
during the migrate→cutover window it can mint a wrong per-category amount,
or — if the flat amount is unset — mark the operation SUCCEEDED and
silently never create the invoice. Operations queued before the window
carry frozen amount/item payloads and replay safely. Membership approvals
and entrance-fee minting must therefore be fully idle on the old colour
from migrate until cutover.20260716140000_xero_member_groupingconverges
grouping rules, so a membership-type rule save on the draining old colour
should be avoided. Deploy both in a quiet window and cut over promptly. - No migration makes a Xero, Stripe, or SES call. No member is re-grouped in
Xero until an admin runs the dry-run and bulk re-sync in
docs/XERO_MEMBER_GROUPING_RUNBOOK.md. - The new fee model backfills day-one amounts from the existing configuration
and retains the legacySeasonRateboolean keys and flat Xero mapping
amounts, so both colours price season and annual fees identically during
cutover; entrance/joining fees carry the old-colour window caveat above. CONFIG_BUNDLE_IMPORT_PATHis for disaster recovery and cloning only: it
imports a configuration bundle at boot only when the database holds no
non-seed configuration, and is not a database backup. Continue to use and
restore-testpg_dumpbackups.
Upgrade and verification
Follow docs/UPGRADING.md from v0.11.0 to v0.12.0. In summary:
restore-test a fresh backup, review every pending migration against the safety
ledger, schedule a quiet low-write window, confirm the committee contract
window, deploy through the documented override path, and cut over promptly.
After cutover, verify at minimum:
- Club identity and configuration — admin club identity (name, short
name, hut-leader label...
AlpineClubBookingsNZ v0.11.0
Release target: v0.11.0 on 2026-07-13. This minor public reference release
brings all changes landed after v0.10.1 into one supported tag. The canonical
detailed entries remain in CHANGELOG.md; this page is the release/operator
overview.
Highlights
- First-class multi-lodge scoping across bookings, capacity, rooms/beds,
seasons/rates, instructions, member access, requests, waitlist, roster, kiosk,
hut leaders, lockers, promos, work parties, and operational reporting. - Portable Configuration Export & Import with dry-run, explicit matching,
category selection, merge/overwrite modes, bundle integrity warnings, and an
automatic pre-apply backup attempt when backups are enabled. Operators must
independently ensure a current, restore-tested database backup exists before
applying an import; Configuration Export is not a database backup. - Declared partner relationships, admin partner management, partner invitation
claim/cancellation, shareable-double placement, and capacity headroom bounded
by both double-bed inventory and the lodge sleeping ceiling. - Admin recovery workflows for retroactive bookings, date overrides, explicit
over-capacity admission, capacity holds, finished-stay restrictions, and
per-action member-notification choices. - Safer bed allocation for large/minor groups, whole-stay moves, draft
preservation, room continuity, clearer booking identity, and conflict
feedback. - Finance and credit hardening for Internet Banking credit-note allocation,
cancellation/refund restoration deduplication, Xero lock-date enforcement,
and scoped reconciliation. - Editable access-role definitions, permission-aware setup navigation,
committee contact routing, membership-type retirement/merge, and improved
admin queue/filter/pagination visibility. - Restrained Alpine design system across public utility, member, and admin
surfaces: editable brand tokens, semantic accessible states, responsive
tables/forms, consistent alerts/status/occupancy, reduced motion, skip links,
dark mode, and stronger empty/loading/filter/calendar patterns. - Eleven additional operational email templates are admin-editable while
remaining always-send; admin-initiated member email suppression is explicit
and audited where suppression is safe. - CI now treats the multi-lodge E2E suite and static/dead-code analysis as
blocking release safeguards.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains 30 migrations. Twenty-six have explicit safety-ledger
rows because they touch hot tables, contain contract/data operations, or need
a documented deployment plan. The four without rows were checked directly
against the validator and ledger policy and do not require rows:20260707115737_committee_contact_email_modeadds a new enum plus two
columns to the cold, admin-writtenCommitteeAssignmenttable. The
constantROLEdefault preserves every old-colour insert/read assumption;
there is no backfill, destructive SQL, or hot-table match.20260709120000_add_lodge_is_defaultadds a constant-default flag to the
tinyLodgesettings table, backfills one row, adds a partial unique index,
and replacesdefault_lodge_id()with an old-behaviour-compatible resolver.
Its migration comments record the ordering dependency on the preceding
timestamp repair; it contains no breaking/hot-table match.20260710120000_add_lodge_bed_typeadds a new enum and constant-default
bedTypeplus nullablebunkGroupto the cold lodge-bed inventory. Old
colours see every existing/new omitted bed type asSINGLE; no row rewrite
or destructive operation is involved.20260710140000_add_bed_allocation_second_occupantrelaxes the bed-night
uniqueness on the non-hotBedAllocationtable, adds constant-default
columns, backfills the denormalised type, and installs replacement/partial
unique indexes. Old-colour inserts remain primarySINGLEoccupants and
therefore retain the former one-row-per-bed-night behaviour. Run during
low bed-allocation admin activity for the index swap/backfill, but no
breaking-migration override or ledger row is required.
- The contract migrations that remove induction, finance-report, and legacy
email-lodge fields require the quiet/drained old-colour window described in
docs/UPGRADING.mdanddocs/BLUE_GREEN_MIGRATION_SAFETY.tsv. - The maximum sleeping-capacity fix can reduce bookable capacity when a lodge
has more active beds than its configuredLodgeSettings.capacity. Audit it
before deployment withdocs/CAPACITY_MODEL.md. - Configuration Export & Import is for configuration portability, not disaster
recovery. Continue to use and restore-testpg_dumpbackups.
Upgrade and verification
Follow docs/UPGRADING.md from v0.10.1 to v0.11.0. In summary: restore-test
a fresh backup, review every pending migration against the safety ledger, audit
capacity and default-lodge intent, deploy in a quiet low-write window, cut over
promptly through the documented override path, then verify lodge scoping,
capacity, module/member access, booking/bed-allocation operational views,
finance/Xero reads, email lodge identity, and the application theme.
This release does not publish an npm package (package.json remains private).
The supported artefacts are the Git tag/GitHub release and the repository's
container-image release workflow.
Publication evidence
- Release commit:
6161e6a5824dd03626edbf41d6f3924ae030e2df - Release PR: #1854 (merge commit; all required checks green)
- Main CI: run
29211672529— verify, migration drift, static analysis, secret scanning, Docker image security, and GHCR publish passed - Main E2E: run
29211672504— Playwright E2E and E2E multi-lodge passed - Main CodeQL: run
29211672220— passed - App image:
ghcr.io/thatskiff33/alpineclubbookingsnz-app:6161e6a5824dd03626edbf41d6f3924ae030e2df- OCI index digest:
sha256:9fcc4659c5a3c794e067e1dc4b59a749aa6d65fb5f4bda4a9abf6f5d4a5887bb
- OCI index digest:
- Migrate image:
ghcr.io/thatskiff33/alpineclubbookingsnz-migrate:6161e6a5824dd03626edbf41d6f3924ae030e2df- OCI index digest:
sha256:73528bac0698394aed71c1f8907cb49a005de27957ad35ae6d665c465111b358
- OCI index digest:
Maintainability follow-up
- No release-blocking follow-up remains. Continue tracking the non-blocking hotspots documented in
docs/MAINTENANCE.md.
This release does not publish an npm package because package.json remains private.
AlpineClubBookingsNZ v0.10.1
Tag: v0.10.1 · Previous: v0.10.0 (2026-07-07) · Merge commit: 331cb7e27.
What this release is
A patch release: four payment/booking-recovery hardening fixes and one operator
cleanup script on top of v0.10.0. No new features, and no behaviour changes
outside the raced/edge shapes the fixes close.
No database migrations, no schema changes, no post-upgrade actions.
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv gains no rows and either app color can
serve throughout the deploy. The standard procedure still applies: back up the
database before deploying (docs/UPGRADING.md, "v0.10.0 → v0.10.1").
Changes
- Quote re-send vs concurrent decline race closed — an admin re-sending a
booking-request quote while the member declines can no longer resurrect a
DECLINED/CANCELLEDrequest or send its quote email; the losing re-send
rolls back with a 409. (#1508, issue #1504) - Recovery replays converged with their inline Stripe bodies — the
refund-request and booking-modification recovery replays previously sent a
differentreasonunder the same idempotency key, so Stripe rejected the
replay withidempotency_errorand recovery retried to exhaustion instead of
converging (safe-failing; never a double refund). Shared per-path body
builders now guarantee byte-identical replays. (#1509, issue #1507) - Refund-appeal allocation plan frozen — the approve route computes the
per-transaction Stripe refund allocation once, uses it inline, and persists
the same slices to the recovery operation on inline failure, so a replay
re-requests exactly the original slices under the original idempotency keys.
Supersedes the completed-refund remainder heuristic; in the exotic mixed
Stripe + Internet-Banking appeal shape the route now refunds the plannable
Stripe portion inline and logs any shortfall (net Stripe money unchanged; the
IB portion still settles via credit note). (#1513, issue #1510) - Aggregate per-invoice cap on never-settled Internet-Banking credit mint —
multiple never-settled IB payments matched to a single invoice can no longer
collectively mint account credit above that invoice's cash. No current app
flow produces the aggregate shape, so real-flow mint amounts are unchanged.
(#1512, issue #1505) - New operator script
npm run payments:backfill-cancel-flattened— a
one-off, idempotent, dry-run-by-default backfill restoring the stored
Payment.statuson rows the pre-#1489 cancel defect flattened toFAILED
on cancelled bookings. The read path already compensates, so this is
cosmetic-only for stored rows; it makes no Xero and no Stripe calls. See
"Backfill cancel-flattened payment statuses" indocs/MAINTENANCE.md.
(#1511, issue #1506)
Optional post-upgrade cleanup
If your fork ever ran a pre-v0.10.0 build, booking cancellations may have
flattened captured (PARTIALLY_)REFUNDED payments' stored status to
FAILED. You can restore the stored rows with
npm run payments:backfill-cancel-flattened — dry-run first, review the
report, then re-run with --apply. Detail in docs/MAINTENANCE.md.
Validation evidence
- Release PR #1514 CI fully green at head
f9c396c75(tree identical to the
merge commit):verify(lint, typecheck, tests, build), Playwright E2E,
Migration drift check, Static analysis gate, CodeQL, dependency-review,
gitleaks (full-repo + PR-diff), docker-image-security. - Local on the release branch:
npm run lint0 errors (5 pre-existing
warnings, unchanged from the v0.10.0 cut);npm run typecheckpass (banner
alpine-club-bookings-nz@0.10.1). npm run db:check-driftnot run — no schema change in this release
(git diff v0.10.0..v0.10.1 -- prisma/is empty).- An independent review agent verified changelog completeness (all 5
first-parent merges sincev0.10.0covered, no direct commits), the
no-migrations claim, the version bump, and the UPGRADING/MAINTENANCE
cross-references before merge.
Images
Published by main CI for the merge commit:
ghcr.io/thatskiff33/alpineclubbookingsnz-app:331cb7e2784fedc862e6feca712d7ba3dcf13193ghcr.io/thatskiff33/alpineclubbookingsnz-migrate:331cb7e2784fedc862e6feca712d7ba3dcf13193
Maintainability follow-ups (non-blocking)
Unchanged from v0.10.0 — the current known-hotspots list in
docs/MAINTENANCE.md (large-file split queue headed by
src/lib/xero-operation-outbox.ts, queued for PR-b of #1272's replay-stack
co-location).
AlpineClubBookingsNZ v0.10.0
Tag: v0.10.0 · Previous: v0.9.0 (2026-06-27) · Merge commit: 0182cd7ea.
What this release is
A large quality-and-hardening wave layered on top of the public booking,
membership, and finance capabilities, followed by a remediation wave
(epic #1348) that closed the post-wave audit findings and a live-feedback
admin-UX wave (epic #1438). The public deployment shape is unchanged.
Downstream forks: read docs/UPGRADING.md and the Migration/deployment notes
in CHANGELOG.md before deploying, and back up your database first. This
release contains two destructive/behaviour migrations and other hot-table
migrations.
Post-upgrade actions you must take
These are the actions a fork operator has to take after upgrading. Full detail
and the exact migration names are in docs/UPGRADING.md (v0.9.0 → v0.10.0).
(a) Re-enable your capability modules — some features switch OFF on upgrade
Migration 20260627120000_core_module_defaults_off changes the default for the
high-risk capability modules — kiosk, chores, finance dashboard, waitlist,
Xero integration, bed allocation, and Internet Banking payments — to off,
and switches them off on any fork whose Admin > Modules row was never saved
by an admin (the untouched singleton default row). If you never opened and saved
Module settings, these features will turn off on upgrade.
Action: after upgrading, open Admin > Modules and re-enable the modules
you use once their provider/setup is ready. Rows an admin already saved are
preserved; general-purpose modules stay on.
(b) Complete or export in-flight inductions first — unfinished ones are deleted
Migration 20260702100000_induction_workflow_types moves inductions to a
single-Pass flow and, doing so, deletes in-flight (DRAFT/IN_PROGRESS)
per-item induction results and clears their self-assessment answers. Completed
historical inductions are kept.
Action: before upgrading, finish or export any inductions a member has
started but not completed. There is no recovery after the migration runs.
(c) Audit membership access roles only if you ran intermediate main
The contract migration 20260630120000_rename_member_role_to_user collapses
the legacy Member.role values (MEMBER/ASSOCIATE/LIFE) into USER and
recreates the role enum. It assumes no live deployment used the intermediate
Access-Roles window.
Action: if your fork deployed a main build between 2026-06-28 and
2026-06-30, run npm run db:audit-access-role-cleanup after upgrading and
resolve anything it flags. Forks that upgraded straight from the v0.9.0 tag
never entered that window and can skip this.
(d) The three verified-safe migration items — you do NOT need to re-audit
These are already recorded as blue/green-safe in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv; no fork action required:
ClubThemesub-AA gold theme bump is conditional — it only rewrites a
persisted theme still on the old sub-AA default; a theme you have customised is
left as-is.BookingGuestNightbackfill is automatic — it runs during migration and
old code ignores the table.- Access-role backfills are old-code-compatible — old code keeps reading
Member.role/financeAccessLevelwhile the new access-role tables are added.
(e) AgeTier NOT_APPLICABLE — deploy in a quiet window, or defer the backfill
Migrations 20260707000000_add_age_tier_not_applicable (enum add) and
20260707000100_backfill_org_age_tier_not_applicable (backfill) introduce a
NOT_APPLICABLE age tier for organisation-type members. The backfill is
old_code_compatible=no: a pre-v0.10.0 app color cannot read the new enum
value, so old-color reads of the flipped rows can error while both colors are
live (the classic blue/green enum-backfill hazard).
Action (per the owner decision on epic #1438, 2026-07-07): deploy both
migrations in a quiet window and cut over promptly, or defer the backfill
migration until the old color has fully drained, then run it (the UPDATE is
idempotent and safe to run late). The enum-add migration is a plain expand and is
safe either way. Detail and the ledger caveat are in docs/UPGRADING.md and
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv.
Behaviour changes to note
- Capability modules default off (see (a)).
- Booking Officer / on-behalf booking scope widened — Booking Officers can
open booking detail;bookings:editholders can create and quote on behalf of
members; an admin's own bookings still go through normal member payment paths. - Email preferences enforced on reminder and chores sends.
- Non-member hold policy is admin-toggleable.
- Cancellation policy — tiered credit restore (owner decision D7). A member
who paid entirely with account credit and cancels inside the 0%-refund tier now
gets nothing back, the same as a card payer; a captured-but-partially-refunded
cancel is tiered on the remaining value (#1493). See the committee heads-up
draft (docs/releases/v0.10.0-committee-headsup-draft.md) — brief your
committee before wider rollout.
Removed / changed routes
- The standalone
POST /api/bookings/cancelroute was removed earlier in the
line; cancellation goes through the consolidated cancel path. Forks calling it
directly should move to the current booking cancel flow.
Validation evidence (fill in at tag time)
npm ci— clean install on the release commit.npm run lint— result.npm run typecheck— result.npm test/ build / Playwright E2E — from the release PR's green CI run.npm run db:check-driftagainst a shadow database — in sync.- Migration-drift and static-analysis gates — green on the release PR.
Deferred to post-approval (owner-gated)
- Create the annotated
v0.10.0tag on the merged commit and publish this
GitHub release (checklist step 6). - Image names / commit SHA / GHCR references — record at tag time.
- Non-blocking maintainability follow-ups — carry the current
docs/MAINTENANCE.mdknown-hotspots list.
AlpineClubBookingsNZ v0.9.0
Release commit: e9752c19ed4a1e7b68d7b778148ec0e8d1a75e51
- Release classification: minor public reference release. The change set since
0.8.0adds public join flows, module controls, induction, locker,
finance-dashboard, provider-recovery, and security hardening while preserving
the existing public deployment shape. - Added group-booking join flows and APIs, including organiser-owned join
codes, member self-add, non-member email verification, organiser management,
organiser cancellation cleanup, public join pages, member dashboard context,
and protected route/API coverage for group joinability. - Added group-booking settlement options for both each-pays-own and
organiser-pays modes. Organisers can collect one combined Stripe payment or
one Internet Banking/Xero invoice for joined bookings, while joiners remain
linked to their own child bookings for capacity, status, and audit purposes. - Added lodge induction and sign-off workflows with induction templates,
section/item results, assigned signers, self-assessment capture, member
sign-off records, route access hardening, and nomination settings support for
deployments that require induction before membership completion. - Added member locker administration and allocation, including API validation,
unique locker names, dashboard/member context, and admin controls that can be
disabled through Admin Modules. - Added database-backed Admin Modules toggles for group bookings, lockers,
induction, work parties, promo codes, hut leaders, communications, and
skifield conditions, keeping deploy-time.envcapabilities as the outer
operator gate. - Added member category and profile metadata support, including Life and
Associate member categories, title, gender, occupation, life-member date,
comments, configurable member-field visibility, CSV import/export hardening,
and refreshed member edit/detail screens. - Added subscription booking lockout controls so clubs can block bookings for
members with unpaid annual subscriptions, configure the lockout behavior in
admin, and align the subscription year with either Xero's financial year or
an explicit local override. - Reworked the finance dashboard to use the single operational Xero connection
already used by bookings, payments, and subscriptions. Finance-specific Xero
OAuth routes, token storage, and finance Xero usage metering were removed,
while finance reports gained revenue reconciliation, chart-of-accounts
snapshots, KPI cards, trend/mix charts, balance-sheet, cash, costs, working
capital, pricing-sensitivity, and booking metric views. - Added Whakapapa/skifield condition widgets and admin cache controls with
cached report payloads, freeze windows, public endpoint handling, and module
gating for deployments that do not expose mountain-condition content. - Fixed image upload/runtime storage and visual-editor behavior, including
read-only root filesystem upload handling, image resizing, admin toolbar and
alignment tests, photo-gallery token rendering, and safer upload trace
redaction. - Improved email/provider recovery visibility with token-email recovery
actions, undeliverable admin-alert escalation, waitlist-offer email failure
surfacing, Xero amount-mismatch repair alerts, missing Xero refund credit-note
reporting, stale Xero operation/inbound-event recovery, exhausted payment
recovery health signals, and the consolidated operator queue. - Hardened security and idempotency boundaries, including source-scoped
processed webhook event claims, SES SNS SignatureVersion 2 enforcement,
Xero token refresh leases, payment-link/client-secret ownership tests,
group-join response neutralisation, mixed-method route boundary coverage,
public rate-limit proxy assumptions, and high-severity dependency refreshes. - Migration/deployment notes:
20260615110000_add_lodge_induction_signoffcreates induction template,
result, signer, and settings tables plusMember.requiresInduction; run
during low membership-admin traffic before enabling induction-gated flows.20260616120000_induction_assigned_signers_and_self_assessmentadds
induction self-assessment fields and assigned-signer records; avoid active
induction edits during cutover.20260618120000_add_group_bookingadds group-booking and join staging
tables for shareable join codes; open new group joins only after the new
runtime is live.20260619120000_add_booking_organiser_settledadds
Booking.organiserSettledfor organiser-pays child bookings; run during
low booking traffic and do not create organiser-pays joins until old app
colors have drained.20260619130000_add_group_booking_settlementand
20260620120000_add_group_settlement_internet_bankingadd combined group
settlement records for Stripe and Internet Banking/Xero settlement.20260620121500_add_whakapapa_report_cacheand
20260620133000_add_whakapapa_cache_frozen_untiladd cached skifield
report payloads and freeze-window controls.20260620145000_add_lockersand20260622100000_harden_locker_namesadd
member locker allocation and then enforce unique, bounded locker names;
resolve duplicate locker names before the hardening migration.20260621150000_scope_processed_webhook_event_idempotencyreplaces the
global webhook-event idempotency key with a(source, eventId)key so
Stripe, Xero, and SES events cannot collide across providers.20260621160000_add_xero_token_refresh_leaseadds the operational Xero
token refresh lease used to prevent parallel refresh-token rotation.20260622120000_add_module_togglesadds Admin Modules activation booleans
for the newly modularised features, all defaulting on for upgraded installs.20260623120000_add_member_status_fields,
20260623130000_add_member_gender_title, and
20260626120000_member_field_visibility_and_categoriesadd the new member
metadata/category fields and settings; avoid assigning new enum categories
until the new runtime is serving traffic.20260626120000_add_membership_lockout_settingsadds the singleton
subscription booking-lockout settings row used by admin controls.20260626120000_add_chart_of_accounts_finance_snapshot_typeadds the
finance chart-of-accounts snapshot type used by revenue reconciliation.20260626121000_drop_finance_xero_storage_and_usagedrops the retired
finance-specific Xero token and usage tables after the runtime has moved to
the single operational Xero connection.