Skip to content

Preview: native-product-pages integration (do not merge) - #1130

Draft
busbyk wants to merge 71 commits into
mainfrom
native-product-pages
Draft

Preview: native-product-pages integration (do not merge)#1130
busbyk wants to merge 71 commits into
mainfrom
native-product-pages

Conversation

@busbyk

@busbyk busbyk commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Standing draft PR to keep a Vercel preview of the native-product-pages integration branch live. Not for merge.

busbyk and others added 30 commits April 1, 2026 09:57
Port forecast/warning Zod schemas from AvyApp to web package with real API
fixtures from NWAC, SAC, and SNFAC. Covers forecasts, summaries (off-season),
null warnings, string-typed size transforms, and all media type variants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds fetchForecast(), fetchWarning(), and resolveZoneFromSlug() to the
NAC service. All functions apply the DVAC->NWAC center alias and use
5-minute ISR revalidation. Includes unit tests for zone resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port dangerName, dangerColor, dangerTextColor, dangerIconUrl from avy app.
Copy danger and problem icon PNGs to public/images/. Document cross-repo
color discrepancies in docs/nac-data-display.md for future alignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a per-center checkbox under a new "Features" tab in Settings,
defaulting to false. Includes a utility to read the flag by tenant slug,
seed data, migration, and regenerated types.

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

Pixel-perfect SVG triangle ported from AvyApp with verbatim path data.
Elevation band rows display label, colored bar, icon, and danger name.
DangerRating composes both into today + tomorrow outlook sections.

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

Pixel-perfect port of avy/components/DangerRose.tsx converted from react-native-svg
to standard web SVG. 24-sector rose (8 aspects x 3 elevations) with cardinal direction
labels. Active sectors highlighted based on AvalancheProblemLocation array prop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port SeverityNumberLine from AvyApp to web SVG. Two exported
components: LikelihoodSlider (single-value) and SizeSlider
(min/max range). Server component, no client JS.

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

Server components for forecast page text/HTML sections. Includes shared
HTML sanitization utility using isomorphic-dompurify with restrictive
allowlist. WarningBanner uses details/summary for progressive enhancement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Composite card displaying problem icon/name, locator rose, likelihood
and size sliders, sanitized discussion HTML, and media thumbnail.
Uses local problem icon assets mapped from AvalancheProblemName enum.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client components using shadcn Dialog + Carousel (Embla) for full-screen
media viewing with image, YouTube, and fallback support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Class-based error boundary that catches render errors in forecast sections
and displays a styled fallback message. Wrapping of individual sections
happens in page composition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Assembles all forecast components into a server-rendered page with
per-section error boundaries. Route checks useNativeForecasts feature
flag to toggle between native rendering and legacy widget.
generateMetadata enhanced with real zone name and bottom line in
native mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AllZonesForecast fetches all zone forecasts + warnings in parallel.
ZoneForecastCard renders compact cards reusing WarningBanner, ForecastHeader,
DangerRating, and BottomLine. Route checks useNativeForecasts flag to toggle
between native grid and legacy widget.

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

- Remove tenant slug prefix from zone links (middleware already rewrites)
- Parse HTML in elevation band labels and lightbox captions via sanitizeHtml
- Add /images/ to middleware exclusion so danger/problem icons load
- Add closeClassName prop to Dialog for visible lightbox close button
- Formatting fixes from prettier

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brings the completed native avalanche-forecast implementation forward from
origin/replace-widgets (14 additive commits; retired beads epic monorepo-9ha)
behind the per-tenant `useNativeForecasts` Settings flag. Issue 01 of the
native-product-pages PRD; gates issues 02-05 and 08.

Base: branched off origin/tooling-domain-context (matches current main; the
fallow dead-code suggestions are deferred to a later reconciliation pass).

Hand-merged conflicts (4):
- services/nac/nac.ts: union of main's getMapLayer/getForecastZoneDanger and
  RW's fetchForecast/fetchWarning; deduped DVAC mapping via normalizeCenterSlug.
