Patch Ledger for Downstream Sites
August 9, 2026 · View on GitHub
If you templated your app from Angular Momentum and then modified the code, you can't pull updates from this repo — but you can port them. This file is the living ledger of what changed in each AM release, organized by concern (what was wrong, what fixed it, why you care) rather than by commit or line — your code has diverged, so raw diffs won't apply. Commit hashes are included as reference anchors for digging deeper.
Maintainers: every release gets an entry here, in the same commit as the version
bump (alongside server/data/changeLog.ts). The changelog says what shipped; this file
says how a diverged fork applies it. bump_version.js inserts a TODO(release)
placeholder automatically, and the pre-commit hook refuses to commit until it's
replaced with the real entry.
How to use this file downstream
- Your copy of this file arrived with the template — it's already in your repo, and from the moment you diverge, it becomes your ledger while upstream's version is the source you re-sync from. (Templated from before 21.4.0? Fetch this file from upstream once to seed your copy.)
- Record your watermark at the top of your copy:
Synced through AM <version>— the AM version you templated from, then whatever you've caught up to since. Your app's own version numbers are irrelevant here — AM's version is the only anchor, and it lives in your copy, not in any code. - Work each unchecked item: apply it, adapt it, or consciously skip it. Check it off either way and note how you applied it — your implementation of a feature may be unique, and the note is what makes the next patch against that area tractable.
- To catch up later: fetch the current version of this file from AM, prepend any entries newer than your watermark to your copy, and repeat step 3.
Item tags: [server] [client] [build/deploy] [tauri] [test] —
test-only items can't break your production app; port them if you kept AM's test
suites. A *Superseded:* line means later releases changed or replaced the work —
read it before spending effort, especially when auditing several releases at once.
This ledger covers every release after 21.2.19 (the earliest template snapshot in
the wild). If you templated from something older, first reconcile against the in-app
changelog (server/data/changeLog.ts) and git history up to 21.2.19.
21.6.2 — 2026-08-09
- [client] Scroll-away header limited to mobile widths (
0c62124)ScrollIndicatorDirectiveslid the header out of the way on every viewport. That trade — motion in exchange for vertical space — only pays when the space is scarce; on a desktop it's movement for its own sake, and a header that holds still is easier to aim at. Now gated to viewports belowSCREEN_SIZES.md; wider ones keep a plain sticky header. The gotcha if you port this: two code paths write the transform. The scroll handler is the obvious one, butcorrectHeaderPosition()— a 150ms debounced correction that runs after scrolling stops — writes it directly too, so gating only the handler leaves a desktop header that sits still while you scroll and then slides away a beat after you stop. Crossing the breakpoint upward must also clear any transform the narrow layout left behind, or a resize strands the header off-screen. Note the existing directive specs had to start stubbingwindow.innerWidth; without it they silently depend on the Karma browser's actual window size and would flip behaviour on a different runner.
21.6.1 — 2026-08-08
- [client] Offer sign-in immediately when the app drops a session (
86d3630) A lapsed refresh token surfaces at startup, not mid-session — it expires while the app is closed, so the user reopens to an anonymous app and needs two clicks (profile icon → Log in) to recover from something the app did to them. The auth menu now opens straight to the login form in that case. The part worth copying is the discrimination, not the auto-open: asession_existedmarker is set whenever a session becomes active, cleared only on deliberate logout, and pointedly not cleared when a refresh fails. That asymmetry is what separates "we dropped you" (prompt) from "you signed out" (stay quiet) from "no account here" (stay quiet) — without it, an auto-opening login dialog is just a nag, which matters a lot if your fork also renders every route anonymously. Implementation notes: the marker holds no identity (a bare boolean, so the form opens empty for the browser's password manager — on a shared device this reveals that somebody has an account, not who); the prompt is deferred to the existing returnUrl auto-open so the two paths can't race; and it is guarded to fire once per app load so later signal churn or navigation can't re-pop the menu. AM hangs the detection offinitializeSession()'s existing "refresh failed, clearing stale session" branch — find the equivalent point in your own bootstrap. Caveat worth inheriting: this makes a dropped session comfortable, not correct. WithpersistSessionandautoRefreshTokenon and long-lived refresh tokens, frequent logouts indicate a persistence bug (Tauri's localStorage adapter, a 401 handler logging out on a refreshable error, a storage clear) that this feature will happily paper over. Measure the frequency before concluding it's fixed.
21.6.0 — 2026-08-08
-
[client] UI preferences survive sign-in and are cleared on every sign-out (
e7f265a) Theme/timezone/language were promoted from the anonymous scope only when the user accepted the "import your local data?" prompt, so choosing Skip silently reverted preferences they had just set. The distinction that matters: the import prompt is about content that might belong to a previous user of a shared device, whereas preferences describe how the app should look for whoever is using it right now.StoragePromotionService.promotePreferences(userId)now runs on every sign-in before the import decision, copying only the three preference keys (existing user values still win, so a returning user is never overwritten). Two adjacent gaps closed inUserSettingsService:resolveThemeConflict/resolveTimezoneConflictreturned a default without persisting it when neither side had a stored value (the brand-new-account case, so the choice was lost again on next load) — both now sync to the server, and timezone also writes locally, without which the profile page had nothing to read back and rendered its empty "Select timezone" placeholder. Andclear()now also runs on the interceptor's 401 path (an expired session used to leave its theme applied) and shares one in-flight run, so the auth-state effect and the explicit logout handlers collapse into a single pass. To port: if you kept AM's anonymous→user promotion, add the preferences-always path and check your own conflict resolvers for the both-sides-empty branch; if you replaced storage scoping entirely, the transferable idea is that preferences and content deserve different consent rules. -
[client] IndexedDB demo no longer persists values it just loaded (
e48df8c)loadStoredValue()set the form control withemitEvent: false, then dispatched a syntheticinputevent at the textarea to refresh the PrimeNG float label. Angular'sDefaultValueAccessorlistens for exactly that event, so the load round-tripped intovalueChangesand the debounced auto-save wrote the loaded value back into whichever scope had just been loaded — planting''in every fresh user's storage at login. The dispatch was unnecessary (pTextarea refreshes its filled state from the control value during change detection). Worth auditing in your own code: anysetValue(…, {emitEvent:false})followed by a manual DOM event dispatch re-enters the value pipeline and defeats the flag. Affects the demo component only in AM, but the anti-pattern travels. -
[client] Anonymous profile is labelled, and the header prompts on mobile (
7c898fc,271e9c7) Signed-out visitors saw a bare "Profile" heading in both the page and the account menu; both now read "Anonymous Profile" (newprofile.Anonymous Profilekey in all ten locales plus the translation schema — the schema is a closed allowlist, so new keys must be added to bothpropertiesandrequiredor validation fails). The header Sign up CTA no longer hides below thesmbreakpoint, where prompting anonymous users matters most. Header action glyphs carry no horizontal padding, so the rowgapis the entire separation between touch targets — widened to 1.8rem (2.2rem belowlg). Vertical padding moved off the Sign up pill and onto the always-present profile glyph button, so the header's height driver is constant across auth states. -
[client] Toasts anchor to the bottom on phones and tablets (
271e9c7) PrimeNG's defaulttop-rightputs toasts directly over the header action glyphs on a phone — covering the very controls that raise most of them, in the corner hardest to reach one-handed. Overridden below thelgbreakpoint to bottom-centre, clear of the footer rail andenv(safe-area-inset-bottom). Done in CSS rather than binding[position]because the input is static and a responsive binding re-instantiates the toast on resize. Also collapses empty.p-toast-detail: PrimeNG renders the element even for summary-only toasts, and its top margin plus the text column's flex gap showed up as ~11px of lopsided bottom padding. -
[client] PrimeNG button loading spinner alignment (
271e9c7) ThepButtondirective (unlike thep-buttoncomponent, where the class lands on the svg itself) renders loading as a<span class="p-icon">sized to--p-icon-sizewrapping a hard-coded 14×14<svg>. The undersized svg sat on the span's text baseline in the corner of an oversized box, and its stroke — drawn to the edges of the0 0 14 14viewBox — overhung its own box, reading as the arc escaping its container. Fixed by centring the svg and letting it fill the span (> svg { width: 100%; height: 100% }). If you set--p-icon-sizeaway from PrimeNG's default, you have this bug. -
[test] E2E flake tail root-caused: four distinct causes, no timing noise (
a50522a) Each retry-passer had a real mechanism. (1) A broadcast from a parallel worker reaches every connected client, the anonymous page stores it, andhasAnonymousData()is then true at the next login in any worker — so an unexpected Import Local Data dialog blocked login. Logins now route throughwaitForLoginComplete(), which resolves on the profile menu, that dialog, or an error toast. (2) Two tests clicked SSR-rendered buttons before hydration bound their handlers; the click is silently inert — addedwaitForAngular(). (3) The server broadcast is a bareio.emit: at-most-once, only to sockets connected at that instant, and a page right after login may be mid-(re)connect, so a lost emit could never arrive — the spec re-sends until one lands. (4) The performance suite tours six routes before measuring and had no headroom in the 30s default. Also:screenshotPageComponentclipped past the bottom of the page on short viewports, and Playwright fills that overflow with black — baselines had a black bar baked in that grew as content shrank. Port whichever of these your suite shares; (1) and (3) are properties of broadcast-over-websocket plus parallel workers, not of AM. -
[test] Flow reporter no longer clobbers the Playwright HTML report (
c752dfe) Both reporters were pointed atplaywright-report/. The HTML reporter treats that folder as exclusively its own and clears it, so the diff images vanished fromdata/whileindex.htmlstill loaded — every Actual/Expected pane rendered as an empty checkerboard, which looks like missing snapshots rather than a tooling collision. The flow reporter now writes toflow-report/(gitignored; uploaded alongside the HTML report in CI). If you added any custom reporter with an output path, check it isn't sharing a folder with a built-in one. Also suppresses two Sonar false positives (Web:ItemTagNotWithinContainerTagCheckfor<div>- groupeddt/ddinside a<dl>, spec-legal since HTML 5.2;typescript:S2925for performance.spec's heap-settle measurement windows) — verify rule keys against the SonarCloud API rather than inferring them from the message text, which is how the first attempt at both got wrong keys that would have silently done nothing. -
[server] Transactional email templates homogenized (
b33073f) The five stored templates had drifted apart in heading case and greeting style, and one referenced a "Forgot Password" control with two closing curly quotes (the real label is "Forgot your password?"). Now: imperatives for action emails, statements for notifications, "Hi there," throughout. Note these are body templates only — subject lines live in the Supabase dashboard, not the repo, and must be updated by hand; the commit message carries the matching subject list.
21.5.0 — 2026-07-31
-
[client] Supabase auth errors keyed off published error codes (
6aba374) Several auth flows leaked raw English error text regardless of locale: the OTP component parsed Supabase errors with the server-API parser, the profile page seterror.messagestraight into the UI, and unknown errors fell back to the raw message as a translation key. The helper now mapsAuthError.codeagainst theErrorCodeunion shipped inside@supabase/auth-js(typedPartial<Record<ErrorCode, string>>— upstream code renames become tsc failures, which is the bit-rot detection), wraps unknowns in a translated "Something went wrong: {detail}" shell, and keeps exactly one message-text regex (rate-limit seconds, failing soft to a countdown-less variant). To port: copyclient/src/app/helpers/supabase-error.helper.tswholesale (it was byte-identical across forks before this release), add the two newerror.*keys to your locale files, and audit every component that renders an auth error for the translate-with-params pattern (parseSupabaseError→translate(key, params)). -
[tauri] Adaptive Android launcher icon at stock glyph proportions (
0a15c2f) Launchers wrap legacy PNG icons in a white disc and shrink the whole padded square, so artwork rendered ~45% of the circle next to stock apps' ~60%. Baretauri iconis no fix: its adaptive foregrounds put artwork at ~90% of the 108dp canvas, which the 66dp safe zone crops. Newclient/scripts/android-adaptive-icons.jsrebuilds the per-density foregrounds from the same padded desktop source with the artwork atARTWORK_FRACTION(0.5) of the canvas, plus a white background layer, a monochrome layer for Android 13+ themed icons, and themipmap-anydpi-v26XML;npm run tauri:iconschains it aftertauri icon. This replaces the older downstream ritual of deletingmipmap-anydpi-v26/after regens — adopt the script instead. Also delete the unreferenced Android Studio template drawables if yourgen/androidstill carries them (drawable/ic_launcher_background.xml,drawable-v24/ic_launcher_foreground.xml— the green grid is one launcher-cache accident away from ringing your icon). Requires ImageMagick on the dev machine. -
[client] Theme cookie no longer outlives the session that wrote it (
beaf2bd)applyThemewrites a 1-yearthemecookie for SSR, and the logout reset only ran on a live authed→unauthed transition inside a running tab — so a session that ended by tab close or expiry left the last user's theme styling anonymous SSR loads indefinitely (light theme, nobody logged in). Fix:applyThemesplit into DOM application + cookie write, newresetTheme()restores the built-in default (dark, matching index.html'sapp-darkclass) and deletes the cookie withmax-age=0. It's called fromclear()when the anonymous scope has no stored theme, and fromloadLocalPreferences()when startup finds no theme for the current scope. To port: if you kept AM's user-settings service, take the whole diff; if you rolled your own theming, the concern is the same — any theme persisted outside user-scoped storage (cookie, bare localStorage) needs an explicit delete on logout and a cold-start reconciliation, because the logout event is not guaranteed to fire. -
[client] Dialog curtains fade again — don't pin backdrop opacity (
c2b0845).app-overlay-backdrop { opacity: 1 }(plus a redundant showing-class rule) was defeating CDK's built-in backdrop fade in both directions and stalling backdrop removal on CDK's 500ms fallback timer, sincetransitionendnever fired. Fix is deletion: style the backdrop's background/blur but leave opacity alone — CDK handles 0→1 on open, 1→0 on close,prefers-reduced-motion, and forced-colors on its own. Check your fork's overlay styles for the same pin. -
[build/deploy] Sync sonar.projectVersion with releases or the main gate rots
sonar-project.propertiesshipped withsonar.projectVersion=1.0hardcoded. The quality gate measures "new code" inprevious_versionmode, so a version string that never changes pins main's new-code window at the first analysis forever — old issues accumulate as "new" until the gate fails on a deploy that touched none of them (staging passes: long branches carry their own younger window). Fix:bump_version.jsnow rewritessonar.projectVersionon every bump, which resets the window to each release's diff. If your fork gates on Sonar with previous_version mode, wire your version bump to your Sonar config the same way. -
[tauri] Known issue: mobile dev proxy drops POST bodies (
8ded706) Knowledge item, no code to port:tauri [android|ios] devsometimes routes the webview through Tauri'stauri.localhostproxy, which drops POST bodies (tauri-apps/tauri#13166) — assets and health checks work, so the app looks online, but every login 400s with an empty body. Diagnostic: check the page origin in the webview inspector;tauri.localhostmeans proxied (broken), a LAN-IP origin means direct (fine). The CLI--hostflag should pin the direct path but is untested.
21.4.1 — 2026-07-14
-
[build/deploy] Deploys keyed off releases, not commits (
5de81cf) Every green push to main triggered the full deploy chain — Heroku rebuild plus all five Tauri platform builds — even for docs or tooling changes that altered no app content. Fixed with acheck-releasegate job in the deploy workflow: if the current version already has a GitHub release, the whole chain skips; a new (unreleased) version deploys as before, and manualworkflow_dispatchalways forces a deploy (the retry/redeploy lever). Apply: if you kept AM's deploy workflow, port the gate job and point the downstream jobs' conditions at its output. If your fork deploys differently, the transferable idea is: gate on "is this version already released," not on "did CI pass." -
[build/deploy] This ledger, and the tooling that keeps it honest (
5aec961,4aff60b,f08e71f,50900c2) docs/PATCHES.md itself shipped in this release, backfilled to the 21.2.19 template baseline. The release tooling enforces it upstream:bump_version.jsinserts aTODO(release)placeholder entry on every bump, and the pre-commit hook refuses all commits until it's replaced with real notes (it also blocks empty changelog placeholder entries). Fork safety is built in: bumps only write the ledger when the package name and repository match upstream, so your own releases never stamp your versions into a file keyed to AM's. Apply: nothing to port — your copy of the ledger and the guard arrived with these files. Start using it: record your watermark and work the entries. If you want the same fill-before-committing discipline for your own changelog, the hook pattern transfers directly. -
[test] E2E determinism: durable logout, permission mirroring (
aa3f827,eadde21) Two portable lessons from chasing CI-only failures. (1) Logout is only durable once the auth provider clears its persisted session — navigating right after the UI updates can boot the next page still authenticated; the indexeddb scoping test now polls until the Supabase token is gone from localStorage, and its post-reload logged-out check asserts positive signals (header rendered, zero profile links) instead of a not-visible check that passes vacuously on a booting page. (2)Notification.permissionon macOS requires OS-level authorization, absent on CI runners — the permission test now asserts the UI mirrors whatever the browser reports rather than assuming the context grant surfaces as 'granted'. Apply (if you kept AM's e2e suite): port both patterns; they generalize to any test that reloads after logout or asserts on browser-mediated permission state.
21.4.0 — 2026-07-13
-
[server] [client] WebSocket auth-expiry eviction + silent client recovery (
31bc253,bcb55e6) Sockets that authenticated once stayed in their user's broadcast room forever, even after the auth token expired. The server now reads the JWTexpclaim at authentication and schedules an eviction: at expiry the socket leaves its user room and receives anauth-expiredevent. The client listens for it, refreshes its session, and silently re-authenticates the socket — or signs the user out if the session is truly dead. Apply: add the expiry timer to your websocket auth handler and theauth-expiredlistener wherever your client manages socket auth (AM:server/services/ websocketService.ts,client/src/app/services/user-settings.service.ts). Without it, a tab left open past expiry keeps receiving user broadcasts it shouldn't, or silently stops syncing. -
[build/deploy] Supervised web dyno; compiled server JS (
87c0b17) The Procfile backgrounded the API viats-nodein a subshell, so Heroku only watched the SSR process — an API crash left the dyno serving an app whose every API/websocket call failed until the daily cycle, and asleep 3raced API boot. Fixed with a small supervisor (scripts/heroku-web.mjs): starts the compiled API (newserver/tsconfig.build.json, built duringheroku-postbuild), waits for/api/healthbefore starting SSR, exits non-zero when either child dies so the platform restarts the dyno, and forwards SIGTERM for graceful shutdown. Apply: if your Procfile backgrounds one process behind another, port the supervisor wholesale (it's self-contained) and add a server compile step. Requires the/api/healthendpoint (21.3.0) and theprocess.cwd()static-path fix (21.3.0).typescriptmust live independenciesif your host prunes devDeps. -
[tauri] Content Security Policy for the webview (
8e59a5a,1d5d06f,0847162) The desktop/mobile shells shipped"csp": null— any XSS ran unrestricted, with reach into the Tauri IPC surface. Fixed by defining a real policy intauri.conf.json, with three hard-won concessions:'unsafe-eval'(the ICU translation compiler builds functions at runtime — without it the app black-screens), style-hash injection disabled viadangerousDisableAssetCspModification: ["style-src"](injected hashes neutralize the'unsafe-inline'Angular's runtime styles require), and'unsafe-hashes'plus one sha256 for the async-CSSonloadhandler. Full reasoning and test procedure:docs/CONTENT_SECURITY_POLICY.md. Apply: copy the policy, then replace the origins with yours (API domain, Supabase project, analytics). Test with a bundled--debugbuild —tauri devdoesn't apply the policy — and clear the app's webview storage between attempts, because the service worker caches the old policy's HTML. -
[server] Test-only endpoints require a loopback peer (
8a1fc19) The/api/auth/test/*endpoints (user create/delete/cleanup) were guarded only by NODE_ENV — one misconfiguration away from exposing user management to the network. They now also verify the TCP peer is loopback. Apply: port the guard middleware if you kept the test endpoints. -
[server] og-image endpoint rate-limited and capped (
402b56d) The screenshot endpoint drives headless Chromium; even host-allowlisted (21.3.8) it was open to resource exhaustion. Now rate-limited with a response cap. -
[server] Unauthenticated username-creation endpoints removed (
d389a10) Username creation happens only through the authenticated signup flow now — the standalone REST endpoint had stayed open even after 21.3.8's mutation auth-gating (it lived on the auth router, which that middleware deliberately didn't wrap). Apply: if your fork kept the standalone endpoints, remove or auth-gate them. -
[test] E2E trustworthiness overhaul (
8332ed6,0064eaf, and follow-ups) The suite could pass while features were broken: dozens of assertions were wrapped in if-visible guards (three referenced selectors that never existed, so those checks had literally never run), and ~139 hardwaitForTimeoutcalls made it slow and flaky. Fixed by making every check unconditional, replacing sleeps with state-based waits, loosening full-page visual comparisons slightly (0.005 ratio) with the footer version masked so releases don't churn baselines, and adding an end-to-end test that forces websocket auth expiry (via a loopback-guarded test endpoint) and asserts the client recovers. Two portable gotchas surfaced: the shared login helper must handle the auth menu opening in signup mode (its default on a fresh open), andNotification.permissionon macOS requires OS-level authorization — assert your UI mirrors what the browser reports, never assert 'granted' on CI. Apply (if you kept AM's e2e suite): audit for the same three rot patterns — conditional guards, hard waits, and selectors that don't exist (they fail silently inside.catch(() => false)).
21.3.8 — 2026-07-10
Note: 21.3.8 was never deployed on its own — it shipped to production together with 21.4.0. Its README "Maintenance TODO" additions are a preview of 21.4.0's work, not open items you must adopt.
-
[server] Mutations require authentication (REST + GraphQL) (
c3549ff) Feature-flag and notification mutations accepted unauthenticated writes — anyone who could reach the API could flip flags for every connected client or send notifications. Fixed with arequireAuthForMutationsmiddleware (reads stay public; non-GET requires a valid Supabase Bearer token) wrapping the affected routers, and a GraphQL-level check that parses each document and applies the same rule only to mutation operations. Apply: port the middleware (server/middleware/requireAuth.ts) and wrap every state-changing router your fork added on the templated pattern — the mutation surface was open by default. Note it deliberately no-ops in development/test NODE_ENV; verify production actually enforces. Superseded: extended in 21.4.0 — the unauthenticated username-creation endpoint wasn't covered by this middleware and was removed outright (d389a10). -
[server] og-image screenshot endpoint restricted to own hosts (
c3549ff) Classic SSRF: the endpoint screenshotted whatever URL the caller supplied, meaning anything the server could reach — internal services included. Fixed by parsing the URL, restricting to http(s), and checking the hostname against a hard-coded allowlist of the app's own origins. Apply: if you kept og-image generation, port the allowlist block and use your domains. Superseded: extended in 21.4.0 with a rate limit and response cap (402b56d) — apply both together. -
[build/deploy] Deploy and quality gates made real (
014c549) Three gates looked like gates but weren't: the deploy workflow'sworkflow_runtrigger fires on failed CI too and the success check was commented out (every push deployed regardless of tests); the Sonar step only waited for analysis to finish, never checking the quality gate verdict; and the local test harness had no error handling, so a failing step scrolled past andnpm testexited green. Fixed by requiringworkflow_run.conclusion == 'success'on all deploy jobs, querying Sonar'sproject_statusAPI and failing on anything but OK, and running the test harness underset -euo pipefail. Apply: check your deploy workflow's trigger conditions first — this is the one that ships broken builds. Superseded: refined twice later. 21.4.0 (1429dc7) enforces the Sonar gate on main only (SonarCloud's plan refuses gate data for other branches, and the original died silently there). After 21.4.0 (5de81cf), acheck-releasejob keys the whole deploy chain off whether the current version is already released, so housekeeping merges stop triggering redeploys. Port the current workflow state. -
[test] 100% coverage mandate enforced in tooling (
014c549) The coverage bar was convention only. Now hard-coded: jestcoverageThresholdand karmacoverageReporter.check, all four metrics. Apply: set thresholds to whatever bar your fork actually holds — but set something, or the number is decorative. -
[server] ngsw-worker.js served no-cache from the server users actually hit (
3537c54) 21.3.6 added no-cache headers for the service-worker script — on the API server, which browsers never ask for it. The SSR server was still serving it throughexpress.staticwith a 1-year maxAge, so worker updates could stall for a day or more. Fixed with an explicit no-cache route on the SSR side. Apply: find which of your processes actually answers/ngsw-worker.js(devtools Network tab) and make that one send no-cache — misattributing this was the bug. -
[tauri] Android minSdkVersion 35 → 24 (
3537c54) minSdk 35 limited the app to Android 15+, hiding it from nearly every Play Store device. One-line config change; check what your native plugins actually require. -
[client] rel="noopener noreferrer" on external target=_blank links (
3537c54) The external donation link handed the opened page awindow.openerhandle back into the app (reverse tabnabbing). Fixed on the templated anchor. Apply: grep your fork for everytarget="_blank"you've added and add the rel. -
[build/deploy] Version bumps update every lockfile occurrence (
01924ed) The bump script's lockfile patterns lacked the global flag, so only the first name/version pair was updated each release — lockfile entries silently drifted behind. Fixed with/gand a re-sync. Apply: grep your lockfiles for your own old version numbers; the drift is probably already there. Superseded: the script gained more responsibilities after 21.4.0 (patch-ledger placeholder + upstream detection) — take the currentbump_version.jswholesale if syncing past that. -
[build/deploy] Xcode 26 migration finished for mobile CI (
f488165,79816a6) App Store uploads now require the iOS 26 SDK, but CI runners default to older Xcode. CI explicitly selects Xcode 26.3, and the iOS project weak-links the Swift compatibility shims Xcode 26 dropped (Tauri's precompiled binaries still reference them; the link otherwise fails). This is the finished state of the migration started in 21.3.7 — port this state, not 21.3.7's intermediate.
21.3.7 — 2026-06-24
-
[server] [client] Turnstile CAPTCHA removed — bot defense is email OTP + rate limiting (
7713456,39e4b74) The Cloudflare Turnstile pipeline (invisible widget on signup, token smuggled through Supabase metadata, server-side verify that admin-deleted failures) was removed entirely. Nothing new replaced it: the two replacement mechanisms — email OTP verification (unverified signups never get a session) and API rate limiting — already existed in the template. The teardown spans the client widget and its form plumbing, the server verify service and its env keys, the i18n strings, the privacy-policy paragraph, and the CAPTCHA docs; the signup-verification webhook was kept as a payload-validating stub for future server-side signup checks. As a side effect, the focus-coordination machinery the widget required collapsed to a simple focus-on-open, and signup visual baselines were re-shot. Apply: if your fork ported Turnstile from an earlier AM state, first verify OTP- rate limiting work, then tear Turnstile out (the
7713456diff is the checklist). If you never had it, check this off.
- rate limiting work, then tear Turnstile out (the
-
[build/deploy] Post-build scripts no longer swallow ng build flags (
bdd0392)npm run build -- --flagappended the flag to the whole script chain, so post-build scripts (like the service-worker patcher) received it as their argument and misfired. Fixed by splittingng buildinto its ownbuild:ngscript so--reaches only the Angular CLI. Apply: if your build script chains anything afterng build, split it the same way — any flagged invocation is currently corrupting your post-build steps. -
[server] [client] ReDoS regex fixes (
2710150) Two polynomial-backtracking regexes reachable from user input — the slug pipe and the username hyphen-trimming pattern — could burn CPU on crafted input. Fixed by restructuring into sequential, anchored passes. Apply: port both if you kept the slug pipe or username sanitization; they're drop-in (client/src/app/pipes/slug.pipe.ts,server/constants/username.constants.ts). -
[build/deploy] iOS deployment target → 17.0 (
b8f4ca7) First half of the Xcode 26 migration (raising the target lets the linker drop pre-17 Swift compat requirements), plus a tauri-action pin bump off a deprecated Node runtime. Superseded: 21.3.8 finishes this migration — port that state instead. -
[test] Empty e2e test bodies got real assertions (
2710150) Several e2e tests ended without asserting anything — green no matter what. Superseded: 21.4.0's e2e overhaul rewrote the same specs far more aggressively; skip straight to that state if you're porting the suite.
21.3.6 — 2026-06-22
-
[client] [server] Service-worker offline resilience overhaul (
e2016d7) Five coordinated fixes to how the PWA survives bad networks, each preventing a distinct failure. (1) A post-build script (client/scripts/patch-ngsw.js) defers the generated worker'sskipWaiting()until its asset cache is fully populated — otherwise a network drop mid-install activates a worker with a half-empty cache and the app 504s offline instead of serving the previous version (angular/angular#45377). (2) Data groups switch from network-first to cache-first (performancestrategy) with shorter timeouts, so gateway errors from a flaky network can't clobber good cached data. (3) Asset groups getupdateMode: lazyso a partial background update can't wedge navigation. (4) The worker script itself is served with no-cache headers so browsers actually pick up new workers. (5)navigator.storage.persist()is requested at startup so the browser doesn't silently evict the cache and IndexedDB under disk pressure. Apply: each piece is independent; (1) is the highest-value and is a copy-in script chained afterng build— after Angular upgrades, confirm the[patch-ngsw]success line still appears in build output. Superseded: (4) was incomplete — 21.3.8 discovered the SSR server (the one browsers actually hit) was still serving the worker with a 1-year cache; apply both halves. -
[client] Eviction-resistant IndexedDB cache store (
e2016d7) Even with persistent storage, the SW cache is the most eviction-prone tier. A new IndexedDBcacheobject store (migration v4) with raw-keygetCache/setCacheon the IDB service gives critical app data a fallback that survives SW cache loss. Apply: if you kept AM's versioned-migration system, add the migration and the two service methods — but use your database's next free version number, not literally 4. -
[test] Hoisted-listener conversion completed; socket leak sealed (
a9910c8) Finishes 21.3.5's server-spec work: the last per-call supertest listeners became shared hoisted servers, and a cross-testapp.set('io')leak got cleanup hooks. Together with 21.3.5's item this is one concern — see that entry.
21.3.5 — 2026-04-26
-
[client] Signup streamlined (
e29eab7,b99a61c,96cdaca) Confirm-password was dropped (friction without protection — the complexity validator already catches typos), the username field prefills from the email prefix on blur (sanitized to the allowed charset), and auth fields auto-focus every time the menu opens, not just first mount. Apply: drop your confirm-password field and its i18n strings; portonEmailBlurfromauth-signup.component.ts. Superseded: the focus wiring was entangled with the Turnstile CAPTCHA and got simplified when 21.3.7 removed it — take the focus logic from the 21.3.7 state. -
[client] Anonymous-preferences reset detects the browser language (
e69454e) Logout reset preferences to a hardcodeden-US— wrong for every non-English browser. Fixed by matchingnavigator.languagesagainst the supported list (with the same parser SSR uses), and cleaning up a persist-plugin key that made never-customized users trip the "import local data?" dialog with phantom data. Apply: portdetectBrowserLanguage()inuser-settings.service.ts; the key cleanup only matters if you kept the anonymous-data-import flow. -
[client] Dialog UX fixes: onCancel callback + iOS tap-outside dismiss (
e7437b6,0445939) Two dialog-layer bugs: the import dialog detected cancellation by polling a signal every 100ms (leaked timers), fixed with a properonCancelcallback fired bydismiss(); and on iOS WKWebView, tapping outside a dialog did nothing because the CSS that enables touch-scrolling shadows the CDK backdrop — fixed by also listening on the overlay pane and dismissing when the tap lands in the gutter. Apply: both port to any fork that kept AM's CDK-overlay dialog pattern (confirm-dialog.service.ts,dialog-base.component.ts,dialog-menu.component.ts). -
[client] Feature-monitor skips its first tick until flags hydrate (
1c6524a) Feature flags are fail-closed before they load, so the route monitor bounced deep links to gated routes back to/the moment flags arrived. Fixed with a one-shot latch that swallows the first emission. Apply: any fork that redirects off feature-flag state in an effect has this deep-link race — add the same latch. -
[client] Small UX/logging cleanups (
1def518,78a8d37) The avatar initial now prefers the chosen username over the email's first letter; SSR-time fetch failures (expected during prerender) log at debug level instead of error; the dev proxy stops flooding e2e logs. Apply: grab what applies; all are one- or two-line changes referenced from the hashes. -
[test] One hoisted supertest listener per server spec file (
2e42d45) Per-call supertest listeners produced socket-hang-up flakes under parallel jest. Every server spec now creates one sharedhttp.ServerinbeforeAlland runs all requests against it. Completed in 21.3.6 (a9910c8), which converted the last stragglers and sealed a cross-testapp.set('io')leak. This is AM house style for all server specs now. Apply (if you kept AM's server test suite): grep forrequest(app— every hit becomesrequest(server)against a hoisted listener. -
[tauri] Android build targets JDK 17 (
8232f65) Current Android Gradle Plugin and Kotlin toolchains require 17; the generated project still targeted 1.8 and broke on up-to-date SDKs. Set source/target compatibility andjvmTargetto 17 in the generatedbuild.gradle.kts. -
[server]
Turnstile log sanitization— skip (a35e88c) Sanitized attacker-influenced CAPTCHA error codes before logging them. Superseded: 21.3.7 removed Turnstile entirely; nothing to port unless your fork keeps a Turnstile integration. The general lesson stands: sanitize third-party response fields before logging them.
21.3.4 — 2026-04-09
-
[test] Playwright timezone pinned so visual baselines survive CI (
0233316) Screenshot baselines captured locally (US Pacific) diverged on CI (UTC) wherever the UI renders times. Fixed by pinningtimezoneIdto a fixed, DST-free offset in the Playwright config. Apply: pin a timezone in your config, then re-capture any baselines that render times. -
[build/deploy] Sonar scanner via npx instead of a downloaded binary (
61e880e) CI curled a platform-pinned scanner zip; now it runsnpx sonar-scannerbacked by a devDependency. Skip if you don't use SonarCloud; if you do, port together with 21.3.8's quality-gate enforcement.
21.3.3 / 21.3.2 — 2026-04-09
- [build/deploy] Everything the production build invokes must live in
dependencies(82f1c3c,eab1e66) Two same-day hotfixes for one durable rule: Heroku prunes devDependencies, songwasn't on PATH at build time (21.3.2 moved@angular/cli), and then the build failed one package deeper (21.3.3 moved@angular/build). The project hit this a third time in 21.4.0 (typescript, for the server compile). Apply: audit your fork's whole build chain in one pass instead of playing whack-a-mole — on any host that prunes devDeps, every binary and library your production build touches goes independencies. AM's currentpackage.jsonfiles are the reference for where things landed.
21.3.1 — 2026-04-09
-
[server] Express 5 SSR fixes (
547554b) Express 5 rejects the bare'*'catch-all (the SSR server crashed on startup) — it's now'/{*splat}'. AndCommonEngineneedsallowedHostsor localhost requests silently fall back to client-side rendering, making SSR look broken in local testing and Lighthouse runs. Apply: change your catch-all pattern when you take Express 5; add your domains toallowedHosts. -
[test] Karma-wide fetch mock for the health endpoint; Actions bumps (
547554b) Companion to 21.3.0's health check: a global fetch wrapper inkarma-setup.jsanswers/api/healthso unit tests don't 404-spam. Also routine checkout/setup-node v3→v4 bumps in CI.
21.3.0 — 2026-04-09
-
[server] [client] Real
/api/healthendpoint for connectivity checks (72fcf47) "Online" was verified by fetching the favicon — which proves static serving, not API liveness. A lightweightGET /api/healthwas added and the connectivity service pings it instead. Apply: add the endpoint and repoint your connectivity checker. This endpoint becomes load-bearing later: 21.4.0's dyno supervisor polls it at startup, and the Karma mock in 21.3.1 depends on it. -
[build/deploy] SSR static path resolves from
process.cwd()(0a0914d) The API server resolved the client's dist directory relative to__dirname, which breaks when the server runs as compiled JS from a build directory. Fixed by resolving from the process working directory. Apply: prerequisite for 21.4.0's compiled-server supervisor — fix your path resolution now if you compile the server. (Same commit scoped Sonar's sources and quieted a tsconfig warning; copy-the-line items.) -
[server] Security dependency bumps —
express-rate-limitIPv6 bypass (44c5a73) The production-relevant one:express-rate-limit< 8.3.2 has an IPv6 rate-limit bypass, and rate limiting guards real endpoints here (and becomes the og-image defense in 21.4.0). Bump to ≥ 8.3.2 regardless of anything else in this release. -
[client] Share menu: social buttons, QR code, native share sheet (
beb40ef,72fcf47) New optional header menu offering social-platform share buttons, a QR code of the current URL with a branded overlay, and the native OS share sheet where available. Wiring spans the component directory, provider setup in the app config, a few new dependencies, styles, and strings in every locale file. Apply: optional feature — port it with your own branding and locales, or skip cleanly; nothing later in the ledger depends on it. -
[test] Share-menu visual specs; screenshot threshold moved into config (
4ae34cd) Visual specs for the new menu, andmaxDiffPixelRatiobecame a strict config-level global instead of per-test settings. Superseded: partially by 21.4.0, which keeps the strict global but loosens full-page shots (0.005) and masks the footer version. Apply both in one pass if catching up that far.