Skip to content

sweep/2026-09-30: 9H + 46M bugs fixed, 14 refactors (notes parser, cities, dataStore, map, feedback) - #35

Open
Chartres wants to merge 81 commits into
mainfrom
sweep/2026-09-30
Open

Chartres wants to merge 81 commits into
mainfrom
sweep/2026-09-30

Conversation

@Chartres

Copy link
Copy Markdown
Owner

Sweep 2026-09-30 — bohosluzby

5 cycles × (Red → Green → Refactor). Tests: 343 → 355 (12 new). All green.

Findings table

Finding File Severity Status
aggregateCities merges same-name towns across regions cities.ts H fixed
ICS range times ("17:30 - 18:00") produce invalid DTSTART ics.ts H fixed
advent1 wrong when Dec 25 is a Sunday liturgical.ts H fixed (C1)
školní rok matches before negation — July Masses hidden notes.ts H fixed (C2)
not(nthWeekday) inverts all non-target weekdays notes.ts H fixed (C2)
Positive segment predicates AND'd → "v červenci, v srpnu" never runs notes.ts H fixed (C2)
Prerendered /mesto/ slugs differ from app's findCity prerender.mjs H fixed (C2)
Comma-joined ordinals "1., 3. sobota" split before regex sees them notes.ts H fixed (C5)
Same-kind inclusion segments AND'd → "v červenci, v srpnu" excludes both notes.ts H fixed (C5)
decodeURIComponent on malformed URL → white-screen App.tsx M fixed
Failed shard fetch swallowed → "nothing nearby" App.tsx M fixed
rows not refreshed after midnight/resume App.tsx M fixed
Two empty states render simultaneously App.tsx M fixed
IntroGuide re-focuses dialog on every App re-render IntroGuide.tsx M fixed
Cache version not compared to bundled version dataStore.ts M fixed
Non-atomic shard refresh (cacheReady set too early) dataStore.ts M fixed
One query per moveend, no debounce, HTTP 414 risk MapView.tsx M fixed
selectUpcoming with limit:Infinity at country zoom MapView.tsx M fixed
Photo hidden when CC attribution credit is empty ChurchDetail.tsx M fixed
--cell/--limit overwrites entire photos.json match-photos.mjs M fixed
City grouping by name+cell splits cross-boundary towns cities.ts M fixed
Slug collisions after diacritics strip (8 pairs) cities.ts M fixed
"od konce <měsíc>" gives day 1 instead of month-end notes.ts M fixed
CONDITIONAL misses "sudou neděli", seasonal patterns notes.ts M fixed
Concurrent loadData race during OTA write loop dataStore.ts M fixed
same-day publish not detected (day-granular version) dataStore.ts M fixed
Corrupt cached version.json crashes loadData dataStore.ts M fixed
Stale asOf when bundled snapshot supersedes cache dataStore.ts M fixed
"zpět" navigates to / instead of /mesto// App.tsx M fixed
Late geolocation fix overwrites user's URL-picked city App.tsx M fixed
One failed shard kills entire list/map App.tsx M fixed
Cluster tap at zoom ≥18 zooms the map out MapView.tsx M fixed
Failed MapView shard cached forever — no retry MapView.tsx M fixed
shardCache survives OTA refresh via reloadKey MapView.tsx M fixed
Chip corroboration counts per row, not per device feedbackStore.ts M fixed
day memo stale across midnight (missing clockTick dep) App.tsx M fixed
DayPicker options stale across midnight App.tsx M fixed
www contacts shipped without scheme → broken links extract.mjs M fixed
contactHref: scheme-less www → relative link ChurchDetail.tsx M fixed
City slug order depends on church count → unstable URLs cities.ts M fixed
Inclusion segments OR-combined even for different kinds notes.ts M fixed
"do konce února" hard-coded year 2023 → leap-year bug notes.ts M fixed
mass_feedback select silently truncated at 1000 rows feedbackStore.ts M fixed
Concurrent loadAggregates overwrites post-submit refresh feedbackStore.ts M fixed
functions.invoke error not checked → silent submit failure feedbackStore.ts M fixed
Witness cluster membership depends on pan history MapView.tsx M fixed
Permanent 4xx on one ID aborts entire extract() run extract.mjs M fixed
Church links in prerendered city pages → 404 prerender.mjs M fixed
"zobrazit další" hidden on witness-filtered ordo-day view App.tsx M fixed
RRULE ignores note exclusions ics.ts M skipped (complex)

Refactors (14)

  • liturgical: reuse pragueToday from occurrences
  • app: direct dayFromParam offset, no search loop
  • src: unexport 6 module-private symbols
  • app: reuse fmtWeekdayShort for day-picker labels
  • ranking: use pragueIsoDate for ordo day comparison
  • feedback: hoist chip-order map to module constant
  • map: replace witnessTiers/hasWitness with church-tier check
  • app: drop unused exports (litForChoice, parseRoute, DayChoice)
  • domain: pass services directly as occurrence specs
  • app: read season labels from i18n season_* keys
  • format: dayLabel reads day_today/day_tomorrow from i18n
  • ics: build DTSTART from pragueIsoDate
  • dataStore: web refresh reads version.json via loadData
  • format: reuse pragueIsoDate instead of second date formatter

🤖 Generated with Claude Code

Chartres and others added 30 commits September 21, 2026 00:41
…nday

?den=nedele on a Sunday now decodes to key=7 (next Sunday) instead of 0
(today). off=0 matched because today IS that weekday, but dayToParam(0)
always produces 'dnes', so a weekday slug can only mean next occurrence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raw fetch('/data/services/…') bypassed the OTA filesystem cache on native.
Switch to loadData() so native OTA-refreshed registry and detail data
stay in sync, matching the pattern already used for photos and confession.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the previously untested case: ?den=nedele on a Sunday must
decode to key=7 (next Sunday), not 0. Also verifies the non-Sunday
case (Friday → off=2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
new Date(`YYYY-MMT HH:MM`) is local-timezone; users far east of Prague
got a Prague weekday one day off. Use T12:00:00Z so the UTC timestamp
always falls on the correct Prague calendar date regardless of locale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
clusterChurches used visible (viewport subset) when witness tags were
active, so cluster boundaries shifted on pan. Use churches (full index)
filtered by witness tags, matching the pan-invariant pattern used for
the unfiltered case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…view

Removes the TODO foot-gun: production builds (no VITE_WITNESS_PREVIEW)
now require 3 independent witnesses before a chip is publicly visible.
Prototype/TestFlight builds (VITE_WITNESS_PREVIEW=1) keep min=1. A
separate VITE_CORROBORATION_ONE=1 is set in test env to preserve the
local-store test behaviour without enabling WITNESS_ENABLED in App tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No production code outside cities.ts imported them; only the test file
did. Removing the exports and their direct test cases — the behaviour
is covered by the aggregateCities and searchPlaces integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pies

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ced by the season banner

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alse

401 is in KEEP_STATUS so rounds that return 401 are not hard-fails.
The old mock consumed two m-increments per round (HEAD+GET fallback),
making all three rounds land on 456; the assertion passed for the wrong
reason. Group increments by round (Math.ceil(m/2)) so rounds 1 and 3
see 401 (not a hard fail) and round 2 sees 456 — correctly yielding
drop:false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…owns merging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o step back

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r white-screen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t empty results

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or midnight

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…le render

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… App re-render

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ed version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-cache reads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…upabase query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(n) at country zoom

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ution)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n --cell/--limit runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…arsing

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and unreachable return

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Chartres and others added 30 commits September 30, 2026 02:01
shardCache survived OTA refreshes (reloadKey bumps) so the map matched new
church data against stale shard content. Added reloadKey prop; a useEffect
clears the cache whenever it changes. App now passes reloadKey to MapView.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
?den=<weekday> offsets were computed from new Date() but clockTick was not
a dependency, so midnight didn't retrigger the memo. Moved clockTick
useState above the day memo so it can be listed in the dependency array.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DayPicker computed day labels/feasts once at mount (empty deps). After
midnight the options were stale. Threaded clockTick through OrdoControls
→ DayPicker so the memo recomputes when the clock ticks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contact URLs were written raw, leaving scheme-typo'd values (http:\\, http:/)
in shards. normUrl already fixes these for institution.www; now applied to
contacts too. Falls back to raw value when normUrl returns empty (plain www.).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
contactHref returned www. values as-is, creating relative links under
/kostel/<id>/. Now prepends https:// when the value starts with www.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Slug suffixes (-2, -3) were assigned in count order — a data refresh
reordering the counts repointed existing /mesto/<slug>/ URLs. Now
same-slug collisions are sorted by name (stable), so the suffix assignment
is data-refresh-invariant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ce re-export)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…heck