- [zone]/page.tsx: native/widget branch gated on getUseNativeForecasts; kept
  main's revalidate=1800 + dynamic OG image and enriched og:description with the
  forecaster's bottom_line when native is on.
- all-zones page.tsx: native/widget branch; kept main's simplified NACWidget.
- migrations/index.ts: kept main's migrations, dropped RW's stale entry.

Migration: regenerated the Settings flag migration against current schema
(27.8k-line snapshot vs RW's stale 22.4k that predated 6 migrations landed since
the fork). Safe additive `ALTER TABLE settings ADD use_native_forecasts`.

Sanitization: the native forecast components sanitize HTML server-side. RW used
isomorphic-dompurify, but main had dropped it (its only consumer was a client
component on plain dompurify), and DOMPurify/jsdom does not survive Next's server
bundle. Switched sanitizeHtml to sanitize-html (pure JS, no jsdom) so it works in
both the server components and the one client consumer.

Verified: pnpm tsc / lint / test (528) / drift green; migrate + seed from scratch
succeed; native single-zone + all-zones pages server-render with live NAC data
(HTTP 200, no jsdom error), flag-off centers still serve the widget, flag defaults
off and toggles per tenant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…clickable cards, detail header

Iterating on the reconciled native forecast page:
- ForecastHeader: render as a plain block instead of its own Card (it was a
  card-in-card inside the all-zones zone card); relabel "Published" -> "Issued".
- Format issued/expires in the avalanche center's timezone (NAC metadata
  `timezone` field) via formatDateTime, instead of the server's UTC.
- All-zones ZoneForecastCard: make the whole card a single click target
  (stretched link to the zone detail page; warning banner kept interactive).
- NativeForecastPage: add a zone-name <h1> header with an "Avalanche Forecast" /
  "Seasonal Summary" subtitle (the off-season product is product_type=summary).

Note: NWAC currently returns a `summary` product (daily forecasts ended ~Apr 20,
expires Oct 31) - there is no daily forecast in the off-season, so the page shows
the seasonal statement (hazard_discussion) without danger ratings or problems.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reconciliation commit used --no-verify (to bypass the additive-ALTER
migration-safety warning), which also skipped lint-staged's prettier pass.
Apply that formatting so pnpm-lock.yaml and the new migration's JSON snapshot
match the repo's prettier-formatted convention and keep the branch diff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AFP product_type for off-season products is `summary`, but that is not a
user-facing label — NWAC titles the content itself (e.g. "2026 Spring Statement"
in the hazard_discussion). So only show the "Avalanche Forecast" subtitle for
actual `forecast` products; summary products carry their own heading in the
discussion. Avoids imposing terminology the AFP doesn't use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Forecast HTML comes from the NAC API, so any absolute (or protocol-relative)
link in it points off the AvyWeb tenant site. sanitizeHtml now adds
target="_blank" rel="noopener noreferrer" to those via sanitize-html's
transformTags, leaving relative links in the same tab. Adds a server test
covering external/relative link handling and tag stripping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native detail and all-zones pages used `mx-auto max-w-4xl px-4 sm:px-6`, which
is narrower with different padding than the rest of the site. The breadcrumbs, the
widget path in these same routes, and all content pages use the Tailwind
`container` class. Switch both native wrappers to `container space-y-6 py-6` so the
content width lines up with the header/breadcrumb and the legacy widget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a danger-colored calendar date picker to the native single-zone
forecast page so readers can browse any past forecast/statement for the
zone. Default (no date) shows the current product; picking a date renders
that historical product at a shareable, server-rendered dated URL
(/forecasts/avalanche/[zone]/[date]). Implements issue 09.

Data layer
- Product list schema + archiveDates: noon valid-date rule (center tz),
  dedupe same-date products to the latest publish, carry danger_rating.
