Skip to content

Native forecast page: reconcile from replace-widgets + initial polish#1121

Draft
busbyk wants to merge 20 commits into
native-product-pagesfrom
native-forecast-reconcile
Draft

Native forecast page: reconcile from replace-widgets + initial polish#1121
busbyk wants to merge 20 commits into
native-product-pagesfrom
native-forecast-reconcile

Conversation

@busbyk

@busbyk busbyk commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds native Next.js avalanche-forecast pages (single-zone + all-zones grid) behind the per-tenant useNativeForecasts Settings flag: a center that enables it renders native server components; with it off, the routes render the legacy NAC widget exactly as before (instant per-center rollback).

The page was built earlier on the unmerged replace-widgets branch (14 commits ahead / ~420 behind main); this PR reconciles that work onto current main, regenerates the DB migration, and adds review polish.

Why this targets native-product-pages, not main: native-product-pages is a long-lived integration branch for this multi-PR feature — each slice (data adapter, forecast freshness, observations, warnings, E2E…) lands as its own small PR into it for incremental review and a single shared feature preview, and the whole branch merges to main as a unit when the feature is complete. This is the foundational first slice.

Related Issues

First slice of the native-product-pages feature. Subsequent slices (data-layer adapter, forecast freshness, historical date picker, observations, warnings, E2E) land as their own PRs into native-product-pages.

No linked GitHub issues yet. I just have this locally on my machine. It probably makes sense for me to convert these to GH issues once the tooling PRs (#1117, #1118, #1119) land. So I'll plan to do that once those are merged.

Key Changes

  • Flag gating — both forecast routes branch on getUseNativeForecasts(center); the widget path is untouched for flag-off centers (the main no-regression check).
  • Settings migration — regenerated against the current schema (the brought-forward branch's snapshot predated recent migrations). Additive column; details below.
  • Sanitizer swap isomorphic-dompurifysanitize-html — forecast HTML is sanitized in server components, and DOMPurify needs jsdom, which breaks Next's server bundle at runtime. sanitize-html is pure-JS. External (other-domain) links now open in a new tab with rel="noopener noreferrer".
  • 4 merge-conflict filesservices/nac/nac.ts, the two forecast routes, migrations/index.ts; all additive, keeping main's newer route config (ISR + OG image).
  • UI — site-standard container width, flattened + fully-clickable zone cards, center-timezone dates (from NAC metadata), "Issued" labels.

How to test

  1. pnpm seed && pnpm dev
  2. Admin → Settings → Features → enable useNativeForecasts for a center (e.g. NWAC).
  3. /forecasts/avalanche and /forecasts/avalanche/<zone> render native; a flag-off center still shows the widget.
  4. NWAC is off-season, so its current product is a seasonal summary (statement) — danger ratings/problems are intentionally absent.

Screenshots / Demo video

TBD

Migration Explanation

..._add_use_native_forecasts_to_settings: additive ALTER TABLE settings ADD use_native_forecasts integer DEFAULT false (down: DROP COLUMN). No table recreation, no PRAGMA foreign_keys=OFF; migrate:check flags only the generic ALTER notice. migrate + seed from scratch verified.

Future enhancements / Questions

Forecast freshness (revalidate-on-view), a historical date picker, and richer off-season presentation are tracked as separate slices, not in this PR. GH issues coming soon as mentioned above.

busbyk and others added 20 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>
@github-actions

Copy link
Copy Markdown
Contributor

Preview deployment: https://native-forecast-reconcile.preview.avy-fx.org

@busbyk busbyk marked this pull request as draft June 17, 2026 23:28
@busbyk busbyk requested a review from rchlfryn June 17, 2026 23:28
@busbyk

busbyk commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

@rchlfryn intentionally leaving as draft. I'm not ready to merge this but requested your review for awareness of my approach. Feel free to leave any high-level comments or wait for the tooling PRs to land so I can create the GH issues I have locally and then you can see the whole picture of my plan.

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