Issue: WizardFactory/TodayWeather#2558. Initial implementation prepared on 2026-09-24; updated to a static-only client. The full mobile parity release remains open; this is an executable integration candidate, not a deployed or production-approved release. Behavior below includes the PWA audit fixes on this branch (webapp-pwa-audit-fixes, based on b8a3c504, 2026-09-26).
Use Node >=22.12 (verified with 22.22.2) from the repository root. These npm workspaces are independent of the historical client/ and server/ packages.
npm ci --ignore-scripts
npm run devOpen http://127.0.0.1:5173. The default is direct/live: the browser calls the existing public API and the launcher starts only Vite. Set a loopback WEB_ORIGIN for another development host/port. For explicitly labelled synthetic data use VITE_WEB_MODE=demo npm run dev; changing cities does not make demo fixtures real observations.
# Build only the static client. No API process is needed.
npm run build
npm startOpen http://127.0.0.1:4174 to exercise the production bundle, service worker and snapshots. Live failures never substitute demo responses; a previously received snapshot may be shown with a disconnected/stale notice for up to 24 hours. The preview is a local file server, not a production requirement.
Deploy web/dist to app.todayweather.ai using the S3 + CloudFront runbook: Option A reuses the existing distribution and bucket; Option B uses the template with a planned alias cutover. Issue #2646 prepares reuse of the existing hosting; production execution remains separately authorized. No AWS resources or DNS were created by this implementation.
npm run typecheck
npm test
npm run build
npx playwright install chromium
npm run test:e2e
# Unmocked, read-only smoke against the public API (local preview of web/dist)
node scripts/web-live-smoke.mjs --json test-results/web-live-smoke.jsonBrowser tests own a static-only server on port 4174 and intercept external API reads with synthetic raw provider fixtures. Their global setup stops the run when web/dist is older than web/src, web/public, web/index.html, web/vite.config.ts, scripts/web-precache.mjs, any web/.env* file or the core sources; rebuild first (WEB_E2E_ALLOW_STALE_DIST=1 skips the check for deliberate runs against an older build). They assert that the browser makes no /api/ requests, and a context-level fallback aborts any public API request that no test route handles, so a test that removes its page routes still cannot reach the real API; they do not prove live provider freshness. Set PLAYWRIGHT_EXECUTABLE_PATH for a preinstalled browser. CI also publishes web-static-dist, retained for 90 days. The live smoke is the only check that calls the real API; it runs daily and on manual dispatch in the Web live smoke workflow, never on pull requests, and after a deployment with --base https://app.todayweather.ai.
Only direct transport is supported. Omit VITE_WEB_TRANSPORT or set it to direct; proxy is rejected. VITE_WEB_MODE selects live or labelled demo data at build time; a live build contains no demo fixtures. The UI speaks Korean, English, Spanish, Japanese, German, Portuguese and French (web/src/i18n/): the browser language picks the first supported one (else English), Settings can override it, and API requests send the selected language as Accept-Language. The former web API workspace, notification sender and Docker deployment recipe have been removed. Node runs build/development/preview tools only.
| Mobile capability | Web implementation | Remaining release work |
|---|---|---|
| Startup and saved locations | Explicit search/current-location consent, catalog + live geocode, 30 favorites, delete/restore, start screen. Enter selects an exact name/ID or a unique name prefix, otherwise resolves the text as an address | OS permission matrix and non-Korean geocode/provider checks; no current-location disable toggle (see gaps) |
| Current/hourly weather | Current values, yesterday delta, merged KMA one-hour/three-hour forecast, KMA precipitation shown as observed amounts for the past and server-calculated forecast amounts ahead (D45), forecast publication time, hourly chart and accessible numeric table | Upstream missing yesterday values remain unavailable |
| Daily/combined weather | Yesterday and future daily rows, KMA past-day observed accumulation and server-calculated daily forecast amounts from today, AM/PM icons, min/max range, overview | Confirm every legacy ancillary metric/provider format |
| Air details | Seven pollutants, station selection, hourly/daily series, independent observation timestamps; text-only provider summary with unknown-time caveat when station data is absent; /air/:id?pollutant= preselects a pollutant |
Live air availability and all four provider standards |
| Nationwide weather/air | Schematic regional map and complete numeric lists; weather/pollutant tabs; air values with units; missing values show — without a unit |
Repair stale upstream nationwide air feed before release |
| Special weather reports | Structured bulletins, offset-qualified KR publication times in KST and official links | Provider freshness and active-warning device checks |
| Preferences | Six unit families, four themes applied before first paint by an early <head> script (theme.js, which also sets theme-color), startup/refresh, local backup/import; import deletes stored weather of removed places |
Photo theme uses a sky color treatment, not native photo packs |
| Notifications | Unavailable guidance recommending the existing mobile app, with a return-to-weather link; no permission request, subscription or scheduling form | Web Push and conditional rain/snow/air rules remain a separately scoped future feature |
| Sharing/install/help | Catalog-only share links, install help, privacy/storage/provider guidance, units help (/help#units), PWA manifest and icons |
HTTPS/iOS home-screen install and all supported device checks; no share preview; privacy policy and terms links deferred; upstream English error text kept (D50) |
| Store purchases/widgets | Honest web availability/price and native-feature guidance | No native purchase migration, billing, native widgets/watch or background tracking |
Four settings subpages are consolidated into /settings; weather/air overview is available in the same product. Supported interfaces preserve the user actions, not the old Cordova navigation structure. The paid tier is not represented as implemented.
Accessibility and time presentation: the closed mobile menu is inert and out of the tab order; opening it takes focus; Escape, the close button or the backdrop return focus to the menu button (aria-expanded); widening the window past 680 px closes it. View, pollutant and nationwide tabs expose aria-pressed, and toasts use a persistent #toast-region live region. Focus moves to main after a route change, after the menu has closed and inert is removed when the link was in the menu; Hourly/Daily routes scroll to and focus the selected chart after data renders, including direct load and history. Without renderable data, loading/error routes focus the main content; retry success then focuses the selected chart. The shell publishes its content-inert state; weather focus waits for the non-inert commit and observes menu closure explicitly, rather than relying on animation-frame timing. Only pending route/data focus requests are resumed; closing the menu without navigation keeps the menu-button return focus. Other view switches may retain their control; other route/history changes announce main. KMA/AirKorea times are labelled KST and overseas times "(현지 시각)"; last-receipt times in notices are shown in KST. Staleness notices compare KST times exactly (3 hours for observations, 24 hours for forecast publication and warnings); overseas times use a date-only rule counted from the end of that date at UTC−12, so a fresh reading is never flagged (details). A CORS-readable 429 or 503 is not retried: 429 shows a rate-limit message with the Retry-After wait only when the API exposes that header (otherwise a generic wait, assumed to be 60 seconds), and 503 shows a generic temporary-outage message. With a stored snapshot, a 429 shows the snapshot with the rate-limit notice and does not request again automatically (focus, reconnect, remount) before the retry time; the refresh button still requests immediately. A 429 or 503 without CORS headers looks like a network failure and gets the single network retry (request flow).
The specification is a parity ledger; these targets differ from the current client.
| Specification item | Current client | Status |
|---|---|---|
| Mobile bottom navigation Hourly / Daily / Air / Locations / More (layout) | Off-canvas sidebar menu plus in-page view tabs | Not implemented; deferred |
| S02 "disable current location" | The current-location entry can only be deleted or replaced by a new fix | Not implemented; deferred |
| Share: "confirm approximate region preview" and share flow step 1 | Shares or copies the catalog city link immediately; only curated city IDs, never coordinates | Preview not implemented; deferred |
| Locale: translation catalog and browser-locale override | Seven-language web catalogs (web/src/i18n/), browser detection with a settings override; the mobile catalogs served as a terminology glossary, not as the key set |
Implemented; translations are agent-authored and cross-model reviewed, native review pending (see review list) |
S07/S08 /settings/units, /settings/:section |
One /settings page |
Adapted |
| Measurement of fresh-data success, failed searches, source age, API errors (acceptance) | No telemetry | Not implemented; see release gaps |
| Proposed performance targets (cached content within 1 s, p75 LCP ≤ 2.5 s, API success ≥ 99%), 200% zoom and a WCAG 2.2 AA review (acceptance) | Not measured or reviewed. The reference screen sizes were reviewed in emulation on 2026-09-27 (see the phone layout row); CI covers 320 px phone checks (web/e2e/mobile.spec.ts) and 390, 1024 and 1440 px for specific layout and menu behaviours |
Not verified; release gate |
| Supported-browser baseline (Safari/iOS Safari, Edge, Firefox) | CI browser tests run in Chromium only. The layout check (npm run test:layout, test guide) runs every reference size in Chrome and Safari (WebKit) and desktop sizes also in Edge, in all seven languages: 7,020 views with no finding on 2026-09-27 after Safari-specific fixes (page-drawn selects clip their text, the station picker shrinks, section headers wrap, compound words break where the engine has no hyphenation dictionary). Offline shell, snapshots and updates also worked in WebKit with a real origin outage |
Real Safari/iOS and Android devices, home-screen installs, notch safe areas and Firefox not verified; release gate |
| Rate limited: "retry-after where supplied" | 429 shows the Retry-After wait when the API exposes the header (Access-Control-Expose-Headers: Retry-After), otherwise a generic wait of 60 seconds; a stored snapshot is kept until then; 503 shows a generic outage message; neither is retried when CORS-readable (one delayed retry only on 502/network failure) |
Implemented; the live API responses observed on 2026-09-26 did not expose Retry-After (backend follow-up) |
S05 /air/:locationId?pollutant=... |
Query preselects the pollutant | Implemented |
| S14 help "units" instructions | Help units section | Implemented |
See implemented architecture and its interactive diagram. The technical design and hosting diagram use the same static-only boundary. The default runtime is a static PWA on private S3/CloudFront with direct calls to the existing public API. It requires no new persistent Node service or API Lambda.
packages/weather-core/: source-aware, unit-aware normalization without mutating provider payloads. KMA-50temperatures and negative nonnegative metrics become unavailable; valid zero remains zero. All supplied daily rows are retained for the shared daily chart/table. KMA rain is marked as observed (rn1: current 1 hour, past short 3 hours, past-day accumulation), approximate (shortest 1-hour category lower bound) or server-calculated forecast (r06/s06: 3-hour short rows, daily totals from today) per D45. Zero snowfall stays in data but is hidden in the UI, while positive values below 0.1 mm (0.01 in) display as<0.1(<0.01).web/: React/TypeScript UI, versioned local preferences, IndexedDB snapshots keyed by location and all units (age pruning, deletion and the 30-entry bound read keys and values in one transaction), query cancellation and explicit stale/demo notices. A snapshot-backed result is stale at once and refetches on focus or reconnect, except a rate-limited snapshot still inside itsRetry-Afterwait. Normalization cache-key revision v4 separates new weather query/snapshot keys from older normalized values; favorites and settings are preserved. Snapshots are shown first while refreshing, deleted with their place and pruned after 24 hours. Coordinates are rounded to three decimals; sharing uses a curated public city ID rather than exact current location.web/src/direct-api.ts: fixed public API operations, bounded reads, JSON/schema checks, canonical physical units and local normalization; local catalog and capabilities. Network/CORS failures receive localized retry guidance without guessing their cause; a readable 429 raises a rate-limit error carrying theRetry-Afterwait (60 seconds when unreadable) and a 503 a generic outage message; cancellation and timeouts retain their own handling. The overseas catalog remains available, but error messaging cannot repair upstream availability.web/src/demo/: browser-owned synthetic raw fixtures, selected only by explicit demo mode and bundled only in demo builds.infra/web/static/: private S3/OAC, CloudFront/TLS/DNS template, route function and guarded dry-run uploader whose preflight names each destination mismatch and rejects error-to-page mappings. Issue #2646 prepares existing-hosting reuse; AWS execution remains separately authorized.scripts/web-live-smoke.mjsand.github/workflows/web-live-smoke.yml: unmocked read-only smoke of the static app and public API, before deployment (local preview) or after it (--base).
The service worker caches the shell and hashed assets, not API responses. Activation is user initiated for updates; only the newest complete previous shell cache is retained for existing tabs, and incomplete caches are deleted unless another worker is still installing. The app checks for a new worker hourly and when the page becomes visible. Only the tab whose user pressed "업데이트" reloads; other open tabs drop their now-stale "새 버전이 준비됐습니다" offer and show "새 버전이 적용됐습니다" with a "새로고침" button. Pressing "업데이트" when no worker is waiting only hides the offer and records no reload request, a request left over from before a page load is cleared, and the next controller change consumes the request even in a page that had no controller yet, so a later release never reloads a tab its user did not update. Browser snapshots are separate and expire after 24 hours. Maps and warnings require a network connection. Losing browser storage loses local favorites and snapshots.
Bounded public reads on 2026-09-24 returned HTTP 200 for health, domestic coordinate weather, reverse geocode, national data and warnings. Raw probe receipts are local, ignored SDLC records. A successful response does not establish data quality:
- Seoul's KMA current temperature was 25.2 C, while its yesterday temperature used
-50; the upstream summary reported an invalid +75 degree comparison. The client computes comparisons from validated values instead. - Daily data mixed April 2025 rows with September 2026 rows. The adapter excluded historical rows from the future forecast.
- Nationwide air included a 2021 observation timestamp; it is visibly stale. Domestic weather did not include an air station list. On 2026-09-29 Seoul weather carried current air from the air provider chain (#2628, source
google), and the nationwide air list was empty (the page shows an empty state).
No provider freshness repair, legacy server mutation, database migration or mobile release has been performed. A public release with maximum parity must resolve these upstream and device gates, plus the explicit feature gaps above; keep #2558 open.
These gaps remain before a public release. They are recorded, not solved, by this branch.
| Gap | What exists now | What is missing / owner |
|---|---|---|
| Deployment execution | Option A existing-hosting reuse, dry-run uploader with preflight; Option B is a separate migration alternative | Production authorization, verified operator resource inputs, complete release and rollback artifacts, and final-domain checks remain required |
| Option A configuration checks | The runbook lists read-only checks for the cache policy, viewer certificate, DNS and custom error responses; the preflight enforces alias, S3 origin, OAC, HTTPS redirect, empty OriginPath, no ResponsePagePath, LIVE route function and CSP connect-src |
Cache policy, certificate and DNS are manual operator checks; they have not been run against the live distribution for this branch |
| Staging | Pre-deploy: npm run build, the local static preview and the live smoke without --base exercise the real API with local hosting |
No isolated pre-production host: release.json siteOrigin and the uploader's SITE_DOMAIN are fixed to app.todayweather.ai, and Option B cannot use that alias before cutover. The first real-domain verification of each release therefore happens in production, mitigated by the dry run, preflight, post-deploy live smoke and rollback. A staging parameter is outside the current production-domain preparation |
| Monitoring and rollback triggers | The daily Web live smoke workflow checks the public API contract and the current build on a local preview; a manual dispatch with base_url checks the deployed site. Besides failures (including a service worker that does not install or control the page, a precache entry that does not return 200, or an offline deep link the cached shell does not answer), it warns when the Seoul observation is older than 3 hours or the forecast publication older than 24 hours, the main sign of a stalled domestic collector. Each run's job summary lists every failure and separates new warnings from the chronic ones named in the script's KNOWN_WARNINGS (an entry can also require matching evidence, so the same check failing for another reason shows as new; the console log marks (known) by check name only); the JSON result marks each warning known or new. Summary text is Markdown-escaped and URLs in upstream body snippets are replaced with [url]. The runbook names AK (or the operator AK names) as the owner and gives the triage per failure category |
No alerting on smoke failure or new warnings: a warning-only run succeeds, so a new warning is visible only in the job summary; the workflow has no notification step, GitHub emails scheduled failures only to the user who last edited the cron line, and it disables the schedule after 60 days without repository activity. No client telemetry (none by design so far), no CloudFront access logging or metrics in the template, no API-side error-rate source, and no agreed wrong-city/unit/date, source-age, error-rate or upstream-load rollback triggers. Operations and AK must choose these before public exposure |
| Release identity and archive | release.json carries commit (with -dirty for a local build of uncommitted changes, which the uploader refuses to --execute) and builtAt; the dry run and --execute print them; the live smoke's --expect-commit (workflow input expect_commit) fails unless the deployed release.json reports the uploaded commit; web-static-dist is kept 90 days. Only the artifact of a push run on master or a clean local build is a deployment candidate: pull_request runs build GitHub's temporary merge commit, so their release.json records that merge commit, not the pull request head (release identity and rollback) |
CI artifacts expire and are not an archive: the operator must keep the currently deployed artifact (for example downloaded to operator-controlled storage) before replacing it. No archive location or owner is chosen yet. The UI version label is still the fixed "TodayWeather Web · 0.1" |
| Browsers and devices | Chromium automated tests | Safari/iOS Safari, Firefox and Edge runs, installed iOS/Android PWA sessions |
| Owner deferrals | — | Privacy policy and terms links; localized text for upstream English error.message and core normalization errors (D50) |
| Backend follow-ups | The client handles both cases below with localized messages | The API should send Access-Control-Expose-Headers: Retry-After so browsers can read the 429 wait. Since 2026-09-27 API error responses (400, 404, 501) carry access-control-allow-origin: * for browsers (#2584 remains open for native Android WebView requests). A spent overseas provider budget answers 501 instead of a temporary-unavailable status with Retry-After |
The English, Spanish, Japanese, German, Portuguese and French catalogs were written by an agent from the Korean source (using the mobile catalogs as a terminology glossary), cross-reviewed by a different model (6 clear errors fixed), and revised for natural wording without invented abbreviations (#2613 rules; 57 strings). A native speaker should still confirm, per language:
| Language | Open choices |
|---|---|
| All | Institution names in the KAQ forecast credit (romanization of the professor's name, the institute's official translation); how Korea-only concepts (전국 = nationwide, 특보 = weather warnings) read |
| English | "Like an app, only easier" for the install card; "Find" as the search shortcut hint |
| Spanish | "tiempo" vs "clima" for weather; iOS menu wording ("Agregar a inicio" for Latin America vs Spain); "Añadir" vs "Agregar" |
| Japanese | Brand kept as "TodayWeather" (not 今日天気); 大気質 for the 미세먼지 section; 空気質指数 for AQI; 視程, 二酸化硫黄 |
| German | "Schwüleindex" for the discomfort index; "Regenrisiko" for rain probability; exonyms Tokio/Singapur; "Min."/"Std." in refresh intervals |
| Portuguese | "tempo" vs "clima"; hyphenated "off-line"; "Localizando" used for two actions |
| French | Plain spaces before :/? (no non-breaking spaces); straight apostrophes; "Risque de pluie" for rain probability |
Server-provided text (weather descriptions such as "맑음", grade words, place and station names, warnings) follows the request language only once the backend supports it (separate issue). The nationwide map positions markers by the server's Korean region names; if the backend localizes them, it must keep a stable region code for the map. Known limitations: observed/published time stamps and sunrise/sunset keep the international YYYY-MM-DD HH:MM form in every locale; the web manifest name ("오늘날씨 · TodayWeather") and meta description are static and cannot follow the user's language; a language chunk that cannot load (offline before it was cached) keeps the current language with a connection message.
Unit tests run with npm test (Vitest), browser tests with npm run test:e2e (Playwright, Chromium, static preview with mocked API fixtures). Only the live smoke calls the real API. File names are relative to the repository root.
| Requirement area | Unit | Browser (mocked API) | Live smoke / manual |
|---|---|---|---|
| Overseas weather (Visual Crossing, live since 2026-09-27): Tier A cities of #2615 render with current, yesterday, hourly, daily, air where available, the Visual Crossing credit and a localized wind direction (degrees → 16 compass points); long server place names are shortened in the heading | web/test/locale.test.ts (compass points), web/test/display.test.ts and packages/weather-core/test/forecast-air.test.ts (air provider credit) |
web/e2e/mobile.spec.ts (long place name and wind value at 320 px) |
2026-09-27: live responses for the 30 Tier A cities in all seven request languages were recorded once and replayed in the web app (7,560 views: 320/393 px and 768 px in Chrome and Safari, 1440 px also in Edge). 15 cities answered (13 overseas, Seoul, Busan) and all rendered; the other 15 overseas cities returned 501 after the provider's daily budget was exhausted and showed the localized error state. Backend gaps: no UV in overseas responses (the provider request does not ask for uvindex), no AQI for Tokyo, Sydney and Miami (resolved by the air provider chain on 2026-09-29), and a spent provider budget answers 501 instead of a temporary-unavailable status. District-level place names ("Manhattan", "4th arrondissement") are the intended geocoding result. 2026-09-29, after the air provider chain (#2628) was deployed: the 30 cities were recorded once in ko and once in en (the other languages replay the English recording); 29 answered with air from google (Seoul and Busan too, as AirKorea had no fresh observation), Brasília returned 501. Air times are local wall times; server air wording is localized for ko, en, ja and de only |
Languages: seven catalogs with identical keys and placeholders, no Korean left in other languages; browser detection (region ignored, unsupported → English); per-country default units with preserved saved/explicit units and regional hour/date/number conventions (web/test/locale.test.ts); settings override persisted; <html lang>, title and Accept-Language follow the choice; per-language relative days, dates and numbers; localized catalog city names and search; offline shell in another language; German and French at 320 px |
web/test/i18n.test.ts |
web/e2e/i18n.spec.ts |
2026-09-27 layout review of every view in all seven languages at the reference sizes (see the phone layout row) |
| Phone layout: hourly chart labels never touch when 1-hour and 3-hour rows mix, 16px form text (no iOS focus zoom), native controls follow the dark theme, nationwide map markers do not overlap and use short names, 44px weather actions on one row at 320 px, settings values shown in full | — | web/e2e/mobile.spec.ts (Chromium in CI; also run locally on iPhone and Galaxy device profiles) |
2026-09-27 emulated layout review (now scripted as npm run test:layout: phones/tablets in Chrome and Safari, desktops also in Edge) of every view (light, and dark for four views) at the top-10 Korean CSS sizes per class from StatCounter (Jun–Aug 2026): 10 iPhone (WebKit) and 10 Android phone sizes (320–440 px wide, from issue #2605 comment), 10 tablet sizes (iPad on WebKit, Galaxy Tab on Chromium) and 10 desktop screens (1920×1080 to 3440×1440, height minus ~135 px of browser and taskbar; 1440×900 and 1707×1067 also on WebKit). No overflow, clipping, wrapped actions, sub-16px touch fields or sub-44px touch icons remained |
| Direct transport: exact paths, credentials, canonical units, size/JSON/redirect bounds, one retry on 502/network, timeout, localized errors | web/test/direct-api.test.ts, web/test/retry.test.ts, web/test/audit-fixes.test.ts |
web/e2e/web.spec.ts (upstream failure, address geocode), web/e2e/precipitation-time-errors.spec.ts (overseas network failure), web/e2e/audit-fixes.spec.ts (501 text/plain) |
Live smoke: weather, nation, warnings and geocode responses with CORS headers |
429/503: rate-limit wording and Retry-After seconds, 60-second default, separate 503 outage wording, no retry, snapshot kept until the retry time |
web/test/audit-fixes.test.ts (Retry-After seconds, no retry), web/test/audit-fixes-2.test.ts (429 without the header, Retry-After as an HTTP date, 503 wording, weatherStaleTime with retryAt) |
web/e2e/audit-fixes-2.spec.ts (429 with Access-Control-Expose-Headers: Retry-After and a pinned clock: snapshot shown with "30초", a single request across view remounts and online events, a new request once the wait has passed) |
Not observed live: no real 429/503 was triggered |
| Normalization, units, time parsing, D45 precipitation, air grades, warnings | packages/weather-core/test/*.test.ts, web/test/display.test.ts, web/test/warning-time.test.ts |
web/e2e/docs-gap.spec.ts, web/e2e/forecast-air.spec.ts, web/e2e/precipitation-time-errors.spec.ts, web/e2e/review.spec.ts, web/e2e/audit-fixes-2.spec.ts (nation weather — without a unit) |
Live smoke: KMA source, parsed stamp, known precipitation labels, air credit (AirKorea or the named provider), Seoul forecast shape (hourly and daily rows, numeric current temperature) |
| Staleness and time-zone labels | web/test/audit-fixes.test.ts (exact KST rule), web/test/audit-fixes-2.test.ts (overseas UTC−12 date end; saved and last-receipt times formatted in KST whatever the device zone) |
web/e2e/audit-fixes.spec.ts (zone labels), web/e2e/audit-fixes-2.spec.ts (a KMA observation flagged after 3 hours and not before) |
Live smoke: freshness warnings for the Seoul observation (older than 3 hours, or the page's stale-observation notice), the Seoul forecast publication (older than 24 hours, or the page's stale-forecast notice), nationwide air observation times and the newest warning announcement (older than 24 hours) |
| Units and refresh | web/test/state.test.ts |
web/e2e/audit-fixes-2.spec.ts (non-default units render in weather and air; changing a unit in settings converts the shown weather while the API is still asked for default units; the 30-minute refresh interval re-requests weather and the manual (0) setting does not) |
— |
| Search, geolocation, favorites and current-location state | web/test/state.test.ts, web/test/state-privacy.test.ts, web/test/audit-fixes.test.ts |
web/e2e/web.spec.ts, web/e2e/docs-gap.spec.ts, web/e2e/audit-fixes.spec.ts |
— |
| Snapshots, offline, corruption, clear data, storage unavailable, import and export | web/test/state.test.ts, web/test/state-privacy.test.ts |
web/e2e/web.spec.ts, web/e2e/docs-gap.spec.ts, web/e2e/forecast-air.spec.ts, web/e2e/audit-fixes.spec.ts (import accept/cancel, storage unavailable), web/e2e/audit-fixes-2.spec.ts (export omits the current location and selection; oversized, malformed and wrong-version imports are rejected; the 30-entry bound drops the oldest snapshots) |
— |
| Service worker: first claim, update offer, previous assets, complete-cache cleanup, installing-worker race, completion marker, failed install, hourly and visibility update checks, digest | web/test/worker.test.ts, web/test/audit-fixes.test.ts, web/test/audit-fixes-2.test.ts (a failed install deletes its own cache), web/test/platform.test.ts |
web/e2e/review.spec.ts, web/e2e/update-flow.spec.ts (served with the production CSP, with CSP violations collected on every page; no reload before the click; only the tab that pressed "업데이트" reloads, another tab keeps its state, shows "새 버전이 적용됐습니다" and drops its stale "새 버전이 준비됐습니다" offer), web/e2e/audit-fixes-2.spec.ts (hourly check) |
Live smoke: the worker becomes ready and controls the page after a reload; an offline reload of /weather/seoul/hourly is answered by the cached shell with the saved-data notice. Installed iOS/Android sessions: manual |
| Screens, navigation, accessibility, themes, install, share, notifications guidance | — | web/e2e/web.spec.ts, web/e2e/audit-fixes.spec.ts (closed menu inert, Escape, aria-pressed, live region, dark theme before first render, share), web/e2e/audit-fixes-2.spec.ts (focus after menu navigation, close button and backdrop focus return, view tabs keep focus, Back onto a view-tab entry focuses main, widening closes the menu, saved theme applied before <body>, theme-color follows the chosen theme, photo and classic themes, install prompt, manifest) |
Live smoke: deep links render; screen-reader and other-browser checks: not covered |
| Hosting routes, CSP, cache headers | web/test/static-hosting.test.ts, web/test/worker.test.ts (CloudFront and worker route agreement on samples including every SPA route; /theme.js is sampled only as a non-navigation path) |
Every browser test (the fixture fails on CSP violations and local /api/ requests in every page of the context, and aborts public API requests no test route handles); serving /theme.js through the route function is exercised through the preview |
Live smoke: deep links, 404s, CSP connect-src, cache headers, release.json, every same-origin file index.html references and every entry of the deployed /sw.js precache list (200 with a matching content type) |
Uploader: dry run, order, demo rejection, each preflight mismatch, release commit, refusal of -dirty builds |
web/test/static-hosting.test.ts (mocked AWS CLI), web/test/audit-fixes-2.test.ts |
— | Real destination mismatch, cache policy, certificate/TLS, DNS: manual Option A checks |
Release identity (-dirty), CI path filters, artifact retention, live smoke off pull requests, smoke helpers (forecast shape, --expect-commit, freshness, precipitation labels, KMA observation time, precache list parsing and entry problems, known-warning labels and job summary) |
web/test/static-hosting.test.ts, web/test/audit-fixes-2.test.ts, web/test/live-smoke.test.ts (domestic freshness with a pinned KST clock) |
web/e2e/audit-fixes.spec.ts (no demo fixtures in a live build); the global setup refuses a web/dist older than its sources, build configuration, precache script or web/.env* files |
Live smoke: release.json commit, a -dirty warning, and with --expect-commit a mismatch fails; failed checks save a screenshot next to the JSON result; warnings are marked known or new in the JSON and in the job summary written when GITHUB_STEP_SUMMARY is set |
The live smoke also requests one non-KR place (/weather/tokyo/hourly, overseas VC). A non-200 or unreadable response is a warn, not a failure (an upstream problem), and is reported as a new warning since overseas weather went live on 2026-09-27. A 200 would have to pass the overseas (VC) shape check, show a numeric temperature and the "Weather Data Provided by Visual Crossing" credit.
Not covered by any automated check: an interrupted upload (recovery is to retry the same complete artifact), pressing "업데이트" when no worker is waiting, a page without a service-worker controller consuming the reload request, TLS, the real-gateway error cases of V42 (JSON error, gateway/Lambda error, redirect), a real 429/503 and whether the API exposes Retry-After, the browser path of a 429 without the expose header and of a Retry-After HTTP date (both covered by unit tests only), CORS from the real app.todayweather.ai origin before deployment, and monitoring or rollback triggers. The unit-switch test changes the unit only after the weather has rendered.
Deferred: a unit change while a weather request is still in flight (the stored snapshot and the shown units after the response lands), a browser test of the air-station staleness path (the 3-hour limit and zone label on air station times), screenshot comparison, a browser test of the default KMA main journey against a stored response, an end-to-end run of a demo build, Safari/Firefox/Edge runs, automated accessibility (axe) scanning. Backend follow-ups are listed in the release gaps.
The rewrite verification matrix V01–V41 describe legacy server/app behavior; V42 is the web consumer contract. The live smoke covers its KR success paths and records the current non-KR failure as a warning; its error, redirect and unit-mismatch cases are covered only by the mocked tests above.
Follow the test guide: unit and browser tests, the layout check across sizes, browsers and languages, and the live smoke. The Web app workflow publishes web-browser-evidence and web-static-dist (90-day retention) as CI artifacts; the Web live smoke workflow publishes web-live-smoke. Local SDLC reports under reports/sdlc/ are ignored and are not part of this PR; maintained implementation and operating guidance live under docs/ and infra/. Historical results above retain their original dates and scope.
The production display/chart implementation and its current acceptance evidence are documented in the adoption contract, dated evidence and user manual. Earlier dated browser/device claims above remain historical; they are not current design-system verification. API, source credits, units, stale snapshots and release/deployment boundaries remain applicable.