- fetchProductArchive narrows server-side via date_start/date_end (the
  params avy uses; type/zone_id are ignored), fetches uncached + trims to
  a small slice, then caches via unstable_cache (the full archive is ~13MB
  for NWAC, over Next's 2MB fetch-cache limit).
- fetchProductById for the historical product (immutable, long cache);
  kept view-only, not issue 02's adapter.
- GET /api/[center]/forecast-archive?zone&from&to for lazy month loading.

Pages
- New on-demand dated route [zone]/[date] (immutable ISR, noindex).
- Extracted shared NativeForecastView; live + dated pages compose it.
- Live page anchors the picker window on the current product's date (not
  today) so off-season the calendar opens populated on that month.

Picker (ForecastDatePicker.client)
- shadcn Calendar in a popover; days colored by danger_rating via the
  shared dangerColor scale, no-product days disabled, current/selected day
  outlined; calendar lazy-loads older months' colors on demand.
- Days, prev/next arrows, and "Current forecast" are real Next <Link>s so
  the global nextjs-toploader handles navigation feedback (its bar only
  starts on anchor clicks, not router.push).

Verified live at desktop + 375px; tsc/lint/test (541) pass.
ADR 017 records the native forecast-glossary design: a national shared
GlossaryTerms collection, client-side marking fed by a cache-tagged
/api/glossary endpoint (decoupled from the forecast page cache), no
per-center gate, and a definition popover with a "Learn more" link.

Adds the "Forecast Glossary" cluster (Glossary Term, Glossary tooltip)
to DOMAIN_CONTEXT.md and indexes 017 (plus back-filled 015/016) in the
decisions README. Re-acknowledges the CLAUDE.md->README drift binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Issue 02 of native-product-pages: the data layer every native page consumes.

- Normalized model (services/nac/model/forecast.ts): API-shape-agnostic types
  the components depend on. Owns the top-level product types and reuses the leaf
  domain enums/types from the v2 wire schema. "No active warning" is plain null,
  not v2's null-object, so consumers stop special-casing the miss representation.
- Source adapter (services/nac/sources/): ForecastSource/WarningSource interfaces
  with a v2 implementation that maps the legacy response into the model. Pure,
  unit-tested v2 mappers. Components/pages fetch through the adapter, never a
  raw fetcher.
- Control 2 (data source): per-product v2/v3 selection via code/env config,
  uniform across tenants, defaulting to v2, with a per-center v3 canary allowlist.
- Control 1 (rollout): Settings.useNativeForecasts becomes a per-product
  nativeProducts:{forecast,warning} group; getNativeProductFlag(center,product)
  reader. Migration backfills both flags from the old value; seed updated.

tsc, lint, test (incl. v2->model mapper tests), and drift all green. Migration
verified on a dev.db copy (backfill preserves data before the column drop);
--no-verify only bypasses the ALTER/DROP review gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A concise, human-facing README covering the normalized model, the per-product
source adapter, the model-vs-wire dependency direction, and the two controls
(rollout vs data source), plus how to extend the layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relocate the data-layer architecture overview from src/services/nac/README.md
to docs/nac-data-layer.md (repo convention: docs live in docs/). Repo-root the
code paths and cross-link with nac-data-display.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One physical line per paragraph/bullet (no manual hard-wrapping).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bare force-static froze the all-zones grid (per-zone danger + bottom line)
at build time, which is unsafe for a daily forecast. Move the all-zones route
to ISR (revalidate=300) and lower the single-zone route from 1800 to 300 as a
short freshness backstop ahead of the revalidate-on-view path. The dated
archive route keeps its long immutable backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the afp danger section: today and tomorrow render as side-by-side
date-headed columns (each with its own triangle), per-band ratings use the
"{level} - {Name}" format (e.g. "4 - High"), and the section adds an
Elevation Band Descriptions disclosure plus the 1-5 danger-scale legend with
expandable travel-advice/likelihood/size definitions and a link to the full
NAC danger-scale explainer. Columns stack at <=375px. The compact all-zones
card passes no dates, so it keeps the stacked Today/Tomorrow layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Title each card "Problem #{rank}: {name}" and lay out the four labeled
columns the widget shows (Problem Type icon + name, Aspect/Elevation rose,
Likelihood, Size). Render the example photo (medium size) floated inline to
the right of the discussion with its sanitized italic caption, instead of a
full-width uncaptioned thumbnail below.

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

