Skip to content

[pull] main from cwjoshuak:main#32

Merged
pull[bot] merged 35 commits into
git-0xCat:mainfrom
cwjoshuak:main
May 7, 2026
Merged

[pull] main from cwjoshuak:main#32
pull[bot] merged 35 commits into
git-0xCat:mainfrom
cwjoshuak:main

Conversation

@pull

@pull pull Bot commented May 7, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

cwjoshuak added 30 commits May 6, 2026 21:26
…DaisyUI 5

Comprehensive dependency upgrade and migration. All deps to latest, with
explicit handling for breaking changes:

- Pin next@16.2.5; React 19, TypeScript 5, Tailwind 4 (CSS-first config via
  @tailwindcss/postcss + @plugin daisyui in globals.css), DaisyUI 5, Luxon 3.
- Node pinned to 22 via .nvmrc (Next 16 requires >=20.9).
- Drop core-js (modern browser targets handle Array.prototype.at).
- Migrate i18n: next-i18next was rewritten in v16 and dropped Pages-Router
  helpers. Replace with plain react-i18next + bundled translations in
  util/i18n.ts. No more serverSideTranslations / appWithTranslation.
- Replace @olerichter00/use-localstorage with in-house hook (util/useLocalStorage.ts);
  the upstream lib was unmaintained since 2021 and SSR-unsafe.
- Switch @tabler/icons (v1, dead) to @tabler/icons-react.
- Replace next/image with plain <img loading="lazy" decoding="async"> for the
  three icon usages. Eliminates Vercel image-transformation quota burn — the
  icons are 20-38px so the loss of Next's optimization pipeline is negligible.
- Move .mp3 alert sounds to /public/sounds and reference as URL strings;
  drop the bespoke webpack asset rule (Turbopack-friendly).
- Add Luxon 3 nullability guards (Interval.start/end are now DateTime | null).
- React 19: useRef<T>(undefined), JSX namespace -> React.JSX.
- Extract isWithinNotifyWindow predicate from pages/alarms.tsx into
  util/alarmTrigger.ts so the alarm-trigger logic is testable in isolation.
- Restore DaisyUI 4 .label flex layout via globals.css compat block (v5
  removed the default flex/justify-between/align-items styling).
- Hydration fixes for clock cells (gate on mounted state).
- Skip events whose IDs aren't in events.json instead of crashing
  (data.json drift symptom — will resolve with the data pipeline).
…logic

Test infrastructure for the modernized stack so future changes have a
regression net.

- Vitest 4 + jsdom + RTL + jest-dom matchers for component/unit tests.
- Playwright config (chromium) for e2e — specs to be filled in next.
- GitHub Actions: typecheck + Vitest on PRs/pushes; separate e2e job that
  builds and runs Playwright with artifact upload.

Initial coverage focuses on load-bearing pure logic — not UI chrome:
- GameEvent.latest(): next-interval semantics across boundary cases.
- createTableData: structural row-pairing invariants (1/2/3/4 events,
  invisible-cell padding for odd counts). Cell components mocked so the
  test is about the math, not the GameEventTableCell render tree.
- generateTimestampStrings: diff-based color-state classes (past/imminent/
  in-progress/upcoming).
- isWithinNotifyWindow: window math, disabled-event short-circuit, no
  upcoming-event short-circuit, exact-boundary inclusion.
Phase A of the planned shadcn rewrite. Sets up the foundation; no JSX
has been migrated yet (DaisyUI classes still litter the pages).

- Drop daisyui; add shadcn deps (Radix primitives, cva, clsx, tailwind-
  merge, lucide-react, tw-animate-css).
- styles/globals.css: replace daisyui plugin block with shadcn CSS vars
  for light + dark, @theme inline mapping, .dark class variant.
