v1.2: Kam na mši — photos, confession, hero, intro, zoom; reviews gated off - #28
Merged
Merged
Conversation
Research-backed plan on feature/mass-reviews (private prototype). Core decision: witness + descriptive tags, NOT reviews/ratings — every serious precedent (ČBK's own app, messes.info, AA Meeting Guide, therapy directories) converged on describe-don't-evaluate, and Canon 220 makes grading a named priest/homily a real risk. No stars anywhere near a Mass. Reuses flywheel-core Supabase + nightly bake into static shards (/bin/zsh, offline). Phased v1-v3; four open forks for Pavol. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
Refocus the schema on positive-only 'quality' chips (hluboký prožitek, vstřícné k dětem locked) — no opposite, no scale — with logistical facts (language) as a slim background. Documents why positive-only prevents evaluation, and the one residual (comparison-by-absence) with mitigations.
Positive-only witness feedback for masses (docs/PILGRIM-WITNESS-PLAN.md),
persisted to localStorage behind a clean submitFeedback() seam so the whole
UX is testable locally. No Supabase, no Edge Function, no notifications yet.
- domain/feedback.ts: locked 7-chip witness vocabulary + factual `jazyk`,
stable massKey (weekday+time slot; ISO date for one-off extras).
- lib/feedbackLedger.ts: expected-attendance ledger; dueCards() eligibility
(≥60 min past, unanswered, never-ask off), markAnswered, neverAsk.
- lib/feedbackStore.ts: submitFeedback upsert (one row per device per mass),
aggregateFor with CORROBORATION_MIN=1 for local toying (prod 3), suggestTag.
- AfterMassCard.tsx: missal dress, no stars/emoji/shadows, ≥44px targets;
"Ano" is the witness, then optional chips + language + suggest-a-tag.
- App.tsx: shows the card on open (dueCards) and force-shows a demo mass via
?feedback=preview; ChurchDetail + native reminder seed the ledger.
- ChurchDetail: plain ordo witness line ("často zmiňují…"/"potvrdilo N
poutníků") under a mass once the aggregate clears the threshold.
- i18n: reverent CZ + EN card copy (de-slopped; banned rating words avoided).
Unit: 263 passing. e2e: persona journey opens ?feedback=preview → Ano →
chips → save → detail shows the corroborated line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…tness card TestFlight has no URL bar for ?feedback=preview, so a build-time flag (VITE_WITNESS_PREVIEW=1, prototype builds only) force-shows the demo card, with a 'Zobrazit náhled znovu' link to retry the flow on device. Never set in production.
…tier chips + map display Task 1 — drop the witness-card language single-select entirely (LANG_OPTIONS/ LangOption, the lang field on MassFeedback/StoredSubmission/submitFeedback, the langOptionLabel helper + fb_lang_* keys). Service-language filter untouched. Task 2 — redesign the chips as calm pills: px-3.5 py-2, rounded-full, hairline border, season-accent selected state (border + 10% tint), container gap-2.5. ≥44px touch, no shadows, no emoji. Task 3 — real Supabase via src/lib/supabase.ts. submitFeedback upserts one row (onConflict church_id,mass_key,device_id) with device_id = flywheel visitor_id, keeps a localStorage mirror for dedup/offline. New async loadAggregates() selects visible rows and fills an in-memory cache; aggregateFor() reads it synchronously. supabase null → localStorage-only (never crashes offline). Refresh after submit. Task 4/5 — detail page and MAP prefetch loadAggregates and render the reverent "často zmiňují … / potvrdilo N poutníků" line (popover on the map, under the mass info). Subtle season-accent dot under the time-chip marks corroborated masses. Task 6 — unit tests for aggregation + threshold with the supabase client stubbed; localStorage-path tests updated; jazyk tests removed; pilgrim-witness e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
Direct anon INSERT/UPDATE on mass_feedback revoked; all writes go through this Deno Edge Function: validates the chip vocabulary (drops unknown tags), rate-limits per device (40/h), verifies a Cloudflare Turnstile token on browser writes (dormant until TURNSTILE_SECRET + a widget exist; native sends none), and upserts with the service role. Reads stay anon SELECT. Schema gained weekday/mass_time/rite/lang/ mass_date for the directness tiers. Deployed to project chgwirzbpspoaoposuwg. Verified: fn returns ok + filters bad chips; direct anon insert now 401.
… witness filter
Client write path now goes through the submit-feedback Edge Function (direct
anon table writes are revoked); the localStorage mirror stays for offline/dedup.
- massKey encodes rite+lang: slotKey = church|w{wd}|{time}|{rite}|{lang}, so a
Latin Mass and the ordinary form at the same hour stay separate. New riteOf()
(greek→byz, latin→lat, else ord) and occurrenceOf() for the write-path fields
(weekday, mass_time, rite, lang, mass_date), threaded through the card/ledger.
- Two directness tiers: loadAggregates folds rows into slot (per mass_key) and
church (all masses) aggregates; aggregateFor returns { slots, church }. Chips
are ordered by frequency (most-mentioned first).
- Graded display (detail + map popover): slot testimony speaks directly
("u této mše často zmiňují … · potvrdilo N poutníků"); otherwise the church
tier gives an ambient, less-specific note ("v tomto kostele …", no count).
- Map marker: a rubric-red reference mark (‟) on the time chip replaces the
season dot — testimony present, no count, missal-styled.
- Collapsible "Ohlasy poutníků" filter (default collapsed): selecting witness
chips narrows list + map to churches carrying ALL of them at slot- or
church-tier. Persisted with the other filters.
- Tests: new massKey format, riteOf/occurrenceOf, two-tier + frequency order,
churchHasTags predicate, Edge Function submit body; new witness-filter e2e.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…istic tests - Map marker: the rubric ‟ is now large and OVERLAID on the time-chip's top-right corner (paper stroke for legibility over the season green), not a small mark beside it. - Filter: 'Ohlasy poutníků' is its own bordered, collapsible block with a hint line and roomy rounded pills (was cramped at the sheet bottom). - Test isolation: force supabase=null in feedbackStore.test.ts and build the e2e app with Supabase disabled, so a local .env.local can't make tests hit the live DB (they were green in CI only because it has no env). 283 unit + 52 e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…stinctiveness rank Refine pilgrim-witness v2 per owner feedback: - Detail leads with ONE church-level witness block (aggregate over all Masses), ranked by corroboration × distinctiveness (weight = count / (1 + prevalence) across loaded churches), capped at top 3. Per-Mass rows no longer repeat the church testimony — a Mass speaks up only when it diverges (a tag it carries that the church block doesn't already surface). - Map popover shows the church-level tags + count + the divergent-Mass note when the shown Mass diverges. Map chip keeps the single overlaid rubric ‟ mark; no per-chip icons on the map (owner's call). - Witness tags render as read-only pills (AfterMassCard look, smaller), each an icon + label, visually distinct from mass-time rows. New thin-line missal SVGs per chip (src/domain/witnessIcons.tsx), one source feeding React + the DOM popover. - Counts vs qualitative behind one helper (witnessShowCounts): VITE_WITNESS_PREVIEW=1 shows a per-tag count on each pill; production shows no numbers, orders by frequency and renders the strongest tag stronger. The aggregate count line stays. - Tests: distinctiveness ranking + divergence unit tests, preview/qualitative pill test; pilgrim-witness e2e updated for church-first. tsc/vitest/build/e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
… web, swipe-back, list witness mark - AfterMassCard: tighten the dead top margin (mt-5 → mt-3) so the witness card sits near the top under the header instead of ~1cm below it. - mapview.css: enlarge .map-chip for older-eyes legibility (0.78→0.92rem, more padding); the em-based ‟ witness mark, faded dot and clusters stay proportionate. - ChurchDetail: surface a prominent "Web farnosti →" affordance near the top when church.www is known, and link the parish web right inside the stale verify-before-you-go warning. Reverent copy via i18n (cs+en). - ChurchDetail: iOS-style edge swipe-back (left-edge touchstart, rightward horizontal-dominant drag → back) and a sticky, paper-backed back button under the header. Native touch events only, no gesture lib; guarded against scroll/taps. - App list rows: quiet rubric ‟ witness mark for churches with corroborated church-wide testimony; aggregates now load whenever a list is up (mirrors the witness-filter load). Subtle, presence-only — detail carries the testimony. - Tests: unit coverage for each change (card margin, prominent + stale parish link, edge swipe vs mid-screen drag guard, list mark presence/absence) and an e2e assertion the seeded church's row shows the mark. Regenerated screenshots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…a floating mark) The overlaid ‟ read as a detached, clickable 'fly' beside the chip. Replace it with a small rubric folded-corner (CSS ::after on .map-chip--witnessed) — clearly part of the box, nothing to mis-tap. Presence only; the popover carries the testimony. Also mock supabase null in App.test.tsx so a local .env.local can't make the list-mark test hit the live DB (same fix as feedbackStore.test). 297 unit green.
…M referral, tidier long notes, map fold key Owner testing on build 68: 1. Shrink the top "Web farnosti →" affordance to a quiet rubric inline link (min-h-11 tap area, no bordered button) — the page is opened primarily for Mass times. The stale-warning parish link keeps its prominence. 2. withReferral(url) in domain/format.ts tags every outbound parish-web link on the detail page (top affordance, stale-warning link, parish-contacts www) with utm_source/medium/campaign, handling ?/& and preserving #fragments. List rows and the map popover are deliberately left untagged. Unit-tested. 3. NoteText is now its own tight-leading line under the service type, so a long note flows cleanly instead of wrapping the time+type row tall with a ragged gap. Rubric treatment for unverified notes kept. 4. Discreet map key for the dog-ear witness fold: a matching mini-fold swatch + "ohlasy poutníků" (cs/en), shown under the map only while witnessed chips are on screen. No tooltip, no "?" button; the popover still carries the detail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…ical space) The parish-web link was still a separate line taking too much room above the schedule. Fold it into the map · navigate · share row (rubric-tinted so it's findable), switch that row to gap spacing for a bit more air. Web link keeps its UTM referral. Mass times move up.
…ction
Confession rows are stored as service rows whose type is "svátost smíření" and
whose TIME is a window range ("08:30 - 11:30"), not a Mass start. decodeShard now
splits them out of `regular` into a new `confession` field, so ranking, the detail
schedule, the recent-viewers countdown, and the language set — all of which read
`regular` — exclude them for free (no change needed in ranking.ts). The detail page
renders them in their own section below the Mass schedule: grouped by day like the
ordo (rubric day headers, hairlines), time range shown verbatim, no calendar/reminder
(a window is not a single event). i18n heading cs "Svátost smíření" / en "Confession".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…lease) Leaflet's default zoomSnap:1 snapped a fractional pinch to the nearest whole zoom on finger-lift — felt as a jump (zooms in, settles out ~0.3 level on release; owner report, build 70). Proven under WebKit + Chromium (identical). zoomSnap:0 rests at the exact fractional zoom. Clustering unaffected (render() buckets at Math.round(getZoom())); does NOT reintroduce chip-drift/travel (marker placement is latlng-based, position:absolute + fresh-rebuild untouched). e2e guard asserts the map isn't snapped + chips still render at fractional zoom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
… intro guide Feature A — first-run intro guide (IntroGuide.tsx): a skippable, shown-once walkthrough (4 missal-styled cards: nearby Mass, list/map, liturgical colour, verify at parish), re-openable from a discreet footer "nápověda" link and forceable with ?intro=1. localStorage flag bohosluzby:introSeen gates it. Focus-trapped, Escape-closes, aria-modal, CZ+EN via i18n, no new deps. Feature B — confession tier-1 (domain/confession.ts): parseConfessionFromNote mines an explicit confession time out of ~37 Mass-note free-texts that mention svátost smíření but have no typed row. Precision over coverage: extracts only an explicit clock time (or a literal před/po mši), never invents one; attributes the time to the confession segment, drops a time equal to the Mass start. Merged into the existing "Svátost smíření" section, de-duplicated; the Mass note still shows on its own row. Tests: confession parser (7) + ChurchDetail note-sourced entry; IntroGuide trigger/skip/force/colour-card (5). Existing App journeys seeded as returning users. tsc/vitest(315)/build/e2e(53) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
Build-time matcher (data/match-photos.mjs) pairs each church with a licensed Commons photo, precision-first: a candidate file must carry a church keyword AND one of the church's dedication tokens as a WHOLE WORD (not substring), so a neighbour's photo or a street-name coincidence (sv. Josefa vs "Josefská" on a sv. Tomáše file) is rejected — no match means no photo, never a wrong church. Geosearch radius tightened to 150m. Only reusable licenses (CC/PD/GFDL) stored, with required attribution (author + license). Bounded runner: --sample/--limit/ --cell; the full nightly run wires into refresh-data.yml later (TODO in file). Sample over 200 Prague churches → 125 matched (62.5%), all CC/PD, 0 that fail to re-qualify against the church name. photos.json committed for the build. Detail page renders the photo (remote, lazy, ≤480px thumb, never bundled) below the meta row with a "foto:/photo:" credit line — hairline caption, no shadow, rounded 2px, object-cover, alt = church name. Loaded via the loadData gateway so it's OTA-refreshable and offline-cached; absent/offline renders nothing. Tests: unit tests for the pure matcher (token extraction + qualify/reject + license + html-strip); RTL + e2e that a church with an entry shows the img + credit and one without shows none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
… bit users miss) The guide explained the liturgical colour-of-the-day but not the thing users actually wonder about: why a map chip is coloured vs grey. New card 2: a coloured time chip = a Mass matching your selection (day/time/filters), grey dot = a church that doesn't match now but is still tappable. Pager auto-updates to 5 cards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…ity post, media blurb)
…e intro re-open Three owner-requested UI changes from device build 72: 1. ChurchDetail: when a church has a Wikimedia photo, render it as a Booking/Airbnb-style HERO at the top of the page with the church name + meta row (mapa · navigace · sdílet · web farnosti · bezbariérový) overlaid on a warm-ink bottom scrim. Fit is a tall cover (46vh, object-top) so the tower survives instead of a landscape band cropping it off; credit line stays under the title. No photo → the existing plain paper header (one DetailHeading block, two skins, so title/links never drift). 2. MapView legend: expand the witness-only key into a compact colour key — season chip = a Mass matching now, grey dot = a church with nothing matching now, dog-ear fold = pilgrim witness. Colour rows show whenever the map is up; the fold row only while a witnessed chip is on screen. 3. Intro guide: add an always-reachable "? nápověda" control in the masthead. The footer re-open link is hidden in map mode (the default view), so a returning user had no way back into the guide; the masthead control survives every view. Footer link kept. CZ+EN i18n throughout; reverent missal design (hairlines, no emoji, no shadows). Tests: hero-title-over-photo vs plain, legend colour rows, masthead intro re-open in map mode. Golden screenshots refreshed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
Confession times have no database to reuse; the ČBK registry we ship types
"svátost smíření" for only ~24 churches. The one rich source is each diocese's
permanent-confession-service table. Prague's (apha.cz) is confirmed:
DEN | ČAS | KOSTEL | MÍSTO | Poznámka.
- data/scrape-confession-diocese.mjs: pure Node, no deps. Parses the Prague
table, matches each row to a registry id (dedication token AND place token
must agree — precision over coverage; a tie or no place match is skipped and
logged), and writes public/data/confession.json keyed by church id. Day and
time-window are kept verbatim (a window carries nuance — "30 min přede mší",
multiple ranges — that must not be flattened). Other 7 dioceses are
best-effort: look for a confession page, try to parse a table, skip silently
if nothing parses. Reuses fold/dedicationTokens from match-photos.mjs.
- Generated confession.json ships 17 windows across the 7 Prague churches
(100015, 100017, 100019, 100029, 100043, 1000133, 1000663), 0 unmatched.
- ChurchDetail: load confession.json via the OTA/offline loadData gateway,
keyed by church id; render the diocesan rows in the existing Svátost smíření
section (verbatim day + time-range + note, discreet source line). The section
now appears when diocesan data is the only confession signal. Kept out of
`svc`, so these windows never touch the Mass schedule or ranking.
Tests: scraper parse+match units (Prague row → {den,cas,note}; the two sv.
Josefa churches resolve by district; no/ambiguous match → null); RTL that a
confession.json-only church shows its windows in the section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
…nks, sticky-back robustness
Owner + design-review UI pass on the mass-reviews branch:
1. Church-witness block reads as ONE sentence — the count folds into the intro
("V tomto kostele N poutníků často zmiňuje:") via a new churchWitnessIntro()
helper (CZ 1/2–4/5+ agreement); the separate "potvrdilo…" line is dropped.
2. Map legend: season chip = "vyhovuje zadání"; the grey swatch becomes a grey
TIME-CHIP = "jiný den" (the no-match dot row removed); adds the black numbered
cluster = "počet kostelů". Fold row (ohlasy poutníků) unchanged.
3. Intro card 2 reworded to the legend's real states — coloured chip = matches
your choice, grey chip with a weekday = another day; drops the "šedá tečka".
4. Popup verbs (OTEVŘÍT ›, WEB) forced rubric red — raised specificity over
Leaflet's default .leaflet-popup-content a link blue.
5. Photo hero pinned to z-0 so the sticky back band (z-20) always paints above
it; the title now scrolls cleanly under the solid band (WebKit robustness).
6. Programmatic focus no longer draws the seasonal ring — tabindex="-1":focus
outline:none covers the detail title; the intro focuses the dialog (tabIndex
-1) instead of its first button. Keyboard focus still rings.
7. Filter sheet: the "Ohlasy poutníků" disclosure caret flips open (group-open),
and the "kolem" custom-time row shows a "vlastní čas" placeholder instead of a
dangling em-dash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
Owner feedback on build 74, three UI refinements:
1. Map legend back to low-key: drop the bordered card (no border, no boxy
paper background) — two discreet floating swatch+label rows on a
barely-there per-row paper wash. Remove the black numbered cluster row
(and its map_legend_cluster i18n key); the cluster marker on the map
itself is unchanged.
2. Church detail with a photo becomes a full-bleed hero: the image reaches
the very top (the app masthead is suppressed for this case via an
onHeroChange report from ChurchDetail), with "‹ zpět na seznam" (left)
and "? nápověda" (right) overlaid on a top scrim, title + meta on the
bottom scrim as before. No-photo detail keeps the masthead + sticky back
band exactly as-is; the map/list masthead is untouched.
3. Photo credit moved bottom-right, tiny, and tidied — the author is taken
before any " (…)" wiki markup ("ŠJů ( cs:ŠJů )" → "ŠJů"); licence +
Wikimedia Commons kept.
Tests: ChurchDetail hero-nav + credit-cleanup unit tests; e2e photo spec
asserts the overlaid back/help + masthead absence; map legend e2e asserts
two chip rows, no cluster row, no border. tsc/vitest(343)/build/playwright
chromium(55) all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
The public App Store build (v1.2) must not expose the pilgrim-witness / "Ohlasy poutníků" feature (Canon 220 — not yet blessed) while everything else ships (photos, confession, hero, intro, map). Add src/lib/flags.ts: WITNESS_ENABLED = DEV || VITE_WITNESS_PREVIEW==='1' || VITE_WITNESS_ENABLED==='1' OFF in a public `vite build` (DEV=false, no flag) → witness absent by construction. ON for dev, Vitest (DEV), TestFlight (VITE_WITNESS_PREVIEW), and the e2e build (VITE_WITNESS_ENABLED, which does not force-show the demo card the way VITE_WITNESS_PREVIEW does). Gated surfaces: - App: after-Mass card (mount + ledger/?feedback=preview trigger), the "Ohlasy poutníků" filter section, list-row witness mark, list loadAggregates, and a stale persisted witnessTags (dropped in loadFilters when disabled). - MapView: chip witnessed fold, popover witness line/pills, legend fold row, witness filter, loadAggregates. - ChurchDetail: ChurchWitness block, MassDiverges, after-Mass ledger seeding, witness loadAggregates. e2e: playwright build passes VITE_WITNESS_ENABLED=1 so the witness/map/filter specs still exercise the feature. New src/witness-gate.test.tsx mocks the flag false and proves detail/map-legend/filter witness surfaces vanish while confession + photo + schedule keep rendering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5AA9dRxCnS1eMCZfxa2bG
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.
Public v1.2 release codebase. Everything since v1.1: map-default, church photos (Wikimedia, lazy/remote), diocesan confession times, full-bleed photo hero, first-run intro guide, pinch-zoom fix, map/legend polish, parish-web bundling, swipe-back. Pilgrim-witness/reviews are present but GATED OFF in production (WITNESS_ENABLED flag; DEV/TestFlight-only) pending a diocese/priest conversation — verified by render that no review UI ships publicly.
346 unit + 55 e2e green.
🤖 Generated with Claude Code