Lead with the Bottom Line above the danger rating to match the legacy widget,
head the avalanche problems with the "Avalanche Problems (N)" count, and feed
the published time + timezone into the danger section so today/tomorrow render
under their real valid dates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
busbyk and others added 3 commits July 1, 2026 15:50
Per docs/coding-guide.md, recreate the branch's migrations after merging main so
the .json snapshot reflects main's schema. Combines the two Settings-flag
migrations (add use_native_forecasts, then rename to native_products_forecast/
_warning) into a single 20260701_224854_native_products_flags that adds the two
native_products columns, with a snapshot including main's tables. Production
never had use_native_forecasts, so the intermediate add/rename/drop is dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the integration branch up to current main (125 commits), including
the native weather-station work (snowobs service, station tables/graphs,
CSV download, precip accumulations) and the blacksmith CI runners.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings current main (125 commits) into the native forecast stack. Notable
incoming work: native weather stations (snowobs service, station tables,
graphs via echarts, CSV download, precip accumulations), blacksmith CI
runners, conductor workspace tooling.

Conflict surface was a single drift.lock signature for the
docs/testing.md -> .env.example binding, which both sides had touched;
.env.example merged as the union (Control 2 data-source vars + SnowObs /
reCAPTCHA vars) and the binding was re-acknowledged.

No new migrations landed on main, so 20260701_224854_native_products_flags
remains main-inclusive and appends last in the migration index.

Gates: tsc, lint, 703 tests, drift:check all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
busbyk and others added 7 commits August 5, 2026 16:04
The merge resolution staged a placeholder signature before `drift link` was
run, so the merge commit captured the pre-link value and a fresh checkout
failed `pnpm drift:check`. This is the signature for the merged (union)
.env.example.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Native forecast pages — combined stack (reconcile + date picker + data layer + forecast page)
Replaces the legacy `warnings` widget on the home page with a native,
server-rendered banner for the center's active warning/watch/special
bulletins, behind the per-tenant `warning` native-product flag.

The warning source adapter is per-zone, so `centerWarnings` fans out
across the center's zones and groups the results by product type. Two
legacy behaviours are carried over deliberately, both erring toward
showing an alert rather than hiding one: every zone is queried (not just
active ones), and active-ness is the presence of a product rather than an
expiry comparison.

The home page is statically generated on an hour-long window, which an
alert issued after render would otherwise outlive. RevalidateOnView pings
/api/[center]/warning-freshness on mount; the handler compares upstream
against the shared cache and purges only on a server-authoritative change,
so freshness spam can't evict the cache. Failure handling is asymmetric --
an unreachable upstream, or a fresh set that has gone empty while the
cache holds alerts, never blanks a live banner.

Along the way:

- Gating moves out of the home page into HomeWarnings, where the center's
  upstream `warnings` capability gates the per-tenant flag.
- ForecastFreshness and the new warning equivalent collapse into one
  shared RevalidateOnView client, which the danger map can reuse.
- Drops the dead `resolveZoneFromSlug` re-export from nac.ts, which also
  breaks the nac.ts <-> resolveZone.ts import cycle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Setting NAC_HOST — e.g. pointing a machine at staging-api.avalanche.org,
which is the supported way to aim the native product pages at staging —
turned seven tests red. The msw handlers hardcoded the production hosts,
so a redirected request went unintercepted and failed.

Moves the two host constants into their own module and has both nac.ts
and the msw handlers read from it, so a suite intercepts whatever host
the code actually calls. The module has no Payload import, so a test can
resolve the hosts without mocking the config.

Also folds the repeated msw lifecycle trio into a shared helper, since
`onUnhandledRequest: 'error'` is what makes an unintercepted request fail
loudly and it should stay consistent across suites.

Verified green three ways: NAC_HOST set to staging, both hosts unset, and
NAC_HOST and AFP_HOST both pointed at staging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-zone banner on the forecast page rendered as a pale tinted
<details> while the legacy widget it replaces shows a solid red bar. On a
safety-critical page — and this is the surface a reader lands on from a
push notification or a shared link — the native version was materially
quieter than what it replaced.

Rebuilds it as a solid alert bar matching the widget: white alert
triangle, uppercase heading, the bottom line, and the validity window
behind a rule. It keeps <details>, so "Read more" expands the hazard
discussion in place rather than navigating away as the widget does, with
the long-form content on a neutral surface for readability.

The two native alert surfaces also disagreed on severity color — the zone
banner used red/orange/yellow, the center banner red/red/blue. Both now
share one presentation module on the legacy mapping, so the same product
type cannot look different depending on where a reader meets it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every failure path in nac.ts did `getPayload({ config })` then
`payload.logger.error(...)`. Where the logger was unavailable — a suite
that mocks getPayload, or a failure early enough that the config never
resolved — that threw a TypeError from inside the catch block, so the
caller saw "Cannot read properties of undefined (reading 'logger')"
instead of the real cause.

That is how a misdirected NAC_HOST presented: seven suites failed with a
logger TypeError that said nothing about the host being wrong. The cost
is not just confusing test output — in production the same path would
turn an upstream 500 into an unrelated crash inside error handling.

Routes all seven sites through a helper that treats logging as
best-effort and never throws. Adds a regression test that asserts the
NACError survives when the logger is unavailable; it fails against the
previous code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Native center-level avalanche warnings banner (issue 05, Part A)
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Migration Safety Check

Found 1 potential issue:

20260807_190252_native_danger_map_flag.ts

Warning (line 6): ALTER keyword detected - review for data loss

sql`ALTER TABLE \`settings\` ADD \`native_products_danger_map\` integer DEFAULT false;`,

Review these patterns and add backup/restore logic if needed. See docs/migration-safety.md for guidance.

busbyk and others added 6 commits August 11, 2026 10:49
The danger map's data is a Product in the ADR-018 sense, so it goes through a
source adapter like forecast and warning already do. Issue 02 scoped the adapter
to those two; this adds the third.

- `model/mapLayer.ts` — the normalized zone model consumers depend on, never a
  v2 or v3 response. Features carry a top-level `id` (the numeric zone id), which
  `mapLayerFeatureSchema` had been dropping; Mapbox's `setFeatureState` keys on it.
- `mapLayerSourceV2.ts` + mappers, wired through `sources/config.ts` so v3 is
  reachable by the Control 2 env flag. Defaults to v2, matching every other product.
- `getMapLayer` takes an optional `day`, which the NAC API has always supported and
  we never passed. Cheap now, expensive to retrofit onto issue 09's date picker.
- `getForecastZoneDanger` moves from `nac.ts` to `dangerMap/mapLayer.ts` alongside
  it; the OG route follows the import.
- `zoneSlug.ts` resolves a zone's upstream link to its AvyWeb path, so the popup,
  the zone list and the click handler can't disagree about where a zone goes.

Also drops four `z.infer` aliases and one schema with no consumers anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything the map draws is decided here, in pure functions, and tested — the map
component only renders the result. The rules are where the bugs are: off-season
outranking a stale rating, an unrated zone ignoring the server's grey, naive
timestamps that are really UTC.

Parity target is the afp danger-map widget, not avy. NAC has built this twice and
the two disagree — off-season and no-rating colors are *swapped* between them,
opacity and popup copy differ. AvyWeb sits on the same sites as these embeds, so
matching the embed is what a reader actually notices.

