Skip to content

fix: sweep the eight decided, ready bugs - #148

Merged
asm0dey merged 21 commits into
mainfrom
fix/bug-sweep
Aug 22, 2026
Merged

fix: sweep the eight decided, ready bugs#148
asm0dey merged 21 commits into
mainfrom
fix/bug-sweep

Conversation

@asm0dey

@asm0dey asm0dey commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Closes eight open bugs that were already triaged and decided, one commit (or one commit plus a follow-up) each. No schema change, no new dependency, no migration.

mvn test is green on the branch tip: 921 tests, 0 failures, 0 errors, BUILD SUCCESS (up from an 898-test baseline). mvn spotless:check passes. i18n parity verified mechanically — AdminMessages has 237 keys and both adm_de.properties and adm_he.properties have exactly those 237, zero missing, zero extra.

What's fixed

Bean Fix
calit-4whp An unvalidated timezone could be stored, then 500 the owner's public booking page and the booking transaction at eleven unguarded ZoneId.of(...) sites. Coerced to UTC on save — on the entity, so both write paths are covered.
calit-ek26 cancelSingle deleted the remote Google event but left all four event-reference fields on the row. Now clears them, matching the group path, including when the account is disconnected.
calit-75vf "1 upcoming bookings". Reworded count-last in en/de/he. Two sibling messages whose English (s) hid the identical bug in German and Hebrew were reworded too.
calit-jk8y Both write-calendar save paths claimed bookings "stay behind" even on a save that changed nothing. Now compares the pre-save calendar ref against the post-save one.
calit-w7gq Saving a Meet type whose calendar can't mint Meet links threw an unmapped BadRequestException → blank, unlocalized 400, form lost. Now the localized IllegalStateException both handlers already catch.
calit-h8mb A disabled account kept a live, bookable public page. Now 404s — on the HTML routes and the public JSON API.
calit-a4yj /signup created a user with no owner_settings row while every other path seeded one. Extracted OwnerSettings.seed(...); all five creation paths route through it.
calit-mhgs /me pages disagreed on timezone — two used the owner's stored zone, one the browser-detected one. All now use the stored zone; the picker became an explicit override. Invitee pages still auto-detect.

Two findings from the whole-branch review

Both were cases where a fix had landed at the bug's reported call site rather than at the invariant, leaving a second reachable path open. Both are fixed here.

  • POST /api/bookings still booked a disabled owner. It resolves the username itself and never goes through PublicResource.resolveOwner, and it is a public unauthenticated endpoint. MeetingHosts.bookable returns true unconditionally for single-host types, so nothing downstream caught it. Reproduced as a real 201 Created before fixing.
  • The first-login wizard still stored an unvalidated timezone. MeSetupResource is a second writer of the same column with the same blast radius. The guard now lives on OwnerSettings and both writers call it; two duplicate private zoneIds() helpers were collapsed into one.

Review notes

  • German and Hebrew strings on this branch are agent-authored and would benefit from a native pass. They were vetted twice for terminology and placeholder parity ("Termintyp", not "Terminart"; formal Sie throughout), but idiom is not signed off. Two pre-existing Hebrew inconsistencies at adm_he.properties:143/146 were deliberately left alone rather than changed without a native reader.
  • docs/drop-flyway-version-range is folded into this PR, deliberately. That branch was this one's fork point, it is fully contained here (git log fix/bug-sweep..docs/drop-flyway-version-range is empty), and its two commits — 83de3dc (drop the Flyway version range from CLAUDE.md) and 4f3e052 (file the jbcontext trial and the create-form workplan) — land with this PR. It needs no separate PR; the branch can be deleted once this merges.
  • 0f6f3d0 is unrelated tooling — the jbcontext trial wiring (MCP server, index hooks, context-explorer agent, generated CLAUDE.md block). Committed separately so the sweep's history stays reviewable on its own. No secrets.
  • Not retrofitted: bookings taken on a disabled owner's page before this upgrade are not cancelled.

Two more, folded in after review

Both were first filed as follow-ups and then done here instead — this is a bug sweep, so they belong in it.

  • calit-jyck — token links could still write to a departed host's calendar. The invitee routes are keyed by an unguessable manage token, not by username, so calit-h8mb's guard never saw them. reschedule and edit-details now refuse; cancel deliberately does not — an invitee must always be able to get out of a meeting. The refusal is not a 404 (the token is legitimate and the booking exists, so 404 would be a lie): the hub swaps both write forms for a localized notice and keeps cancel live, with the same guard server-side for stale tabs. Mutation-tested — neutering the guard fails exactly the three refusal assertions and leaves cancel and the enabled-host control green.
  • calit-sv6a — a settings-less owner silently ate every mail. EmailService.read did ZoneId.of(owner.timezone) off a possibly-null owner. It backs every mail path, but bit hardest on reminders: ReminderScheduler's deliberate per-booking catch (which exists so one poison booking can't roll back the other 49) marked the reminder sent and dropped it, so only an ERROR line recorded that anything happened. That catch is correct and untouched; the unguarded read upstream was the defect. Also swaps two bare ZoneId.of reads for OwnerSettings.coerceZone, so a row written before calit-4whp's guard existed degrades instead of throwing.

Follow-ups still open

None from this branch.

Upgrade note

Nothing to do — no configuration and no database changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug

asm0dey and others added 21 commits August 22, 2026 14:16
Plan covers the eight decided, ready bugs: calit-4whp, ek26, 75vf, jk8y,
w7gq, h8mb, a4yj, mhgs. Excludes o69e (its own plan), mjof (needs a product
answer) and 64hy (already answered).
A crafted POST could store a zone id that ZoneId.of rejects, 500ing the
owner's public booking page and every booking on it. Guard on save,
mirroring the locale guard one line below.
…ncelled

cancelSingle deleted the remote event but left googleEventId, meetLink and
the calendar address on the row, so a cancelled booking kept pointing at an
event that no longer exists. Mirror deleteGroupGoogleEvent, including its
rule that the refs are cleared even when the remote call is skipped.
The notice said '1 upcoming bookings stay on the calendar they were created
on'. There is no pluralization mechanism in the codebase, so reword the
message in all three locales to put the count last, where no noun agrees
with it.

Grepped src/main/java/site/asm0dey/calit/i18n/ for '{count}|(int count)|(long
count)' and found two other counted messages beyond the expected
adm_hosts_error_cap: adm_shared_revokeConfirm_count and
adm_hosts_removeConfirm_count, both phrased "{count} upcoming booking(s)".
The parenthetical (s) already makes those read correctly at any count, so no
further reword was needed. No message was found where a noun disagrees with
the count the way "1 upcoming bookings" did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
…m strings

Commit 72df9e3 judged adm_shared_revokeConfirm_count and
adm_hosts_removeConfirm_count as "already count-agnostic" on the strength of
the English "{count} ... booking(s)" suffix, without checking the German and
Hebrew .properties values. That claim was wrong: de read "1 bevorstehende
Termine" (wrong noun number and adjective ending) and he read "1 פגישות
עתידיות" (plural noun against the numeral 1) -- the identical bug class
calit-75vf exists to fix.

Reword both keys in all three locales (English included, so the whole
message bundle uses one count-last convention instead of splitting it
between the "(s)" trick and the count-last shape). adm_hosts_error_cap is
untouched -- its {max} is a limit, not a subject noun, so the original
judgment there stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
Both write-calendar save paths counted bookings on a different calendar and
showed the notice whenever that count was non-zero -- including on a save
that re-submitted the calendar the type was already on. Compare the pre-save
ref against the post-save one and stay quiet when they match.
Saving a type as Google Meet while its write calendar can't mint Meet links
threw BadRequestException, which no handler caught and no mapper covered --
the Host got a blank, unlocalized 400. Throw the IllegalStateException both
save paths already catch, with a message in en, de and he.

Corrected the brief's German string: "Termintyp" instead of "Terminart" --
the codebase's established term (22 existing occurrences) and the correct
grammatical gender for "dieser Termintyp".
resolveOwner resolved /{username} with no enabled check, so an account an
admin had switched off kept a live, bookable page -- and a disabled owner
who had working hours before being switched off was bookable right now,
mailing someone who has left. Guard the one method both the landing and the
booking routes go through.
/signup persisted the AppUser and stopped, while the other three creation
paths each seeded the NOT NULL settings row the public booking path reads
unguarded (issue #99). Extract OwnerSettings.seed and route all four through
it, so the reason lives once instead of in three near-identical copies.
Not a bug fix — OidcSignInService.provision already seeded owner_settings
correctly. It was a fifth account-creation path missed by the task-7 brief
(5250cb1), which named only four and consolidated onto OwnerSettings.seed.
Leaving this one hand-built defeated the point: the NOT NULL reason and its
null-email handling would still live in a fourth copy instead of once on the
helper's javadoc. Route it through OwnerSettings.seed too, dropping the now-
redundant null-email ternary.
The manage page pre-selected the browser-detected zone in its picker while
the dashboard and /me/pending used the stored one, so a travelling host read
the same booking as 15:00 and 22:00 one click apart. The picker now defaults
to the zone the page was authored in -- stored on /me, detected on the
invitee pages, which emit no data-tz -- and both /me list pages name the zone
on screen.
Commits the working-tree state of the jbcontext 2-week trial (calit-7a6t)
so the trial is reproducible rather than living only on one machine:

- .mcp.json: register the jbcontext stdio MCP server.
- .claude/settings.json: reindex on SessionStart and SessionEnd, both async
  so neither blocks the session.
- .claude/agents/context-explorer.md: the read-only research subagent the
  CLAUDE.md instructions point at, budgeted to 3 searches and 3 reads.
- CLAUDE.md: the generated instruction block, inside the tool's own
  <!-- jbcontext-instructions-start/end --> markers so a later
  regeneration replaces it cleanly.

No secrets or tokens in any of these. `.claude/agents/` is not gitignored
(only `.claude/skills/` is, per the skills-symlink note in CLAUDE.md).

Unrelated to the eight bug fixes on this branch; committed separately so
the sweep's history stays reviewable on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
calit-h8mb closed the disabled-owner hole at the bug's reported call site,
PublicResource.resolveOwner, rather than at the invariant. POST /api/bookings
is a second, public and unauthenticated way in: BookingResource.create
resolves the username itself and never touches resolveOwner, so the guard
never applied to it.

Nothing downstream caught it either. The bean's "the co-host path is already
covered" note holds only for multi-host types -- MeetingHosts.bookable
returns true unconditionally for a single-host type, which is exactly what a
departed owner has. A plain curl still booked the disabled owner, wrote to
their calendar and mailed them: the new test returned 201 Created before this
change.

Apply the same owner == null || !owner.enabled guard at that call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
calit-4whp closed the invalid-timezone hole at the bug's reported call site,
AdminResource.updateSettings, and left the guard as a private zoneIds()
helper local to that resource. MeSetupResource:103 -- the first-login wizard,
which every user passes through -- still did a bare s.timezone = timezone.

Same column, same eleven unguarded ZoneId.of(settings.timezone) readers, same
blast radius: the owner's PUBLIC booking page and the booking transaction
both 500 on a value the JDK cannot parse. The new wizard test stored
"Not/AZone" before this change.

Move the invariant onto OwnerSettings, mirroring the OwnerSettings.seed
extraction this branch already made: zoneIds() as the single source for both
pickers, and coerceZone(String) whose javadoc says every writer of the column
must call it. Route both writers through it, delete the two duplicate private
zoneIds() helpers and repoint all four picker call sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
Seven smaller findings from the last-gate review, none of which change
behaviour except one German string:

- Nothing posted locationType and writeCalendar together, so the two tasks
  that share editMeetingType's transaction were never exercised in conflict.
  Add a test that submits both and pins that the calendar move rolls back
  with the refused save.
- The two count keys reworded in d8b9646 had no coverage: the interstitial
  tests asserted only containsString("1"), which survived the rewording.
  Assert the wording in full.
- TZ_SCRIPT's render() repeated the `initial` fallback expression verbatim.
  Collapse it to `initial`. The inverted-ternary protection moves to
  thePickerDefaultsToTheZoneThePageWasAuthoredIn, which pins `initial`'s
  definition character-for-character; verified by mutation that inverting it
  still fails a test.
- That test named picker behaviour but ran against /me, which has no picker.
  Point it at /me/bookings/{id}/manage, which renders one, and assert so.
- Layout.tzBar's javadoc still claimed the detected zone is selected. Reword
  to the actual body[data-tz]-first behaviour.
- German adm_times_shown_in was a headline fragment; "Alle Zeiten in {zone}"
  reads as native UI German. English and Hebrew unchanged.
- PublicDisabledOwnerTest's form POST sent name/email; submitBooking declares
  inviteeName/inviteeEmail. Use the real contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
- Token-keyed reschedule still reaches a disabled owner's calendar. Found
  by the whole-branch review and deliberately scoped out of fix/bug-sweep:
  calit-h8mb closed the stranger-facing routes, but a manage token bypasses
  both guards. Needs a rendered page rather than a 404 — the invitee holds a
  legitimate token.
- ReminderScheduler NPEs and silently drops mail when an owner has no
  settings row. Pre-existing (that scheduler is untouched by this branch);
  surfaced while verifying it. calit-a4yj closed the creation side, so this
  is the unguarded READ that remains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
…stale

SonarCloud flagged two issues on PR #148, both the same root cause:
java:S1854 (useless assignment) and java:S1481 (unused local) on the
`MeetingType t` at CohostManageTest:283. The seeding call is needed for its
side effect but its return value never was — drop the assignment, keep the
call.

Also refreshes AdminTimeRenderingTest's class javadoc, which still claimed
"/me pages have no #tz-picker". True of the dashboard and /me/pending, but
no longer of the whole class: calit-mhgs pointed the picker-default test at
/me/bookings/{id}/manage, which does have one. The javadoc now describes
both shapes and why each matters.

No production change; 921 tests green, spotless clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
EmailService.read did ZoneId.of(owner.timezone) straight off a possibly-null
owner, so a booking whose owner has no owner_settings row threw NPE. read()
backs EVERY mail path, not just reminders — but the reminder path is where it
bit hardest: ReminderScheduler's deliberate per-booking catch (which exists so
one poison booking can't roll back the other 49) marked the reminder sent and
dropped the mail, so the invitee silently never got it and only an ERROR line
recorded that anything happened. That catch is correct and is left alone; the
defect was the unguarded read upstream of it (calit-sv6a).

Now returns null for a missing settings row — the same "nothing to send" path
every caller already takes for a missing booking — and warns, because after
OwnerSettings.seed covers all five creation paths this should be unreachable.

Also swaps the bare ZoneId.of for OwnerSettings.coerceZone: a row written
before calit-4whp's save-time guard existed can still hold an unparseable
zone, and a DateTimeException here would take out every mail for that owner
rather than just one. The reminder now goes out coerced to UTC instead of
throwing — pinned by test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
The invitee token routes are keyed by an unguessable manage token, not by
username, so calit-h8mb's resolveOwner guard never sees them. An invitee
holding a link from before their host was disabled could still POST a new
time onto that departed host's calendar and trigger a fresh notification to
someone who has left (calit-jyck).

Reschedule and edit-details now refuse. Cancel deliberately does NOT: an
invitee must always be able to get out of a meeting, and cancelling a
departed host's booking is exactly what you want to happen.

The refusal is not a 404 — the token is legitimate and the invitee knows the
booking exists, so 404 would be a lie. The manage hub instead swaps both
write forms for a localized notice (en/de/he) and keeps the cancel button
live. The two POST handlers carry the same guard server-side, so a stale tab
or crafted POST re-renders that hub rather than writing.

Scoped to booking.ownerId, the row the invitee is managing, rather than the
type's creator: on a group booking that is whose calendar carries the event.

Also swaps renderManage's bare ZoneId.of for OwnerSettings.coerceZone — the
same pre-guard-era unparseable-zone exposure calit-4whp was about, on a page
that a 500 would make unusable.

Guard mutation-tested: neutering hostInactive() fails exactly the three
tests that assert refusal, while cancel and the enabled-host control stay
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
Both were filed as follow-ups from the whole-branch review and are now done
here instead — this is a bug sweep, so they belong in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
@sonarqubecloud

Copy link
Copy Markdown

@asm0dey
asm0dey merged commit 1142f89 into main Aug 22, 2026
11 checks passed
@asm0dey
asm0dey deleted the fix/bug-sweep branch August 22, 2026 15:44
asm0dey added a commit that referenced this pull request Aug 22, 2026
Six user-facing entries from PR #148, plus the upgrade note. The one caveat
worth surfacing: bookings taken on a disabled account's page before the
upgrade are not cancelled retroactively.

Written at merge, per the project's rule that changelog entries land when a
change reaches main rather than at release time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
@asm0dey asm0dey mentioned this pull request Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant