Repository navigation
Prayer spaces: type, coordinates, and admin CRUD - #16
Merged
Merged
Conversation
Prayer spaces were readable and nothing else. Two GET endpoints returned the JPA
entity directly, there was no service layer, and no way to create, edit or delete
one — a space could only be changed by editing the database by hand, which the
admin console's own page comment called out as the largest gap between what it can
manage and what Minbar runs.
Model
- PrayerSpaceType {BROTHERS, SISTERS, MULTIFAITH, REFLECTION}. Audience was doing
double duty as both "who is this listed for" and "what kind of room is it",
which cannot describe the campus: a multifaith room and a reflection bay are
both shown to everyone and are not the same place. Type carries a default
audience so the common case still only states one of them.
- latitude/longitude, nullable and written as a pair. Without them the app cannot
pin a space, sort by distance, or offer a Maps link — the reason the map screen
was still reading mock data.
- imageUrl, notes, and a prose `directions` fallback for a space nobody has
written step-by-step directions for yet.
- `location` becomes `roomInfo`; it described where in the building the room is,
which stops reading as "location" once the table also holds coordinates.
Wire
PrayerSpaceView replaces the entity on both endpoints. `steps` is a lazy
@onetomany, so whether a caller got the walking directions depended on whether a
transaction happened to be open — which is why the console fetched each space
again one at a time. The view is assembled inside the service, so the list carries
full directions and that fan-out goes away.
Directions stay stored as their parts (walkTimeMinutes + startingPoint, not "2 min
walk from the CCT main entrance"). Clients compose the copy; the app and the board
word it differently and the server should not pick a side.
Admin
/api/admin/minbar/prayer-spaces, GET/POST/PATCH/DELETE. Reads on
board:content:read — where the musallahs are is not a secret, and /api/minbar
serves the same data unauthenticated. Writes on a new board:prayerspace:write,
bundled into BOARD_EDITOR and BOARD_ADMIN: everything else in board content is
displayed on a wall, where wrong means stale; these are turn-by-turn instructions
someone follows through a building to find somewhere to pray, where wrong means
the wrong floor. Every write is audited.
Migration V8, both dialects. Existing rows backfill space_type from audience.
613 tests pass, including 19 service tests and 18 covering the read/write
permission split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things found by exercising the endpoints against a real database rather than a mock. **A pin could be set but never removed.** For the optional text fields, sending `""` clears them; there was no equivalent for a number, because null already means "leave unchanged". So a space pinned to the wrong building could be corrected and never un-pinned, and the only way back was to delete it and type it in again. `clearCoordinates: true` on the patch request nulls both halves. Sending it alongside a latitude or longitude is a 400 — that asks for two contradictory things in one call. **None of the prayer-space writes were being audited.** V8 added the three `*_PRAYER_SPACE` actions to `AuditAction` and wired them into the controller, but did not widen `audit_events`' CHECK constraint, and `ddl-auto=validate` does not inspect CHECK constraints. Every audit insert failed, and `AdminAuditService` catches and logs rather than rethrowing — correctly, since a failed audit write must not roll back the operation it describes. The result was the worst of both: the edits went through and none of them were recorded. Only visible by reading the table. `LINK_TICKET_EVENT` and `UNLINK_TICKET_EVENT` turn out to have had the same bug since V5. They are fixed here too. V9 widens both CHECK constraints in both dialects and adds a `PRAYER_SPACE` entity type, so these stop being filed under `EVENT` alongside calendar entries. `MigrationEnumParityTest` is the part that matters: 178 cases asserting every constant of every `@Enumerated(STRING)` enum is named somewhere in each dialect's migrations. It is deliberately crude — it cannot prove a value landed in the right constraint — but whole-cloth omission is the mistake that keeps happening, and it is now a build failure with a message that says what to do. Verified it fails on the real defect before V9 fixes it. 794 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prayer spaces were readable and nothing else. Two
GETs returned the JPA entity directly, there was no service layer, and no way to create, edit or delete one — a space could only be changed by editing the database by hand. The admin console's own page comment called that the largest gap between what it can manage and what Minbar actually runs.Model
PrayerSpaceType{BROTHERS, SISTERS, MULTIFAITH, REFLECTION}.audiencewas doing double duty as both who is this listed for and what kind of room is it, which cannot describe the campus: a multifaith room and a reflection bay are both shown to everyone and are not the same place. Type carries adefaultAudience(), so the common case still only states one of the two, and the unusual one — a bay a building has designated sisters-only — stays representable.latitude/longitude, nullable and written as a pair. Without them the app cannot pin a space, sort by distance, or offer a Maps link, which is why the map screen was still reading mock data. Nullable because a reflection bay inside a building has no useful pin, and a space can be listed before anyone has stood in it with a phone. One coordinate without the other is rejected — that is not a partial location.Also
imageUrl,notes, and a prosedirectionsfallback for a space nobody has written step-by-step directions for yet.locationbecomesroomInfo; it described where in the building the room is, which stops reading as "location" once the table also holds coordinates.Wire
PrayerSpaceViewreplaces the entity on both endpoints.stepsis a lazy@OneToMany, so whether a caller got the walking directions depended on whether a transaction happened to be open — which is why the console fetched each space again, one at a time. The view is assembled inside the service, so the list carries full directions and that fan-out disappears.Directions stay stored as their parts (
walkTimeMinutes+startingPoint, not "2 min walk from the CCT main entrance"). Clients compose the copy; the app and the board word it differently and the server should not pick a side.Admin
/api/admin/minbar/prayer-spaces—GET/POST/PATCH/DELETE.Reads on
board:content:read: where the musallahs are is not a secret, and/api/minbarserves the same data unauthenticated. What this adds is the unfiltered list, since a console editing the sisters' musallah has to load it whoever is signed in.Writes on a new
board:prayerspace:write, bundled intoBOARD_EDITORandBOARD_ADMIN. Everything else under board content is displayed on a wall, where wrong means stale. These are turn-by-turn instructions somebody follows through a building to find somewhere to pray, where wrong means the wrong floor.PATCHsemantics: null leaves a field alone,""clears an optional text field, a list replaces wholesale, andclearCoordinates: trueremoves a pin — there is no empty-string trick for a number, so without it a space pinned to the wrong building could be corrected but never un-pinned.The bug this turned up
V8 added the three
*_PRAYER_SPACEaudit actions but did not widenaudit_events' CHECK constraint, andddl-auto=validatedoes not inspect CHECK constraints. Every audit insert failed, andAdminAuditServicecatches and logs rather than rethrowing — correctly, since a failed audit write must not roll back the operation it describes. So the edits went through and none were recorded. Only visible by reading the table.LINK_TICKET_EVENTandUNLINK_TICKET_EVENTturn out to have had the same bug since V5.V9 fixes both dialects and adds a
PRAYER_SPACEentity type.MigrationEnumParityTestis the part that matters: 178 cases asserting every constant of every@Enumerated(STRING)enum is named somewhere in each dialect's migrations. Crude on purpose — it cannot prove a value landed in the right constraint — but whole-cloth omission is the mistake that keeps happening, and it is now a build failure with a message that says what to do. Confirmed it fails on the real defect before V9 fixes it.Testing
794 tests pass, including 22 service tests and 18 covering the read/write permission split.
Exercised against a live SQLite dev database with six seeded spaces: audience filtering,
type→audiencederivation, patch and clear semantics, the half-pin rejection, unauthenticated writes refused, and audit rows landing with the right actor and target. Both frontends were driven in Chrome against it.Migrations
V8 and V9, both dialects. Existing rows backfill
space_typefromaudience.🤖 Generated with Claude Code