`dangerMapSettings` reads the full `widget_config.danger_map` contract that
dashboard-v2 writes, rather than a subset: every tenant has set `saturation`, SAC
has `search: false`, and `allCenters` is present on SNFAC/SAC but absent on NWAC.
Two departures from the stored config, both recorded in the parity ledger:
saturation is not applied (no Mapbox consumer in the NAC stack applies it, and the
shared style is already near-grayscale), and the configured zoom is used minus one,
because those values were authored against the Google Maps build and Mapbox's 512px
tiles render the same number a level tighter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The widget titles the legend "Avalanche Danger Scale" and makes that heading the
disclosure trigger. We had a "Danger Scale" heading plus a separate "Danger scale
definitions" toggle underneath — extra wording the widget doesn't have, and a
second control competing for the same job.

The heading and the color strip both move inside `<summary>` so the strip stays
visible when the definitions are collapsed. Each scale row gains an abbreviation
("Mod", "Cons", "Extr") for the narrow-screen strip, where the full names collide —
the same shortening the legacy legend does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Establishes Mapbox in web — this is the repo's first map dependency — as shared
infrastructure rather than danger-map-local, so issue 16's station map reuses it.
Style is the `avalanche-org` "AFP Custom" style already shared by afp-public-widgets
and dashboard-v2; the token is NAC's public `pk.` token, which both of those
hardcode as a fallback.

Rendering knowingly diverges from both existing NAC implementations. avy renders
one ShapeSource per zone and the widget does the same with `zone-source-${index}`;
this uses a single `geojson` source with data-driven paint, so hover and the
warning flash are `setFeatureState` + `feature-state` expressions rather than
`setPaintProperty` across per-zone layer ids. Zones arrive pre-styled from
`decorateZoneFeatures`, so the paint expressions just read computed properties and
there is no second copy of the precedence rules to drift.

The map is a client component that fetches on mount through `/api/[center]/danger-map`,
not props from the statically generated home page. The host page renders on a
one-hour window and a map painted from hour-old ratings is exactly the staleness the
widget avoided by refetching every page load. Routing the fetch through our own
endpoint keeps the adapter, the zod validation and the styling rules server-side.

The warning flash is rebuilt on requestAnimationFrame at the widget's ~20fps rather
than its 50ms setInterval, so it pauses with the tab instead of burning a timer.

