Repository navigation
chore(frontend/core): medium/low findings from the 2026-07-28 full review #134
Description
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.
Tracking issue for the MEDIUM and LOW findings from the 2026-07-28 full-repo review of the frontend core: frontend/src/api/**, frontend/src/lib/**, frontend/src/modules/**, frontend/src/groups/**, and the top-level cross-cutting modules (app.ts, auth.ts, state.ts, inventory.ts, ladder.ts, permissions.ts, navigation.ts, serviceFilters.ts, topbar-filters.ts, federation.ts, archera.ts, modal.ts, toast.ts, confirmDialog.ts, approval-details.ts, commitmentOptions.ts, purchases-deeplink.ts). The two HIGH findings from the same pass are filed as individual issues.
Each checklist item is independently actionable. Please keep the file:line, failure scenario and fix direction with the item when splitting any of these out.
Three recurring shapes account for most of the list:
||used where??is meant, so a meaningful0is read as "unset". Hits a spend cap (the HIGH), the bulk-purchase capacity percent, and the ladder buffer fraction.- An awaited loader renders unconditionally, with no request-generation guard, even though
frontend/src/topbar-filters.ts:83,92,103,113already implements exactly that pattern in the same codebase. - A listener is registered inside a function reachable more than once, with the dedupe applied to one registration and not its siblings.
Findings from this pass that are NOT in this list
Recorded so nothing looks dropped:
- Group edit modal dropping and widening permissions it cannot render: filed as its own HIGH issue, and closed by the same backend change as sec(auth): the #923 money separation-of-duties carve-out is voidable by any admin in one request cloud-commitments-cli#1550.
max_amount: 0erased on group edit: filed as its own HIGH issue.
Explicitly checked and clean
Recorded so the next review pass does not re-derive them: every innerHTML write reachable from this subsystem escapes its interpolations (groups/groupList.ts:37-80, groups/groupModals.ts:161-172, approval-details.ts:247-261, ladder.ts:254-269, auth.ts:401/1225/1310, commitmentOptions.ts:190-216), with lib/skeleton.ts, lib/chip-select.ts, lib/history-filter-popover.ts, modules/registrations.ts, inventory.ts, archera.ts, federation.ts, toast.ts, modal.ts, confirmDialog.ts and serviceFilters.ts createElement/textContent throughout; Permission.constraints matches the API DTO with no wire mismatch; permissions.ts canAccess/isPurchaser correctly mirror adminCarvedOuts; getAuthHeaders/clearAuth/the cross-tab storage listener are correct and no error path renders a token; getUserPermissions, getResetTokenStatus, setupMFA, enableMFA and regenerateMFARecoveryCodes all throw on shape mismatch; utils.ts money/date helpers return '--' rather than $0; lib/capacity.ts treats 0/null/NaN as unknown; lib/column-filters.ts returns an explicit error rather than a silent match-all.
-
A failed
/api/infofetch renders the first-run admin-setup screen instead of login (MEDIUM)- Where:
frontend/src/api/auth.ts:412-419(getPublicInfo), consumed atfrontend/src/app.ts:46-60 - What:
getPublicInfo()returns a fabricated{ version: '', admin_exists: false }on any non-OK response instead of throwing.init()wraps the call in atry/catchwhose comment says "If public info fails, fall through to login", but the catch never fires because the function resolves successfully with the fabricated value.!publicInfo.admin_existsis therefore true,showAdminSetupModal(undefined)runs, and thereturnmeans the login modal is never shown.getDeploymentInfo()atfrontend/src/api/auth.ts:427-435has the same shape (returns{}on failure). - Failure scenario: the API returns 502 or 503 (cold-start failure, deploy in progress, DB unreachable). Every unauthenticated visitor, including legitimate users who just want to sign in, is shown "Welcome to CUDly / No admin account exists yet. Set up the first admin to get started." with no route to the login form except the small "Already have an account? Log in" link. The screen asserts a false fact about the deployment state and hides the real error.
- Fix: throw on
!response.okingetPublicInfoso the existingcatchininit()reachesshowLoginModal(), and surface the transport failure to the user. Same silent-fallback class as fix(frontend): silent fallbacks fabricate money/coverage values on Dashboard, History, Plans, Opportunities cloud-commitments-cli#1086, different file and no money involved.
- Where:
-
Inventory loaders have no generation guard, so a slow response overwrites a newer one (MEDIUM)
- Where:
frontend/src/inventory.ts:102-131(loadActiveCommitments) andfrontend/src/inventory.ts:361-382(loadCoverageBreakdown) - What: both read
state.getCurrentProvider()/state.getCurrentAccountIDs(), await a fetch, then render unconditionally. Both are wired to the topbar chip subscriptions (frontend/src/inventory.ts:544-545,:558-560), so a chip change re-invokes them. There is no request-generation counter, unlikefrontend/src/topbar-filters.ts:83,92,103,113which implements exactly that pattern for the account chip. - Failure scenario: the user is on Inventory & Coverage -> Active commitments with the Provider chip on AWS (large result set, slow query) and switches the chip to GCP. The GCP request returns in 200 ms and renders 3 rows; the AWS request lands 4 s later and overwrites the table with hundreds of AWS commitments while the chip still reads "GCP". The user reads AWS commitment counts, expiry dates and dollar amounts as GCP's. The same race applies to the coverage percentages on the Coverage sub-tab.
- Fix: capture a monotonic
_gencounter before eachawaitand discard the render whengen !== _gen, mirroringpopulateAccountOptionsintopbar-filters.ts.
- Where:
-
Registrations status-filter listener stacks on every Accounts sub-tab visit (MEDIUM)
- Where:
frontend/src/modules/registrations.ts:254-258(initRegistrations), invoked fromfrontend/src/settings.ts:3346insideloadAccountsTab() - What: a bare
filterEl?.addEventListener('change', () => void loadRegistrations())with no dedupe marker and no priorremoveEventListener.loadAccountsTab()runs fromswitchSettingsSubTab'scase 'accounts'(frontend/src/navigation.ts:274-276), and thatswitchexecutes before theif (isSelfSwitch) return;guard atfrontend/src/navigation.ts:286, so even re-clicking the already-active Accounts sub-tab re-runs it. Every visit adds one more closure. - Failure scenario: an admin clicks Admin -> Accounts & onboarding five times while triaging registrations, then changes the status dropdown from "Pending" to "All". Five concurrent
GET /api/registrationsrequests fire and fiverenderRegistrationsTablecalls race into#registrations-list; the last response to land wins regardless of which filter value it was issued for, so the table can show "pending" rows while the dropdown reads "All". - Fix: use the
btn.dataset['wired'] === '1'marker already used atfrontend/src/inventory.ts:133-146and:384-392, or store and remove the previous handler asinitSavingsHistorydoes for the unit select (frontend/src/modules/savings-history.ts:576-584). Seefeedback_event_listener_dedup; same class as fix(frontend): plans.ts provider XSS (missing whitelist) + duplicate document/modal listeners (users, plans) cloud-commitments-cli#1034, different files.
- Where:
-
Savings-history unit toggle scales only one of two series on a dual-axis chart (MEDIUM)
- Where:
frontend/src/modules/savings-history.ts:382-389, chart config at:404-428, tooltip at:509-522 - What:
periodSavingsDatamaps throughconvertFromMonthly(dp.total_savings ?? 0, unit)(line 384) whilecumulativeSavingsDatausesdp.cumulative_savings ?? 0raw (line 388) with no conversion. The two series are plotted onyandy1of the same chart and rendered together under onemode: 'index'tooltip, where only the Period Savings line carries thesuffix. - Failure scenario: on the Purchases tab a user switches the Savings unit dropdown from "Monthly" to "Yearly". The Period Savings line jumps 12x; the Cumulative Savings line does not move. Switching to "Hourly" divides one line by 730 and leaves the other unchanged. A user hovering a point reads both numbers side by side in the same tooltip and compares them as if they shared a unit; they are off by a factor of 12 or 730 depending on the selection.
- Fix: either convert both series through
convertFromMonthly, or hard-pin the cumulative axis to a single unit and label it explicitly (its axis title at line 497 is a bare "Cumulative Savings" with no unit). Same chart as LeanerCloud/cloud-commitments-cli#963, different defect.
- Where:
-
"Period Savings" KPI is a monthly rate rendered as a bare dollar total (MEDIUM)
- Where:
frontend/src/modules/savings-history.ts:293-321 - What:
monthlyTotalissummary.total_period_savings ?? totalSavings, where the fallbacktotalSavings(:273-287) is a flat sum of per-buckettotal_savings, each of which sumsestimated_savings, a monthly figure per this file's ownAPI_UNIT = 'monthly'at line 20. The value is then run throughconvertFromMonthly(monthlyTotal, unit)at line 298, which is only correct for a rate. It is rendered at line 305 asformatCurrency(displayTotal)with no unit suffix, while the two neighbouring tiles (lines 317, 320) both appendsuffix(/mo,/hr,/yr). The comment at lines 303-304 asserts the opposite of what line 298 does ("it is already a dollar total ... no per-unit rate suffix"). - Failure scenario: a user with commitments saving $1,200/month selects a 30-day period and the "Monthly" unit. The Period Savings tile reads "$1,200.00" with no suffix, next to "Avg Monthly Savings $1,200.00/mo", and is read as "$1,200 saved during this period". Switching the unit to "Yearly" makes the same tile read "$14,400.00", still unsuffixed and still labelled "Period Savings", which reads as $14,400 saved in a 30-day window. The sub-line "shown in yearly equivalents" at line 311 is the only disambiguation and sits below the number.
- Fix: decide whether the tile is a rate or a total and make code, label and suffix agree. If it is a rate, append
suffixlike its neighbours; if it is a total, drop theconvertFromMonthlycall at line 298. Also correct the comment, which currently documents the opposite of the code (seefeedback_snapshot_runrate_avgfor the run-rate vs total distinction).
- Where:
-
Clearing the ladder "Buffer Fraction" field silently removes the over-commitment safety buffer (MEDIUM)
- Where:
frontend/src/ladder.ts:437-443(saveLadderConfig), inputs atfrontend/src/ladder.ts:200-213 - What: each numeric field is read as
Number((el as HTMLInputElement | null)?.value ?? '<default>'). The??fires only when the element is missing; when the element exists and the user cleared it,.valueis'', which is not nullish, so the expression becomesNumber('')= 0. The inputs carrymin/maxbut norequired, andminis not enforced on an empty value, so the browser does not block submit. For five of the six fields the backend rejects an explicit 0 with a 400 (internal/api/handler_ladder.go:143-153,applyLadderConfigNumericDefaults), so the user gets an error.buffer_fractionis the exception: its documented valid range is[0, 1)and an explicit 0 is a legitimate "no buffer" choice, passed through untouched. - Failure scenario: an operator opens Admin -> Purchasing -> a per-account laddering config, selects the Buffer Fraction field (pre-filled
0.10), deletes the contents intending to retype, tabs away to change the cadence instead, and hits Save Config. The request carriesbuffer_fraction: 0, the backend accepts it as an intentional "no buffer", and the automated laddering engine for that cloud account now commits to its full computed target with zero headroom, raising the chance of an unused non-refundable commitment. Nothing in the UI signals the change. - Fix: read the raw string first and treat
''explicitly, either blocking submit with a targeted message or omitting the key soapplyLadderConfigNumericDefaultsrestores the documented default, instead of lettingNumber('')produce a meaningful 0.
- Where:
-
base64Encodeusesbtoa, so any non-Latin-1 password hard-fails login with a raw DOMException (MEDIUM)- Where:
frontend/src/api/client.ts:191-193, used byfrontend/src/api/auth.ts:55(login),:186(resetPassword),:286(setupAdmin),:317-318(changePassword),:343/:386(MFA setup/disable), andfrontend/src/api/users.ts:37(createUser) - What:
btoathrowsInvalidCharacterErrorfor any code point above U+00FF. There is no encoding step (for exampleTextEncoderto a binary string) before the call, and no guard indescribePasswordValidationError(frontend/src/auth.ts:300-313) that would stop a user choosing such a password in the first place. - Failure scenario: a password contains a curly apostrophe, a dash outside Latin-1, a CJK character or an emoji, all common in passphrases pasted from a password manager. Clicking Login throws inside
api.loginbefore any network call.handleLogin(frontend/src/auth.ts:715-731) catches it,mapServerLoginErrordoes not match, and the form displays a raw browser message such as "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range." The user cannot sign in, the message gives no actionable guidance, and the same throw blocks the reset-password and admin-setup paths, so there is no self-service recovery. - Fix: encode to UTF-8 bytes before base64 (
btoa(String.fromCharCode(...new TextEncoder().encode(str))), or an explicit helper), and confirm the Go side'sbase64.StdEncoding.DecodeStringconsumer treats the result as UTF-8. Add a regression test with a non-Latin-1 password on the login, reset and setup paths.
- Where:
-
Capacity % input coerces
0and non-numeric text to 100 and truncates fractions (LOW)- Where:
frontend/src/app.ts:398-401 - What:
Math.max(1, Math.min(100, parseInt(capacityInput.value, 10) || 100)).parseInt('0')is0, which is falsy, so|| 100fires before theMath.max(1, ...)clamp ever sees it:0becomes100, not1.parseInt('abc')isNaN, also100.parseInt('50.7')silently truncates to50.capacity_percentis not audit-only: it is folded into the server-side idempotency key (internal/api/handler_purchases.go:2514,purchaseIdempotencyKey(creatorID, execReq.Recommendations, execReq.CapacityPercent)) and cross-checked against the scaled rec counts byvalidateCapacityConsistency(internal/api/validation.go:661-679). - Failure scenario: a user types
0into the bulk-purchase Capacity field intending to buy nothing, then clicks Execute. The submittedcapacity_percentis100, the exact opposite of their intent, and that is what is recorded on the execution row and used as the idempotency key. Separately a fractional entry like50.5submits50while the recs may have been scaled at 50.5, trippingvalidateCapacityConsistencyinto an opaque 400. - Fix: parse strictly and reject empty / non-integer / out-of-range with a targeted message rather than coercing, matching
readServiceFilterControlsinfrontend/src/serviceFilters.ts:201-213, which already does this correctly. Seefeedback_strict_int_parse.
- Where:
-
Fan-out execute leaves the Execute button permanently disabled if any post-submit step throws (LOW)
- Where:
frontend/src/app.ts:495-656, contrastfrontend/src/app.ts:464-469 - What: the single-purchase path (
handleExecutePurchase) re-enables the button in afinallyblock. The fan-out path re-enables it only at the tail of the function (:652-655), with nofinally, afterformatPaymentAdjustmentNotice,showToast,openArcheraOfferModalandawait loadDashboard(). - Failure scenario:
loadDashboard()rejects (API 500 after the purchases were already submitted). The rejection propagates out ofhandleFanOutExecute, lines 652-655 never run, and the Execute button stays disabled reading "Sending 0/3..." until a page reload, while the purchases actually did submit. The user has no signal that they succeeded and may reasonably assume they need to retry. - Fix: move the re-enable into a
finally, as the sibling path already does.
- Where:
-
getVisibleRecommendations/getRecommendationsreturn shallow copies despite claiming otherwise (LOW)- Where:
frontend/src/state.ts:190-201andfrontend/src/state.ts:106-108 - What: the comment at lines 192-193 says "Defensive clone on read so callers can't mutate module state", but
return [...visibleRecommendations]copies only the array; every element is the sameRecommendationobject held in module state. Consumers cast the result to a local type and pass it around (frontend/src/recommendations.ts:3753,:3784,:2405). - Failure scenario: any consumer that assigns to a field on a returned element (an in-place
paymentorcountedit) mutates the module's canonical list, so a later render, or a purchase built fromstate, picks up the edit even after the user cancels. No live exploit was confirmed in scope, because the current purchase path spreads each rec (frontend/src/app.ts:387-393), but the comment actively invites the unsafe pattern. - Fix: either deep-clone (the codebase already has
deepCloneinfrontend/src/utils.ts:252-256, or usestructuredClone) or correct the comment to state that the elements are shared. Seefeedback_deep_copy_state.
- Where:
-
API path parameters are interpolated without encoding, inconsistently with sibling calls (LOW)
- Where:
frontend/src/api/purchases.ts:49,56,69,88,122,138,145,152,159,166,201,212;frontend/src/api/plans.ts:29,46,56,66;frontend/src/api/groups.ts:19,36,46;frontend/src/api/users.ts:24,49,59;frontend/src/api/apikeys.ts:29,36;frontend/src/api/accounts.ts:159,163,170,174,181,185,195,206 - What: IDs are dropped straight into the template literal. The same file family gets this right elsewhere:
frontend/src/api/recommendations.ts:138(/recommendations/${encodeURIComponent(id)}/detail),frontend/src/api/riexchange.ts:150,frontend/src/purchases-deeplink.ts:105.saveAccountServiceOverridealso interpolates the free-formserviceslug atfrontend/src/api/accounts.ts:195. - Failure scenario: any id containing
/,?,#or..re-routes the request to a different endpoint.createMarketplaceListing(purchaseId)takes an AWSReservedInstancesId(frontend/src/api/purchases.ts:191) rather than an internal UUID, so the value is provider-shaped rather than format-guaranteed by CUDly. The realistic outcome today is a 404 rather than a security break, but the inconsistency means the safe pattern is currently applied by accident rather than by rule. - Fix:
encodeURIComponentevery path segment acrossfrontend/src/api/*, and add a lint rule or a review check so the next endpoint added inherits it.
- Where:
-
fetchOverridesForAccountsswallows every error, so a broken API renders as "no overrides" (LOW)- Where:
frontend/src/lib/overrides.ts:14-20 - What: each per-account
listAccountServiceOverridescall is wrapped in a barecatch {}with the comment "Silent fallback -- callers handle missing entries gracefully". The returned Map simply omits the failed account, which is indistinguishable from "this account has no overrides configured". - Failure scenario: the overrides endpoint 500s, or 403s for a non-admin. The Settings service-defaults UI seeds every affected account from the global default and shows no error. An operator who had previously scoped an account to
include_regions: [us-east-1]sees the account presented as unrestricted, and saving from that screen can persist the wider default over their narrower override. - Fix: return per-account outcomes (value or error) rather than a bare Map, and let the caller render a partial-failure state. See
feedback_no_silent_fallbacks.
- Where:
-
getCostPeriodreturns the static default on corrupt storage without repairing memory state (LOW)- Where:
frontend/src/state.ts:217-236 - What: when
localStorageholds a value outsideVALID_PERIODS, the function returns the literal'monthly'(line 231) but leaves bothcostPeriodMemoryand the corrupt localStorage entry untouched, and notifies nothing. Every subsequent call re-reads and re-discards the same bad value. - Failure scenario: a stale or hand-edited
cudly.recs.costPeriodentry, for example left over from a renamed period value, permanently pins the cost-period selector to Monthly. The user picks "Yearly",setCostPeriodwrites it and the selector works, but any code path that reads before that write still gets Monthly, and the mismatch never surfaces. - Fix: overwrite the corrupt key with the default on read so the state self-heals and subsequent reads are consistent.
- Where:
-
MFA step retains the plaintext password in module scope after an abandoned login (LOW)
- Where:
frontend/src/auth.ts:675-676, cleared at:678-681,:712,:765,:795 - What:
pendingMFAPasswordholds the plaintext password between the first login POST and the MFA code submit. It is cleared on success and on the explicit "Back to login" link, but not on a failed MFA submit that is neitherinvalid_mfa_codenormfa_required(frontend/src/auth.ts:810-811falls through without clearing), and not if the user simply walks away from the MFA prompt. - Failure scenario: the password is correct, the server prompts for the TOTP code, and the user leaves the tab open without finishing. The plaintext password stays reachable in the module closure for the lifetime of the page, recoverable from a heap snapshot or by any script that later achieves execution on the origin, long after the login attempt is over. Amplified by LeanerCloud/cloud-commitments-cli#1150 (auth token and API key in localStorage).
- Fix: clear on any terminal error and on a timeout, and prefer holding a server-issued short-lived MFA continuation token over the raw password.
- Where:
-
initSavingsHistoryde-dupes only one of its four listener registrations (LOW)- Where:
frontend/src/modules/savings-history.ts:560-597 - What: the unit
<select>handler is explicitly de-duped via a stored_unitChangeHandlerproperty (lines 576-584, citingfeedback_event_listener_dedup), but the period<select>(line 566), the refresh button (line 570) and the twostate.subscribe*registrations (lines 595-596, whose unsubscribe handles are discarded) have no such protection. - Failure scenario:
initSavingsHistory()is currently reached once per page load fromfrontend/src/app.ts:187, so no live duplication was confirmed. If it is ever wired into a per-tab-entry path, exactly asinitRegistrationsalready is (first MEDIUM in this list), a single period change would fire N parallelloadSavingsHistory()calls with no generation guard between them. - Fix: apply the same stored-reference dedupe, or the
dataset['wired']marker, to all four, and keep the unsubscribe handles so the subscriptions can be torn down.
- Where:
-
renderConfigTablecrashes the whole ladder table iftarget_coverageis absent (LOW)- Where:
frontend/src/ladder.ts:261 - What:
${cfg.target_coverage.toFixed(1)}%has no null guard, unlike every other cell in the same row (escapeHtml(...),cfg.updated_at ? ... : 'N/A').LadderConfig.target_coverageis typed non-nullable infrontend/src/api/ladder.ts:42, but the field is not runtime-validated anywhere on the response path (getLadderConfigsatfrontend/src/api/ladder.ts:59-62just unwraps the envelope), in contrast to the explicit shape validation infrontend/src/api/auth.ts:133-158. - Failure scenario: a ladder row written before the column existed, or by a partially-applied migration, comes back with
target_coverage: null..toFixedthrows a TypeError insideconfigs.map, the wholerenderConfigTablecall aborts, and the per-account configuration table renders as nothing at all, with no error message unless the caller happens to have atry/catch. - Fix: guard with the same
!= nullpattern used forupdated_aton the adjacent line, rendering'N/A'for an absent value. Consider runtime-validating thegetLadderConfigsenvelope the wayapi/auth.tsvalidates its responses.
- Where:
Findings from the 2026-09-02 codebase audit
Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.
A11-016 (medium)
The same swallow-renders-as-empty shape as the fetchOverridesForAccounts bullet, on an authorization-scoping path. openDuplicateGroupModal catches every listAccounts failure into accounts = [] (frontend/src/groups/groupModals.ts:620-626), and the empty branch of renderDuplicateAccountsList then prints 'No cloud accounts configured yet. Duplicating without scope clones the full source group - add accounts first if you want to restrict.' unconditionally (:562-568). On a 403 or a transient 5xx the operator is told a factual untruth about their deployment and is nudged to create the duplicate with no account scoping, which inherits the source group's allowed_accounts unchanged (:646-650) - the widest available outcome. The modal still opens with the submit path enabled (:630). Rendering an error line and disabling submit on a fetch failure, keeping the 'none configured' copy for a genuinely empty list, is the fix. Audit finding A11-016.
A11-024 (low)
A second defect on the setupAdmin path this issue already names at auth.ts:286. Its non-OK branch awaits a bare response.json() (frontend/src/api/auth.ts:295-298), so a 502 from a proxy or any non-JSON error body throws a SyntaxError that propagates to handleAdminSetupSubmit and renders something like "Unexpected token < in JSON at position 0" in the setup form instead of a usable message. The other three handlers in the same file all guard it: login at :72, requestPasswordReset at :175 and resetPassword at :202 each use .catch(() => ({})). One-line fix, and it lands on the same admin-setup path with no self-service recovery that the base64Encode item calls out. Finding A11-024.
A12-064 (low)
The same coerce-rather-than-reject shape as the Capacity % bullet, on the RI-exchange quote path. collectTargets silently turns an empty or sub-1 Count into 1 (frontend/src/riexchange.ts:1756-1758), four lines after the offering-id branch returns an explicit error for the analogous empty case (:1747-1749). updateRunningTotal repeats the coercion (:1679-1681), so the displayed total agrees with the substitution and nothing signals that the typed value was discarded. A user who clears the Count field intending to retype it, or types 0, gets a quote for a single unit instead of an inline validation error. Returning the same style of error the offering-id check returns is the fix. Audit finding A12-064.
A12-072 (low)
One dead branch in archera.ts, which is in this pass's scope. ArcheraContext declares 'purchase' | 'plan' (archera.ts:47) and openArcheraOfferModal branches on it for the modal headline (:114), but both production call sites pass 'purchase' (app.ts:456, :645), so the "Insure this plan with Archera?" headline is unreachable and frontend/src/__tests__/archera.test.ts:228 asserts a string no user can see. Either wire the plan-creation surface to call openArcheraOfferModal('plan') or drop the parameter and the branch; as it stands the test reads as coverage of a live path. (audit finding A12-072)
Activity
style(frontend): gap the auth-form submit button below the error box
IAmJSD commentedon Oct 24, 2026
Claiming: working on this
fix(frontend): sign-in, ladder save and registrations review fixes (#…
Merge remote-tracking branch 'origin/main' into ui/issue-742
feat(auth): issue a single-use MFA login token instead of resending t…
IAmJSD commentedon Oct 28, 2026
Status update (2026-10-28): the backend half of the MFA item merged in #1571: login for an MFA user returns a short-lived, single-use
mfa_token, and the MFA submit accepts{email, mfa_token, mfa_code}without the password (the old form still works). A small follow-up tightening the token checks is in progress.Left: everything else on this issue is frontend (UI owner), including switching the login form to the token so the browser no longer keeps the password.
fix(auth): make every MFA token mismatch return the same response (#1574
IAmJSD commentedon Oct 28, 2026
Status update (2026-10-28): #1574 merged: every MFA-token mismatch (unknown/expired/used token, wrong email, inactive or locked user) now returns the same
invalid_mfa_tokenresponse, and the user is resolved from the token's bound user id.Left: the frontend half (UI owner): switch the login form to the token so the browser no longer keeps the password, plus the other frontend items on this issue.