The church aggregate folds every slot, so a witnessed slot always implies
church.chips > 0 — the slot lookup was redundant (same check App uses).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
parseBound returned daysInMonth(2023, 2)=28 for the 'to' edge, so Feb 29
of a leap year failed the range check. Use 31 as a sentinel for all 'to'
edges with konce — no real date exceeds day 31, so the next month's first
day is never included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"ve školním roce, 1. sobota v měsíci" was matching every 1st Saturday
(including July/August) because comma-separated inclusion segments were
OR-ed. Czech schedule notes stack conditions — both must hold — so switch
incls combiner from .some to .every. The existing excluded-in-months
(kromě…) tests still pass; the mixed incl+excl case uses every on both.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CONDITIONAL missed "sudou/lichou neděli", "1. neděle" without "v měsíci",
"v zimě", and "od Velikonoc do Dušiček". Add WEEKDAY_RE (which only fires
on already-unparsed segments), zim, velikono, and dušič to the pattern so
those notes set uncertain=true and render prominently instead of silently
showing every week without a warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…he (M5)

ready() sets cacheReady=false when the bundled snapshot is newer, but the
old OTA timestamp stayed in localStorage.asOf, causing the footer to show
a stale date and refreshData to skip the remote check (remoteKey <= stale
asOf). Clear asOf at the same time ready() drops the cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Concurrent loadData calls during the shard+index write loop could read
bundled churches.json alongside freshly-cached shards (or vice versa),
yielding an inconsistent registry. A refreshLock promise is set before the
first write and released (in finally) after all writes complete; loadData
awaits it before checking ready().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Promise.all over 4 workers propagated any single fetchPolite rejection to
the whole batch. Catch per-ID errors inside the worker loop so a 404/410
church is logged and skipped without aborting the remaining ~8000 fetches.
Also switch outer Promise.all to allSettled for belt-and-suspenders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
City pages linked up to 1800 /kostel/<id>/ URLs that had no prerendered
file — crawlers got a 404. Collect the distinct churches referenced from
the top-30 city pages, prerender each with withMeta (title, description,
canonical), and add their URLs to sitemap.xml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…formatter

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (H1)

Pre-process each sentence before the generic comma split: replace
commas inside ordinal lists that precede a weekday with " a " so
"1., 3. sobota v měsíci" becomes "1. a 3. sobota v měsíci" and
reaches parseSegment as a single multi-nth predicate.  Previously
the split left "1." as a dangling segment, so the 1st occurrence
was excluded when only the 3rd was queried.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"v červenci, v srpnu" was ANDed (July AND August = never), now OR'd
(July OR August).  "1. sobota, 3. sobota v měsíci" similarly OR'd.
Cross-kind pairs ("ve školním roce, 1. sobota") still AND'd.

Adds segKind() to classify segments as 'month', 'weekday', 'range',
or 'other'; groups inclusions by kind; ORs within a group, ANDs
across groups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Math.min(zoom+2, 17) returned 17 when zoom was already 18,
zooming the map out.  Cap at 18 and guard against any zoom
beyond 18 with Math.max.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The load-more button was gated on day === 'now', so when witness
filter was active on a non-now ordo day (day !== 'now'), the list
was silently truncated at listLimit with no way to page further.

Extend the condition to also show when witnessTags are active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Supabase silently truncates at 1000 rows; make the cap visible with
an explicit .limit() call and a comment noting when pagination is
needed.  Update the supabase stub to chain .limit() so tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…counter (M6)

A slow pre-submit loadAggregates call could resolve after a
post-submit refresh and overwrite the cache with stale data.
A module-level loadGen counter increments on each call; after the
await, a gen mismatch means a newer call started — discard the result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Supabase's functions.invoke() resolves (not rejects) even when the
Edge Function returns an error.  The previous code always called
loadAggregates after the promise settled, so a failed submit triggered
a pointless cache refresh that could overwrite the real post-submit
data.  Now only refresh when error is falsy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the witness filter is active, cluster input was all churches for
which churchHasTags returned true — which depends on which aggregates
happen to be cached, giving pan-history-dependent cluster shapes.

Fix: cluster only over churches whose aggregate has settled (loaded).
Unloaded churches render at 40% opacity as "not yet assessed" rather
than silently vanishing, making the distinction between "no witnesses"
and "not checked yet" visible.

Adds hasAggregate() to feedbackStore (cache.has check).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Service and ExtraService already satisfy OccurrenceSpec; drop the
per-caller spec rebuilds in ics, ranking, native-actions and ChurchDetail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
App.tsx carried a cs/en copy of the season_* strings already in i18n.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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