A Mapbox map is a `<canvas>`, so `ZoneList` renders the same zones as a visually
hidden list of links — otherwise today's avalanche danger would be unreachable by
keyboard or screen reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `dangerMap` to the native product flags and swaps the legacy danger-map widget
for the native one on the home page. Ships dark: the flag is false for every tenant,
so untick and the widget comes back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`fallow` has been the only red CI job on the integration PR (#1130), and it is
wired into the pre-commit hook, so it has been blocking every commit on this
branch — 05a and 05b both went in with --no-verify. This is remediation of
pre-existing debt across issues 01-05, not a fix for any single issue.

Both gates now exit 0, and the audit reports no issues in 166 changed files.

Tell the tool about deliberate public surfaces
  `model/forecast.ts` exists so the model is the single import surface for
  consumers, so its re-exports have no consumers of their own and read as dead,
  transitively taking their sources in `types/forecastSchemas.ts` with them.
  Both are declared in `ignoreExports`, the same treatment `components/ui/**`
  already gets. This clears 23 of 39 dead-code findings on its own.

  ADR 018 was cited by `model/forecast.ts` but had never actually been written;
  it is written here, since it is the justification for that config.

Delete what was genuinely dead
  `getUseNativeForecasts.ts` (every call site had migrated to
  `getNativeProductFlag`), the unconsumed `getProductDataSource` re-export in
  `sources/index.ts`, and the `ProductListItem` alias.

Suppress the z.nativeEnum false positives
  Members of `AvalancheProblemType`, `ProductStatus` and `ExternalMediaType`
  reached only through `z.nativeEnum` look unused because zod validates against
  member *values*. They are load-bearing — dropping `WindSlab` would make zod
  reject every forecast carrying a wind-slab problem — so they carry per-member
  suppressions with a docblock, matching `AvalancheCenterType` in schemas.ts.

Waive the two dynamic-segment conflicts
  Same false positive three sibling routes already waive: route groups keep
  `(payload)/api/[...slug]` and `api/[center]` separate.

Bring the 15 gated complexity findings to zero
  Mostly by extracting the pure decisions out of components into tested modules,
  which drops the parent's branch count and covers the logic that moved:
  `mediaItem.ts`, `datePickerNavigation.ts`, `dangerRatingLayout.ts`,
  `zoneCardDanger.ts`, plus `parseArchiveWindowQuery` in `archiveDates.ts`.
  The JSX-heavy components (DangerScale, AvalancheProblemCard, the date picker,
  NativeForecastView) are split into named sections. 63 new tests.

  `NativeForecastView` and `ZoneForecastCard` each had their own copy of the
  highest-danger-band lookup; both now use `zoneCardDanger.ts`.

Collapse the 6 gated duplication groups
  `centerRoutePage.ts` holds the per-center route preamble the built-in pages
  shared verbatim — static params, metadata shape, and the platform guard.
  `ZoomPanSurface` holds the zoom/pan interaction the Gallery block and the
  forecast lightbox had duplicated.

Not addressed: the 2 circular dependencies through the Payload config barrel.
They are `warn`, gate nothing, and are down from 27 at the July baseline. The
only fix that actually clears them is injecting `payload` across ~26 call sites
— fallow counts a lazy `await import()` as a graph edge too, so that shortcut
does not work. Left as-is deliberately.

The baselines are untouched: nothing here was accepted as debt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
busbyk and others added 6 commits August 23, 2026 12:57
Clear the fallow gate on native-product-pages
Describes how AvyWeb fetches, caches and renders National Avalanche
Center product data natively: the normalized model and per-product source
adapters, the four control axes (two ours, two the AFP's), the
rendering/caching strategy, the revalidate-on-view freshness path, and
the Product/View separation that layout variants will ride on.

Lands on this feature branch rather than main because the code it
describes is here - and because drift can only bind a doc to files that
exist on the branch. Bound to the model, the source adapters and their
config, the forecast fingerprint, the freshness route and the Settings
collection, so the doc gets flagged when any of them change.

Written against the code as built rather than as proposed. Notable
things it records that the planning docs got wrong or omitted:

- The freshness endpoint makes two independent decisions - whether to
  purge the shared cache (server-side comparison only) and whether to
  refresh this viewer (the caller's header). The header cannot drive the
  purge because the endpoint is unauthenticated and trusting it would let
  anyone amplify load onto the AFP.
- Warning expiry is enforced by the API, not the client. Documented under
  sharp edges with the reasoning, because the previous framing read as a
  stale-banner risk and invited "fixing" it with a client-side check that
  would double-filter.
- getWeatherSource resolves off the forecast selection, so a forecast
  canary silently drags weather with it.
- SNFAC forecasts before 2020-05-01 carry no weather_product_id.
- EWYAIX and SOAIX return no config object at all.

Also ignores .docx exports; the scope and feature-inventory document
lives in Google Drive and only this doc belongs in the repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Added alongside the architecture doc in #1224 to keep local Google Drive
exports out of commits. Wrong scope: those exports are one person's
working copies, not something every contributor needs ignored, and a
blanket *.docx rule would silently swallow a .docx someone legitimately
wanted to commit.

Local exports belong in .git/info/exclude, which is per-clone and never
committed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Catches the integration branch up on 59 commits of main.

Conflict resolutions:

- `src/services/nac/nac.ts` — both sides had independently added a
  `noStore` option; main also refactored the request init into a
  `fetchInit()` helper and fixed `revalidate` from `24 * 60 * 60 * 1000`
  (milliseconds) to seconds, which is what Next expects. Took main's
  version and kept this branch's more specific comment on `noStore`.
- `drift.lock` — disjoint binding blocks on each side, kept both in
  sorted order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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