- Add components.json + lib/utils.ts (cn helper).
- Add @/* path alias in tsconfig + Vitest alias.
- Add primitive components/ui/{button,dialog,checkbox,label,select,
  slider,dropdown-menu,card,separator,scroll-area}.tsx.
- Drop components/index.js barrel — Turbopack didn't like .js
  re-exporting .tsx and threw an opaque "Element type is invalid"
  during render. Switch to direct default-exports throughout.
- Update one Vitest mock path that pointed at the barrel.

Build, typecheck, and 23/23 unit tests stay green.
Phase B. The four modals previously used a CSS-only checkbox-toggle
pattern that DaisyUI provided (an <input type=\"checkbox\"> + <label
htmlFor=...> trigger). Switch to controlled <Dialog> primitives so
we can drop daisyui entirely.

- AlarmConfigModal: rebuild with Dialog/Checkbox/Select/Slider/Label,
  two-column grid layout, sound preview on select-change and on slider
  commit. Volume slider now also updates Howler.volume in real time.
- MerchantConfigModal: same treatment, simpler layout.
- ChangeLogModal: Dialog + ScrollArea, structured Entry helper to dedupe
  the per-date list markup.
- GitHubModal: Dialog with semantic links.
- _app.tsx: hoist gh + changelog open state, pass setters to SideBar.
- SideBar: ghost <Button> triggers in place of <label htmlFor>; PayPal
  link uses asChild + <a>.
- alarms.tsx + merchants.tsx: own configOpen state; settings cog calls
  setConfigOpen(true).

Modals still use some legacy color/btn classes inside (cleanup happens
in the page-chrome and table-cell phases). Build/typecheck clean,
23/23 unit tests still pass.

Also drop deprecated tsconfig baseUrl (paths-only resolution is the
modern form in TS 5).
Phase C. Strips the DaisyUI navbar / select / btn / table-of-tables
scaffolding from the page headers and rebuilds with shadcn Button,
Select, plus plain Tailwind for layout.

NavBar:
- Single sticky-feel header with brand on the left, tabs (Alarms /
  Merchants) using bg-secondary for the active route, link cluster
  + lucide Languages icon + Select on the right.
- Drop the `select select-bordered` locale switcher; use react-i18next
  changeLanguage instead of Next router locale (Next 16 dropped the
  i18n routing config).
- Discord banner pulled out into its own simple bar above the header.

Alarms page:
- Replace the giant nested <table> chrome with grid grid-cols-
  [220px_1fr]: filter sidebar on the left, current/upcoming events
  on the right.
- Date controls become outline icon buttons + a card-style "current
  selection" button that shows month/day and the relative-time
  annotation underneath.
- Region select + dual-line current/server time block on the right;
  the active line is bold, the other is muted-foreground.
- Section title row uses a real <h2> and a Select for notify-mins.
- Filter sidebar entries are now real <button>s with active state via
  bg-secondary, count on the right in tabular-nums.
- Inner currentEventsTable / fullEventsTable still wrap GameEvent
  cells in <table>; those are Phase D's job.

Merchants page (header only):
- Same treatment for region/server selects, settings cog, and the
  current/server time pair. Schedule reference + merchant table
  body still use the old layout (Phase D).

Drop the `classnames` package import that was being used twice.
Build / typecheck / 23 unit tests still green.
Phase D. The dense table-cell layout with a permanently-visible
DaisyUI dropdown menu is gone; cells are now real cards with a real
controlled menu.

GameEventTableCell:
- Card layout: icon, title with item-level prefix, countdown on the
  right, time chips below.
- Disable menu moves to a per-card MoreVertical button via shadcn
  DropdownMenu. Items: Disable Once / 12h / Daily reset / Weekly
  reset, plus a destructive "Disable all future". Disabled events
  show a single Enable item instead.
- Repeated grand-prix events keep their group/show toggle but as the
  first dropdown item with a separator, not a parallel control flow.
- Extract disable handlers (disableOnce / disable12Hours /
  disableDailyReset / disableWeeklyReset / disableThreeWeeks /
  enable) so the JSX no longer has six near-identical 8-line inline
  closures.
- Drop the ~120 lines of dead bottom-of-file commented JSX.

MerchantTableCell:
- Card layout with bg-card / border / spacing. Spawned merchants get
  an amber accent (border + tinted bg in light/dark). Out-of-progress
  merchants fade to opacity-70.
- Replace classnames with cn; drop unused imports (Interval,
  merchantSchedules, the inactive mSchedules side-effect).
- Pull RARITY_TEXT, rarityFromId, goodItemRarityClass, iconURL,
  openLocationImage out as module-scope helpers.
- Item lists use a real ul with rarity-color text and 20x20 icons.
- Potential-spawns sidebar becomes a right-side column; hidden when
  hide-potential-spawns or merchant.spawned.

createTableData:
- generateTimestampStrings color tokens move off DaisyUI semantics
  (text-success / text-slate-400/25) onto Tailwind/shadcn-friendly
  colors (text-emerald-600 / text-muted-foreground/40).
- Padding cell loses the now-unneeded flex/space-x-4 chrome.

Updated 2 unit tests for the new color-class names. 23/23 still pass;
build + typecheck clean.
Phase E. Wraps up the shadcn rewrite.

Theme:
- Switch dark-mode plumbing from DaisyUI's data-theme="dark" + .dark
  class double-toggle to a single classList.toggle('dark', ...). The
  custom-variant in globals.css already keys off .dark, and shadcn
  CSS vars propagate through the component tree.
- One line each in pages/alarms.tsx and pages/merchants.tsx.

Polish:
- Drop dead buttonClick DOM manipulation in alarms.tsx — it added
  /removed btn-active classes that no longer exist; the active filter
  state is already driven by selectedEventType React state.
- Rebuild merchants page schedules section: replace the giant
  table-of-tables with a top action row (data-by SaintBot, last-updated
  timestamp, vote button) above a 3-column grid of bg-card schedule
  reference cards. Schedule 1/2/3 each show its rotation times in
  one column and merchant→location in another, with proper tabular-nums.
- Suppress hydration warning on the last-updated timestamp.
- Replace last legacy bg-stone-400 / dark:bg-base-200 / btn-outline
  classes with shadcn-friendly equivalents.
Addresses several issues from a fresh-data review:

- Dark-mode flash on reload: add an inline pre-hydration script in
  _document.tsx that reads localStorage.darkMode (or prefers-color-
  scheme as a fallback) and applies the .dark class before React paints.
- Dark-mode toggle from the settings modal didn't take effect until
  reload because each useLocalStorage instance was an independent
  state. Add a custom 'lostark:localStorage' window event that the
  setter dispatches and other instances on the same key listen to.
  Also subscribe to the native 'storage' event for cross-tab sync.
- Light-mode page background was pure white, leaving the cards/buttons
  flat. Drop --background to oklch(0.975) with a subtle blue tint;
  cards stay pure white so they layer visibly. Equivalent tweaks for
  --secondary, --muted, --accent, --border with the same tint.
- Chrome too constrained: bump max-w-7xl (1280px) → max-w-[1800px]
  on NavBar / alarms / merchants containers and trim the lg padding.
- Date arrows + settings cog feel small at 36x36; bump to 44x44
  (size-11) with size-5 icons inside.
- Filter sidebar was 220px with no narrow-screen story. On <lg the
  filter list now lays out as wrapping flex-row pills; on lg it goes
  back to a 260px column.
- Cells stacked side-by-side on every width. Now stack vertically on
  <md (single column for half/quarter-screen side-panel use) and pair
  side-by-side on >=md.
- Modal rows weren't fully clickable: wrap each Row's content in the
  <Label> directly with hover background, so the entire pill is the
  hit target.
- GameEventTableCell countdown showed -00:00:00 (yellow) for events
  with no upcoming occurrence today. Hide the countdown chip when
  latest() returns nothing.
- Missing i18n key (e.g. id 3025 -> "Sevek Atun") was rendering as
  the bare id. Fall back to the metadata name from data/events.json
  when t() returns the key unchanged.
Three follow-ups from the latest review:

- Light mode was visually flat — page bg too close to white made
  cards/dropdowns/modals indistinguishable from the page. Push the
  page bg darker (oklch 0.94 with a faint blue tint) and pull
  --card / --popover up to oklch 0.99 so they read as elevated
  surfaces. Tighten --secondary / --muted / --accent / --border
  values to give a clean three-level hierarchy: page → card → button
  surface. Dark mode unchanged.
- DialogContent was using bg-background; that's the page color, so
  modals matched the page in light mode. Switch to bg-card so the
  modal sits visibly on top of the page.
- Outline Button variant also used bg-background; switch to bg-card
  so outline buttons (date arrows, region/notify selects, schedule
  cards) stand out from the page.
- Sidebar icon column: bump button size from 36 to 48 (size-12) and
  icons from default to size-6. Now matches the visual weight of the
  date/settings buttons in the page header.
- The [?] hint next to "Group repeat events" was a bare span with a
  native title — easy to miss and not styled. Add the shadcn Tooltip
  primitive (components/ui/tooltip.tsx + @radix-ui/react-tooltip dep)
  and wrap the hint in a real tooltip trigger so hover/focus shows
  the explanation.
…ters

- Reinstall without --legacy-peer-deps. The flag was a holdover from
  the React-17 era when @testing-library/react v12 / next-i18next 16 /
  Next 12 had peer-dep collisions. Everything's modern now and
  resolves cleanly. Drop it from the CI workflow too.
- Filter sidebar buttons (Fever Time, Siege Content, ...) had a
  border but no fill, so they merged into the page bg. Add
  bg-card to the inactive state — keep bg-secondary for the selected
  filter so the active state still reads.
- SelectTrigger primitive shipped with bg-transparent which had the
  same problem on the notify-mins / region / language / server
  selects. Switch to bg-card with bg-accent on hover; transition the
  background-color too.
- Drop the leading "-" from event/merchant countdowns — the original
  intent was T-minus but it just reads as a negative number out of
  context.
- Event labels were rendering the i18n translation as primary, which
  picked up stale hand-curated strings from public/locales/en/events.json
  (e.g. "[Assail]Preigelli-[Siege]-[Raid]Liebertane" for id 9001 when
  the freshly scraped data/events.json says "[Siege] Preigelli VS.
  Liebertane"). Flip the precedence: prefer the metadata name from
  data/events.json. Fall back to i18n only when (a) metadata is missing,
  or (b) the user is on a non-English locale and a real translation
  exists. ZH overrides keep working via zh/events.json.
- Filter buttons looked like flat list rows — easy to miss as
  interactive. Now they read as cards: shadow-sm at rest, slight
  hover lift (translate-y, hover:shadow), active:scale-[0.99] press
  feedback, and the selected filter fills with primary color
  (bg-primary text-primary-foreground border-primary shadow-md) so
  the active state is unambiguous. Count chips invert their muted
  color for the active fill so they stay legible.
- The Disable Once / 12h / All-future menu items were no-ops for events
  whose latest() returned undefined — i.e. anything whose last
  occurrence already happened today. The handlers anchored their
  durations to next.end and short-circuited on null. Switch the anchor
  to (next?.end ?? DateTime.now()) so the action always shifts the
  user-visible window forward.
- Drop the DropdownMenuLabel I'd added with a non-existent i18n key
  (alarms:disable.until) — it was rendering as the raw key. The five
  Disable items are self-explanatory without a category header.
- Filter sidebar buttons sat 8px above the first event card because
  the cards have p-2 padding and the aside had none. Add lg:pt-2 to
  the aside to align top edges. Mobile (single column) keeps the
  tighter top.
- Merchants page now leads with a "Work in Progress" banner — amber
  badge + sentence explaining the upcoming crowdsourced-vote model.
  Pins the design intent for the next pass when we wire submissions.
- Event cards now show a "Disabled" pill next to the title when the
  alarm is suppressed, plus a relative-time hint ("· in 12 hours")
  derived from gameEvent.disabled. The opacity-50 dim was the only
  prior signal and was easy to miss.
- Settings cog on /alarms and /merchants moves from variant=ghost
  (transparent) to variant=secondary so it reads as a real button
  surface — distinct from the page bg but visibly less prominent
  than the date arrows / region select which stay on outline (bg-card).
- Drop the orphaned "Vote" link on /merchants. The WIP banner already
  pitches the upcoming crowdsourced flow; pointing users at a Discord
  vote link is a stale CTA from the old data flow.
pages/api/{hello,items,server-maintenance,regions/all,regions/[region]}.ts
were demo skeletons returning hardcoded fixtures. Nothing in the app
called them. Delete the whole pages/api/ directory.

common/{Item,Region,Server}.ts were only referenced by those routes;
remove them too. Drop the unused Item import in WanderingMerchant.ts.

Build / typecheck / 23 tests still green.
Radix's Dialog warns when DialogContent has neither a Description nor
aria-describedby={undefined}. Add an sr-only DialogDescription to each
of the four modals (AlarmConfigModal, MerchantConfigModal,
ChangeLogModal, GitHubModal) so the warning clears and screen readers
get a one-line summary of what each modal does.
The wss://ws.lostarktimer.app socket was opened on every /merchants
mount, triggered repeated reconnect attempts in dev (where
NEXT_PUBLIC_VERCEL_ENV is unset), and pushed empty data into the
table. With the page parked behind the WIP banner there's no reason
to keep the connection live.

- Drop the io / Socket imports and the two effects that handled
  socket lifecycle and per-server join.
- Drop merchantAPIData / dataLastRefreshed setters since nothing
  else writes them; merchantAPIData stays as an empty object so the
  filter/sort math still runs and the static merchant data continues
  to render.
- Drop the socket.io-client dep entirely. When crowdsourcing lands
  we'll bring back a different transport.
Phase 1.3. With the UI close to deployment, lock the golden paths and
the cross-component localStorage broadcast in tests so we don't
regress on the way to prod.

E2E (Playwright, 8 specs):
- /  redirects to /alarms
- header chrome (date arrows, settings, region) renders
- next/prev day buttons advance the date label
- region select switches and persists across reload
- filter buttons highlight (bg-primary) when selected
- settings modal opens, dark mode toggles immediately (without
  reload), modal closes
- /merchants WIP banner is visible
- /merchants settings modal opens

aria-label="Selected date" added to the date pill so the role-based
selector is stable.

Unit (Vitest, 9 specs for useLocalStorage):
- hydrates from existing localStorage
- returns initial value when empty
- lazy-evaluates initialValue function once
- persists on set
- removes the key when set undefined
- broadcasts to other consumers via the lostark:localStorage event
  (the regression that caused the dark-mode toggle to require a
  reload)
- ignores broadcasts on unrelated keys
- falls back gracefully on corrupt JSON
- reflects cross-tab native storage events

Total now 32 unit + 8 e2e passing.
The hardcoded region/server lists in data/regions.json and
util/static.ts predated the 2024 server-merge consolidation: they
listed five regions (US West / US East / EU Central / EU West /
South America) with dozens of long-dead servers and used fixed UTC
offsets that produced wrong times for half the year.

Replace with the current three-region topology, scraped from
Xeio/WanderLost (https://github.com/Xeio/WanderLost), the canonical
source for lostmerchants.com:

- NA East (America/New_York): Balthorr, Inanna, Luterra, Nineveh,
  Vairgrys
- NA West (America/Los_Angeles): Brelshaza, Thaemine
- EU Central (Europe/Warsaw): Arcturus, Elpon, Gienah, Ortuus, Ratik

IANA timezone names mean Luxon now handles DST automatically. This
fixes a long-standing bug where region times were 1 hour off during
DST transitions.

util/types/types.ts now derives RegionKey and ServerKey directly from
data/regions.json so the type system tracks the data, not a parallel
hand-curated union.

Add resolveRegion() in util/static.ts as a localStorage-migration
shim: existing users with stored regionTZName === 'US East' get
mapped to 'NA East', 'US West' to 'NA West', 'EU West' and
'South America' fall back to 'NA East' (closest live region).
Without this, returning users would see an empty region select.

Update the e2e spec to assert on the new labels.
- Move data/{data,events,msgs}.json -> public/data/ and switch the
  alarms page from module-level require() to a runtime fetch in a
  useEffect. The three files together are ~80 KB raw and were
  previously bundled into the alarms page chunk on every load. Now
  they're served as static assets, cached independently of the JS
  deploy, and don't bloat the initial JS download. Brief "Loading
  events…" state covers the fetch window.
- buildEventMapping / buildGroupedEvents / buildEventTypeMapping
  helpers replace the module-scope IIFEs so the same logic runs once
  the fetch resolves.
- Add an empty-state card when fullEventsTable is empty and data has
  loaded ("No events for May 12. The schedule is empty for this day…").
  Previously the page rendered a blank gap; users couldn't tell
  whether the data was missing or the day was just quiet.
- The category-filter variant of the empty-state mentions trying the
  "All" filter so users don't get stuck thinking the page is broken.
…mpty days\"

Putting data/{data,events,msgs}.json in /public exposes them at
https://lostarktimer.app/data/data.json — defeats the goal of keeping
the cleaned schedule from being trivially scraped. Move them back to
/data and restore the module-level require() in pages/alarms.tsx.
The JSON ends up bundled into the alarms page chunk again, but isn't
served at a stable URL.

Keep the empty-state card from that commit — it doesn't depend on the
data location and is a real UX win (was "blank page" for days with
no rotations, now a "No events for May 12…" card).
The full data/data.json (~76 KB raw, ~15 KB gzipped) was bundled into
the alarms page chunk and shipped on every visit even though only one
day's worth of events is ever rendered at a time.

Pre-build pass:
- scripts/build-day-data.mjs splits data/data.json into
  data/days/<month>-<day>.json files, each ~5–8 KB. Hooked into npm
  predev / prebuild so the chunks regenerate any time data.json
  changes.
- /data/days/ is gitignored — the files are always derivable.

Page change:
- pages/alarms.tsx loadDayData() does a templated dynamic import
  (import(\`../data/days/\${month}-\${day}.json\`)) so webpack emits one
  fingerprinted chunk per file at build time. The chunks aren't at a
  stable URL, so they're not trivially scrapable.
- The events-processing useEffect now operates on a per-day slice
  ({ type: <ilvl-or-list> }) instead of walking the full month/day
  tree. Source still uses two shapes at the bottom (dict for most
  types, list of single-key dicts for type 2) — handled with a small
  recursive visitor.
- Skip events whose source produced an invalid DateTime (NaN hour,
  malformed time string), and roll end forward a day when the source
  emits a window that crosses midnight (e.g. "23:00-01:00") so the
  resulting Interval is valid.

E2E + unit suites still pass.
The legacy +10-minute adjustment for sailing-style and certain island
event types (TYPE_IDS_INCREASE_BY_10_MIN / EVENT_IDS_INCREASE_BY_10_MIN)
was bumping start forward but reconstructing end from the source's
end-hour string. That worked when the source emitted single times
("2:00") because the end-hour fallback used start.hour after the bump.

normalize.py now canonicalises single times to degenerate ranges
("2:00" -> "2:00-2:00"), so endHr is non-empty and we no longer fall
back. start became 02:10, end stayed 02:00 — end before start. The
just-added spanning-midnight roll then bumped end forward a day,
producing 23h50m "windows" for every event, which is why the alarms
page was suddenly listing dozens of all-day events.

Match the original lostarktimer-utils/scraper.js behavior: apply the
+10 to both start and end so degenerate single-time ranges stay as
points and real ranges shift consistently. The spanning-midnight
roll stays as a defensive guard for legitimate cross-midnight windows.
Match the alarms page's existing convention — the imminent-events
table is wrapped in a ring-2 ring-amber-500/40 to make the active
group visually pop. Apply the same ring per-card on merchants while
inProgress(serverTime) is true (spawn window open). The pre-existing
amber bg/border for spawned merchants is preserved underneath; the
ring is the cross-cutting 'happening now' signal that works whether
or not a sighting has been reported.
Capture the load-bearing context that's currently only in the recent
session: stack, conventions, runtime data flow, why-we-chose-X, and
how to deploy / activate the scraper pipeline. Keeps future work
oriented without having to re-derive everything from git history.

- CLAUDE.md: agent-oriented project guide. Stack, layout, critical
  conventions (no public/data, IANA tz, +10 applied to both endpoints,
  useLocalStorage broadcast, class="dark" theme, controlled modals,
  required DialogDescription). Commands. Pointers to the scraper repo
  and WanderLost.
- docs/ARCHITECTURE.md: runtime data flow for /alarms with build-time
  per-day chunking, theme system, i18n, modal pattern, region/server
  topology table.
- docs/DECISIONS.md: log of non-obvious calls — modernize over rewrite,
  bundled vs public data, per-day chunks, react-i18next vs
  next-i18next, prefer metadata names over stale i18n, +10 applied
  to both endpoints, region migration shim, controlled modals,
  class="dark" only, merchants parked, dropped --legacy-peer-deps,
  no <Image> for tiny icons, scraper atomic-write resiliency,
  spanning-midnight roll.
- docs/DEPLOY.md: pre-flight, Vercel preview, scraper-pipeline
  activation (push + LOSTARK_ALARMS_TOKEN secret + manual run),
  watching prod, rollback.
- docs/DATA_PIPELINE.md: scrape→normalize→PR flow diagram, source
  description, why-private-scraper-repo, what normalize does and
  doesn't do, failure modes, how to add a new normalization rule.
What still has to happen, what's deliberately not done, and what's
parked. Surface the operational steps (push scraper, set
LOSTARK_ALARMS_TOKEN, deploy, optional Mongo/Supabase teardown) in
one file so the next session doesn't re-derive them from chat history.

Reference it as the first thing to read from CLAUDE.md.
should_deploy.sh was a 2023 Vercel "Ignored Build Step" override that's
no longer in use. sound.d.ts declared *.mp3 modules but no code does
import-from-mp3 — alarm sounds load via URL strings from public/sounds/.
Add /test-results/ + /playwright-report/ + .env.sentry-build-plugin to
.gitignore.
@sentry/nextjs for the new observability layer. jsdom was on "latest"
which had drifted to 29.x; pinning to ^26 prevents future jsdom releases
from re-breaking the Vitest suite.
Node 22+ ships an experimental localStorage on globalThis that shadows
jsdom's. Without --localstorage-file <path>, it's a degraded plain-Object
Storage with no clear/setItem methods. Vitest's populateGlobal doesn't
include localStorage in its hardcoded keys list, so when Node has a
builtin, jsdom's never reaches the test.

Force jsdom's localStorage and sessionStorage onto globalThis after env
init. Works on any Node version regardless of the builtin.

See docs/decisions/0018-vitest-jsdom-storage-shadow.md.
All three init configs use enabled: VERCEL_ENV === 'production' so the
SDK is loaded but inert outside Vercel prod. DSN comes from
NEXT_PUBLIC_SENTRY_DSN; org/project come from SENTRY_ORG/SENTRY_PROJECT
env vars (build plugin reads them by default).

Trims vs wizard defaults: tracesSampleRate 1 -> 0.1, removed enableLogs
(would forward every console.* to Sentry), removed automaticVercelMonitors
(no Vercel crons), enabled tunnelRoute '/monitoring' so ad-blockers don't
drop client error reports.

pages/_error.tsx is the Pages-Router boundary that forwards exceptions
via Sentry.captureUnderscoreErrorException.

See docs/decisions/0017-analytics-and-observability.md.
cwjoshuak added 5 commits May 7, 2026 00:14
…nents

Replaces the inline <Script> block in _app.tsx with two extracted
components in components/analytics/. UmamiScript reads the script URL +
website ID from NEXT_PUBLIC_UMAMI_SRC / NEXT_PUBLIC_UMAMI_WEBSITE_ID and
no-ops if either is unset. CloudflareAnalytics is gated on
NEXT_PUBLIC_VERCEL_ENV === 'production'.

GA was duplicative — Cloudflare Insights already covers traffic, Umami
covers product events. One fewer privacy footprint.
Skips Firebase + Mongo from the historical .env.local — both unused.
Last entry was 4/7/2022. Add a one-shot summary of the modernization
(region migration, redesign, dark-mode polish, faster alarm-page loads,
merchants page parked). Replace the stale "Planned" list with the only
thing actually planned: the Wandering Merchants rebuild.
Split the monolithic DECISIONS.md into one file per choice under
docs/decisions/, with a README index. Adds two new ADRs (0017 analytics
and observability stack, 0018 vitest jsdom Storage shadow workaround).

Sanitize for the public repo: drop the upstream source domain, the
private scraper repo path, the personal Umami host, and the
"data-as-value-add" / "not trivially scrapable" framing that effectively
mapped attack surface. Reframe ADRs 0003/0004 as architecture, not
security.

Delete docs/DATA_PIPELINE.md (moved out of this repo), docs/DECISIONS.md
(replaced by docs/decisions/), and docs/NEXT_SESSION.md (status snapshot
that's now expired). Update CLAUDE.md "See also" pointers and DEPLOY.md
to focus on the env-var setup that matters here.
components/analytics/track.ts defines a typed EventMap + track() that
no-ops when window.umami isn't loaded. Properties are chosen for
low-cardinality grouping; volume is bucketed at the call site so raw
floats don't pollute Umami.

Wired:
- alarms.tsx: region_change, date_change (prev/next/today), notify_minutes_change, alarm_triggered (per event in the notify window when the trigger condition fires)
- AlarmConfigModal.tsx: dark_mode, view_24hr, localized_time, hide_disabled, move_disabled_bottom, group_repeats (source='settings'), desktop_notifications (with permission state), alert_sound, volume_change (on commit), reset_disabled_alarms
- GameEventTableCell.tsx: event_disable (5 duration variants), event_enable, group_repeats (source='cell')
- SideBar.tsx: changelog_open, github_modal_open

Updated ADR 0017 with the full event taxonomy table.
@pull pull Bot locked and limited conversation to collaborators May 7, 2026
@pull pull Bot added the ⤵️ pull label May 7, 2026
@pull
pull Bot merged commit 53ebc09 into git-0xCat:main May 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant