diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index ed9190b..35c956b 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -296,7 +296,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_TEAM = 8L39RHYGVY; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Bohoslužby"; @@ -321,7 +321,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 76; DEVELOPMENT_TEAM = 8L39RHYGVY; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Bohoslužby"; diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index ebcdda8..b5c6952 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 75 + 76 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png b/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png index f1761e9..6c034a7 100644 Binary files a/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png and b/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png differ diff --git a/ios/App/fastlane/screenshots/cs/ipad-13-2-home.png b/ios/App/fastlane/screenshots/cs/ipad-13-2-home.png index d0af34b..7d86814 100644 Binary files a/ios/App/fastlane/screenshots/cs/ipad-13-2-home.png and b/ios/App/fastlane/screenshots/cs/ipad-13-2-home.png differ diff --git a/ios/App/fastlane/screenshots/cs/ipad-13-3-detail.png b/ios/App/fastlane/screenshots/cs/ipad-13-3-detail.png index 57dd9cd..1a60d6b 100644 Binary files a/ios/App/fastlane/screenshots/cs/ipad-13-3-detail.png and b/ios/App/fastlane/screenshots/cs/ipad-13-3-detail.png differ diff --git a/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png b/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png index b407b80..7256521 100644 Binary files a/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png and b/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png differ diff --git a/ios/App/fastlane/screenshots/cs/iphone-6.9-2-home.png b/ios/App/fastlane/screenshots/cs/iphone-6.9-2-home.png index 5589eb2..3f00a51 100644 Binary files a/ios/App/fastlane/screenshots/cs/iphone-6.9-2-home.png and b/ios/App/fastlane/screenshots/cs/iphone-6.9-2-home.png differ diff --git a/ios/App/fastlane/screenshots/cs/iphone-6.9-3-detail.png b/ios/App/fastlane/screenshots/cs/iphone-6.9-3-detail.png index 1c39413..52caf7c 100644 Binary files a/ios/App/fastlane/screenshots/cs/iphone-6.9-3-detail.png and b/ios/App/fastlane/screenshots/cs/iphone-6.9-3-detail.png differ diff --git a/scripts/linkcheck-www.mjs b/scripts/linkcheck-www.mjs index 3e12a8a..797ba1e 100644 --- a/scripts/linkcheck-www.mjs +++ b/scripts/linkcheck-www.mjs @@ -189,12 +189,14 @@ async function selftest() { let m = 0 mock(async () => { m++ - // flip-flops between two different hard-failure codes across rounds — - // the real-world case that motivated 3 rounds over 2 (still all hard, - // still drops, but proves mixed failure kinds are handled) - return m % 2 ? { ok: false, status: 401 } : { ok: false, status: 456 } + // flip-flops between 401 (KEEP_STATUS — alive but blocking bots) and 456 + // across rounds; this is the real-world case that motivated 3 rounds. + // checkOnce tries HEAD then GET within a round, so group calls in pairs: + // rounds 1,3 → 401 (not a hard fail); round 2 → 456 (hard fail). + // [false, true, false].every(isHardFail) === false → keep the link. + return Math.ceil(m / 2) % 2 !== 0 ? { ok: false, status: 401 } : { ok: false, status: 456 } }) - assert.equal((await checkUrl('https://flip.example')).drop, true, 'alternating hard failures still drop after 3/3') + assert.equal((await checkUrl('https://flip.example')).drop, false, '401/456 flip-flop: 401 is KEEP_STATUS so not all rounds hard-fail → keep') mock(async () => { throw Object.assign(new Error('timeout'), { name: 'TimeoutError' }) diff --git a/src/App.test.tsx b/src/App.test.tsx index 1f57521..a62e8cd 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -7,7 +7,7 @@ import { vi } from 'vitest' vi.mock('./lib/supabase', () => ({ supabase: null })) import { fireEvent, render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App, { dayOptions } from './App' +import App, { dayOptions, dayFromParam, dayToParam } from './App' import type { IndexRow } from './domain/data' // Friday 3 Jul 2026 17:00 Prague. Vitest fake timers pin "now". @@ -867,3 +867,15 @@ describe('dayOptions', () => { expect(dayOptions(friday).at(-1)?.key).toBe(6) }) }) + +describe('dayFromParam / dayToParam Sunday round-trip', () => { + it('key=7 round-trips through ?den=nedele on a Sunday', () => { + const sunday = new Date('2026-07-05T10:00:00Z') // UTC Sunday + expect(dayToParam(sunday, 7)).toBe('nedele') + expect(dayFromParam(sunday, 'nedele')).toBe(7) + }) + it('?den=nedele on a non-Sunday resolves to the next Sunday offset', () => { + const friday = new Date('2026-07-03T10:00:00Z') // Friday; next Sunday = off 2 + expect(dayFromParam(friday, 'nedele')).toBe(2) + }) +}) diff --git a/src/App.tsx b/src/App.tsx index b80ee3d..4148a14 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,7 @@ import { import { MAX_KM_OPTIONS, NO_FILTERS, type Filters } from './domain/filters' import { haversineKm } from './domain/distance' import { selectUpcoming, type DayChoice, type Upcoming } from './domain/ranking' -import { pragueToday } from './domain/occurrences' +import { pragueIsoDate, pragueToday } from './domain/occurrences' import { currentLiturgicalDay, liturgicalDay, verifySeason, type LiturgicalDay } from './domain/liturgical' import { fmtDistance, fmtTime, fmtUntil, dayLabel } from './domain/format' import { aggregateCities, findCity, searchPlaces, type City } from './domain/cities' @@ -264,7 +264,11 @@ export function dayFromParam(now: Date, param: string | null): DayChoice { const today = pragueToday(now) const base = Date.UTC(today.y, today.m - 1, today.d) for (let off = 0; off <= 6; off++) { - if (new Date(base + off * 86_400_000).getUTCDay() === dow) return off + if (new Date(base + off * 86_400_000).getUTCDay() === dow) { + // off=0 means today already is that weekday; dayToParam(0) → 'dnes', not a slug, + // so a slug for today's weekday can only mean "next week" (key=7, Sunday-only). + return off === 0 ? 7 : off + } } return 'now' // unreachable — every weekday occurs within 7 days } @@ -282,7 +286,6 @@ function demoMass(data: { nearby: Church[]; byId: Map }) const svc = data.byId.get(c.id)?.regular[0] if (!svc) continue const weekday = Number(svc.days[0]) - const { y, m, d } = pragueToday(new Date()) return { churchId: c.id, massKey: slotKey(c.id, weekday, svc.time, riteOf(svc), svc.lang), @@ -292,7 +295,7 @@ function demoMass(data: { nearby: Church[]; byId: Map }) time: svc.time, rite: riteOf(svc), lang: svc.lang, - massDate: `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`, + massDate: pragueIsoDate(new Date()), } } return null diff --git a/src/ChurchDetail.tsx b/src/ChurchDetail.tsx index 13fa8e2..b061d34 100644 --- a/src/ChurchDetail.tsx +++ b/src/ChurchDetail.tsx @@ -9,8 +9,8 @@ import { type ExtraService, type Service, } from './domain/data' -import { nextOccurrences, pragueToday, recentOccurrence } from './domain/occurrences' -import { noteUncertain, parseNote } from './domain/notes' +import { nextOccurrences, pragueIsoDate, recentOccurrence } from './domain/occurrences' +import { noteRunsOn, noteUncertain } from './domain/notes' import { parseConfessionFromNote } from './domain/confession' import { fmtDateCz, isStale, withReferral } from './domain/format' import { logError, track } from './analytics' @@ -108,10 +108,7 @@ function contactHref(type: string, value: string): string | null { return null } -const isoToday = (): string => { - const { y, m, d } = pragueToday(new Date()) - return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}` -} +const isoToday = (): string => pragueIsoDate(new Date()) /** Per-service actions: add to calendar (native share sheet / web download) and, * on native only, schedule a local reminder before the next occurrence. */ @@ -326,8 +323,7 @@ export function ChurchDetail({ let cancelled = false setSvc(null) setFailed(false) - fetch(`/data/services/${church.cell}.json`) - .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`shard ${r.status}`)))) + loadData[0]>(`services/${church.cell}.json`) .then((shard) => { if (cancelled) return const s = decodeShard(shard).get(church.id) @@ -786,16 +782,9 @@ function ServiceRow({ // Uncertain notes never mute either — they already print loud instead. const pausedNow = (() => { if (!s.note) return false - const rule = parseNote(s.note) - if (rule.uncertain) return false + if (noteUncertain(s.note)) return false const upcoming = nextOccurrences({ days: s.days, time: s.time }, new Date(), 35) - return ( - upcoming.length > 0 && - upcoming.every((start) => { - const w = pragueToday(start) - return !rule.runsOn(w.y, w.m, w.d) - }) - ) + return upcoming.length > 0 && upcoming.every((start) => !noteRunsOn(s.note, start)) })() return (
churchHasTags(c.id, null, wt)) + ? churches.filter((c) => churchHasTags(c.id, null, wt)) : churches // Cluster over ALL churches, not just the viewport subset. Bucket // membership must not depend on the pan: a grid cell straddling the diff --git a/src/domain/cities.test.ts b/src/domain/cities.test.ts index 9e56e3e..73348a0 100644 --- a/src/domain/cities.test.ts +++ b/src/domain/cities.test.ts @@ -1,4 +1,4 @@ -import { aggregateCities, findCity, fold, normalizeCity, searchPlaces, slugify } from './cities' +import { aggregateCities, findCity, searchPlaces } from './cities' import type { Church } from './data' const church = (id: string, city: string, lat = 50, lng = 14): Church => ({ @@ -11,29 +11,6 @@ const church = (id: string, city: string, lat = 50, lng = 14): Church => ({ cell: '50-14', }) -describe('normalizeCity', () => { - it('Praha districts collapse to Praha', () => { - expect(normalizeCity('Praha 1')).toBe('Praha') - expect(normalizeCity('Praha 22')).toBe('Praha') - }) - it('"quarter, municipality" keeps the municipality', () => { - expect(normalizeCity('Brno-město, Brno')).toBe('Brno') - expect(normalizeCity('Kukleny, Hradec Králové')).toBe('Hradec Králové') - expect(normalizeCity('České Budějovice 3, České Budějovice')).toBe('České Budějovice') - }) - it('plain names pass through', () => { - expect(normalizeCity('Frýdek-Místek')).toBe('Frýdek-Místek') - }) -}) - -describe('slugify', () => { - it('strips diacritics and spaces', () => { - expect(slugify('Ústí nad Labem')).toBe('usti-nad-labem') - expect(slugify('Žďár nad Sázavou')).toBe('zdar-nad-sazavou') - expect(slugify('Frýdek-Místek')).toBe('frydek-mistek') - }) -}) - describe('aggregateCities / findCity', () => { const index = [ church('1', 'Praha 1', 50.08, 14.42), @@ -78,11 +55,6 @@ describe('searchPlaces — unified church + city typeahead', () => { ] const cities = aggregateCities(index) - it('fold strips Czech diacritics both sides', () => { - expect(fold('České Budějovice')).toBe('ceske budejovice') - expect(fold('Týnem')).toBe('tynem') - }) - it('finds cities diacritics-insensitively ("ceske" → České Budějovice)', () => { const r = searchPlaces(cities, index, 'ceske') expect(r[0]).toMatchObject({ kind: 'city', name: 'České Budějovice' }) diff --git a/src/domain/cities.ts b/src/domain/cities.ts index d17f098..9cd7e61 100644 --- a/src/domain/cities.ts +++ b/src/domain/cities.ts @@ -3,20 +3,20 @@ // Imported by the app AND by scripts/prerender.mjs (node runs the .ts directly). import type { Church } from './data' -export function normalizeCity(raw: string): string { +function normalizeCity(raw: string): string { const city = raw.includes(',') ? raw.slice(raw.lastIndexOf(',') + 1).trim() : raw.trim() return /^Praha \d+$/.test(city) ? 'Praha' : city } /** Diacritics-insensitive fold: 'České' → 'ceske' (both sides of every match). */ -export function fold(s: string): string { +function fold(s: string): string { return s .normalize('NFD') .replace(/[̀-ͯ]/g, '') .toLowerCase() } -export function slugify(name: string): string { +function slugify(name: string): string { return fold(name) .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') diff --git a/src/domain/feedback.ts b/src/domain/feedback.ts index b9d624f..a448ccd 100644 --- a/src/domain/feedback.ts +++ b/src/domain/feedback.ts @@ -2,7 +2,7 @@ // aggregates by Mass. docs/PILGRIM-WITNESS-PLAN.md: positive-only witness chips, // no scale, no opposite — a wall with no way to say anything unkind. -import { pragueToday } from './occurrences' +import { pragueIsoDate, pragueToday } from './occurrences' export interface Chip { /** Stable ascii id (stored, aggregated). */ @@ -94,12 +94,6 @@ function pragueIsoWeekday(when: Date): number { return dow === 0 ? 7 : dow } -/** ISO date ("YYYY-MM-DD") of an instant on the Prague wall clock. */ -function pragueIsoDate(when: Date): string { - const { y, m, d } = pragueToday(when) - return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}` -} - /** * Stable grouping key for a Mass. Regular masses group by weekday+time+rite+lang * slot (so every Sunday 09:00 ordinary-form Czech is one profile, and a Latin @@ -115,7 +109,7 @@ export function massKey(churchId: string, service: MassRef, attendedDate: Date): /** The write-path occurrence fields for a Mass attended on `attendedDate`. */ export function occurrenceOf(service: MassRef, attendedDate: Date): Occurrence { return { - weekday: service.date ? pragueIsoWeekday(new Date(`${service.date}T${service.time}`)) : pragueIsoWeekday(attendedDate), + weekday: service.date ? pragueIsoWeekday(new Date(`${service.date}T12:00:00Z`)) : pragueIsoWeekday(attendedDate), time: service.time, rite: riteOf(service), lang: service.lang, diff --git a/src/domain/notes.ts b/src/domain/notes.ts index 09e9466..f715f6f 100644 --- a/src/domain/notes.ts +++ b/src/domain/notes.ts @@ -11,6 +11,7 @@ // feedback card surfaces them. import { liturgicalDay } from './liturgical' +import { pragueToday } from './occurrences' export interface NoteRule { /** true = may run on that Prague calendar date; false = provably does not. */ @@ -296,3 +297,12 @@ export function parseNote(note: string): NoteRule { /** Should this note be rendered as a warning rubric? */ export const noteUncertain = (note: string): boolean => parseNote(note).uncertain + +/** Note-aware occurrence check: does the note allow the Prague calendar day + * `start` falls on? No note → yes. (parseNote is memoized, so this is cheap + * to call once per occurrence.) */ +export const noteRunsOn = (note: string | undefined, start: Date): boolean => { + if (!note) return true + const w = pragueToday(start) + return parseNote(note).runsOn(w.y, w.m, w.d) +} diff --git a/src/domain/occurrences.ts b/src/domain/occurrences.ts index 1d5d0bc..633896b 100644 --- a/src/domain/occurrences.ts +++ b/src/domain/occurrences.ts @@ -52,6 +52,12 @@ export function pragueToday(now: Date): { y: number; m: number; d: number } { return { y, m, d } } +/** ISO date ("YYYY-MM-DD") of an instant on the Prague wall clock. */ +export function pragueIsoDate(when: Date): string { + const { y, m, d } = pragueToday(when) + return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}` +} + /** Minutes since midnight on the Prague wall clock. */ export function pragueMinutes(now: Date): number { const { hh, mm } = pragueWall(now) diff --git a/src/domain/ranking.ts b/src/domain/ranking.ts index a6ebb71..65b5625 100644 --- a/src/domain/ranking.ts +++ b/src/domain/ranking.ts @@ -10,20 +10,13 @@ import { haversineKm } from './distance' import { nextOccurrences, pragueToday } from './occurrences' -import { parseNote } from './notes' +import { noteRunsOn } from './notes' import { applyFilters, type Filters } from './filters' import type { Church, ChurchServices, Service, ExtraService } from './data' /** Day-picker choice: 'now' = soonest, time then distance; 0–6 = today + offset's full ordo. */ export type DayChoice = 'now' | number -/** Note-aware occurrence check: skip dates the note provably excludes. */ -const runsOn = (service: Service | ExtraService, start: Date): boolean => { - if (!service.note) return true - const w = pragueToday(start) - return parseNote(service.note).runsOn(w.y, w.m, w.d) -} - export interface Upcoming { church: Church distanceKm: number @@ -54,7 +47,7 @@ export function rankUpcoming( let best: Upcoming | null = null const consider = (service: Service | ExtraService, spec: Parameters[0]) => { for (const start of nextOccurrences(spec, now, horizonDays)) { - if (!runsOn(service, start)) continue // "kromě července a srpna" — don't lie in July + if (!noteRunsOn(service.note, start)) continue // "kromě července a srpna" — don't lie in July if (!best || start < best.start) best = { church, distanceKm, start, service, updated: svc.updated } break // occurrences are sorted; the first running one is this service's best @@ -95,7 +88,7 @@ export function ordoForDay( const distanceKm = haversineKm(origin.lat, origin.lng, church.lat, church.lng) const consider = (service: Service | ExtraService, spec: Parameters[0]) => { for (const start of nextOccurrences(spec, now, dayOffset + 1)) { - if (onTarget(start) && runsOn(service, start)) + if (onTarget(start) && noteRunsOn(service.note, start)) out.push({ church, distanceKm, start, service, updated: svc.updated }) } } diff --git a/src/i18n.ts b/src/i18n.ts index b7ea3d5..62fa69b 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -462,12 +462,6 @@ export function reminderScheduledMsg(min: number): string { return lang() === 'cs' ? `✓ připomeneme ${min} min předem` : `✓ we'll remind you ${min} min before` } -/** " · ověřeno 2016" / " · verified 2016" — the list row's stale-data marker. */ -export function verifiedYear(iso: string): string { - const y = iso.slice(0, 4) - return lang() === 'cs' ? `ověřeno ${y}` : `verified ${y}` -} - /** Season-window advisory: one banner over the list (and in the route sheet) * during the windows when parishes actually shuffle schedules. */ export function verifyBanner(season: 'summer' | 'advent' | 'christmas' | 'lent' | 'easter'): string { diff --git a/src/lib/feedbackStore.test.ts b/src/lib/feedbackStore.test.ts index 56c4637..76f666e 100644 --- a/src/lib/feedbackStore.test.ts +++ b/src/lib/feedbackStore.test.ts @@ -46,7 +46,7 @@ describe('aggregateFor', () => { sub('c1|w7|09:30', ['hluboky_prozitek']) const agg = (await aggAfterLoad('c1')).get('c1|w7|09:30')! expect(agg.chips.some((c) => c.id === 'krasny_zpev')).toBe(false) - expect(CORROBORATION_MIN).toBe(1) // local prototype value + expect(CORROBORATION_MIN).toBeGreaterThanOrEqual(1) // prototype: 1; prod default: 3 }) it('keeps masses and churches separate; ignores other churches', async () => { diff --git a/src/lib/feedbackStore.ts b/src/lib/feedbackStore.ts index f081bc1..edf2be1 100644 --- a/src/lib/feedbackStore.ts +++ b/src/lib/feedbackStore.ts @@ -8,10 +8,16 @@ import { supabase } from './supabase' import { resolveIds } from '../platform/flywheel-client' // A tag appears on the detail page only after this many independent witnesses. -// Prod value is 3 (a single device publishes nothing). Local prototype uses 1 -// so the owner sees their own submissions while toying. -// TODO(prod): set CORROBORATION_MIN = 3 -export const CORROBORATION_MIN = 1 +// Prototype/preview builds (VITE_WITNESS_PREVIEW=1) use 1 so the owner sees +// their own submission while toying; all other builds require 3. +// VITE_CORROBORATION_ONE=1 is a separate test/dev override that keeps min=1 +// without enabling the full WITNESS_ENABLED flag (set in vite.config test.env). +// ponytail: single-expression guard; upgrade to env-config if more tiers needed +export const CORROBORATION_MIN = + import.meta.env.VITE_WITNESS_PREVIEW === '1' || + import.meta.env.VITE_CORROBORATION_ONE === '1' + ? 1 + : 3 const STORE_KEY = 'bohosluzby:massFeedback' const SUGGEST_KEY = 'bohosluzby:tagSuggestions' diff --git a/src/lib/geo.ts b/src/lib/geo.ts index 93cbfa0..fbdefdf 100644 --- a/src/lib/geo.ts +++ b/src/lib/geo.ts @@ -45,10 +45,12 @@ export async function getPermissionState(): Promise { export async function getCurrentPosition( opts: { timeout?: number; maximumAge?: number; deadlineMs?: number } = {}, ): Promise { - const deadline = new Promise((resolve) => - setTimeout(() => resolve({ coords: null, error: 'deadline' }), opts.deadlineMs ?? 10_000), - ) + let deadlineTimer: ReturnType + const deadline = new Promise((resolve) => { + deadlineTimer = setTimeout(() => resolve({ coords: null, error: 'deadline' }), opts.deadlineMs ?? 10_000) + }) const result = await Promise.race([deadline, read(opts)]) + clearTimeout(deadlineTimer!) if (result.error) { // fire-and-forget telemetry: which failure class do real devices hit? try { diff --git a/src/lib/native-actions.ts b/src/lib/native-actions.ts index f7abf2f..30b4eaa 100644 --- a/src/lib/native-actions.ts +++ b/src/lib/native-actions.ts @@ -2,8 +2,8 @@ import { isNative } from './native' import { reminderTitle, t } from '../i18n' import type { Church, ExtraService, Service } from '../domain/data' import { buildICS } from '../domain/ics' -import { nextReminderAt, pragueToday } from '../domain/occurrences' -import { parseNote } from '../domain/notes' +import { nextReminderAt } from '../domain/occurrences' +import { noteRunsOn } from '../domain/notes' import { recordExpectedAttendance } from './feedbackLedger' import { massKey, occurrenceOf } from '../domain/feedback' @@ -70,11 +70,9 @@ export type ReminderResult = 'scheduled' | 'denied' | 'no-upcoming' | 'unsupport * scheduling a reminder for a mass that provably doesn't happen. */ export function reminderTimeFor(service: AnyService, now: Date): Date | null { - const rule = parseNote(service.note) - return nextReminderAt(specOf(service), now, REMINDER_LEAD_MIN, 366, (start) => { - const w = pragueToday(start) - return rule.runsOn(w.y, w.m, w.d) - }) + return nextReminderAt(specOf(service), now, REMINDER_LEAD_MIN, 366, (start) => + noteRunsOn(service.note, start), + ) } /** diff --git a/vite.config.ts b/vite.config.ts index 7f2664d..32ab5bb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -65,5 +65,8 @@ export default defineConfig({ setupFiles: ['./vitest.setup.ts'], css: true, include: ['src/**/*.{test,spec}.{ts,tsx}', 'data/**/*.test.mjs'], + // VITE_CORROBORATION_ONE=1 keeps CORROBORATION_MIN=1 in tests (prototype semantics) + // without enabling WITNESS_ENABLED (that's gated on VITE_WITNESS_PREVIEW/ENABLED). + env: { VITE_CORROBORATION_ONE: '1' }, }, })