Weather labels, METAR translation, risk badges, and hero meta chips were still embedded in dashboard-utils even though panel, modal, and detail views use them as focused presentation helpers. This moves them into weather-summary-utils while keeping dashboard-utils re-export compatibility.
Constraint: Preserve existing weather/METAR/risk/hero meta wording.
Rejected: Move airport narrative in the same pass | it has broader AI narrative coupling and should be separated independently.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: TypeScript diagnostics for weather-summary-utils, dashboard-utils, PanelSections, DetailPanel, and FutureForecastModal
Tested: npm run build
Not-tested: Visual snapshot of every weather icon/label combination.
Probability and multi-model view adapters were still embedded in dashboard-utils even though table, panel, modal, and city-card views use them as small pure selectors. This moves them into model-utils and keeps dashboard-utils re-export compatibility.
Constraint: Preserve model/probability return shapes and existing fallback behavior.
Rejected: Merge model-utils with chart-utils | model selectors and chart data preparation change at different rates.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: TypeScript diagnostics for model-utils, dashboard-utils, and PanelSections
Tested: npm run build
Not-tested: Bundle analyzer size comparison.
The intraday chart data builder lived inside dashboard-utils with observation-source and TAF helpers, so chart consumers had to depend on the large dashboard utility surface. This moves chart data preparation, observation-source helpers, and TAF marker labels into focused modules while keeping dashboard-utils re-export compatibility.
Constraint: Preserve existing chart data shape, observation labels, TAF labels, and legacy dashboard-utils exports.
Rejected: Rewrite chart data generation while moving it | this pass is a boundary move only so visual behavior remains stable.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: TypeScript diagnostics for chart-utils, dashboard-utils, observation-source-utils, and taf-utils
Tested: npm run build
Not-tested: Browser visual regression across every chart city.
Pace-adjusted high calculations were embedded in dashboard-utils alongside chart, profile, and modal helpers. This moves the pure pace model and reusable HM time helpers into focused modules while keeping dashboard-utils re-export compatibility for older callers.
Constraint: Preserve existing pace wording, thresholds, and calculation output.
Rejected: Split all remaining dashboard-utils helpers at once | model/chart/modal helpers have wider call surfaces and should move in separate reversible passes.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: TypeScript diagnostics for pace-utils, time-utils, dashboard-utils, and FutureForecastModal
Tested: npm run build
Not-tested: Bundle analyzer size comparison.
Temperature formatting was embedded in the large dashboard utility module, so small scan-terminal views had to import the heavy utility surface for simple labels. This moves the pure temperature helpers into a lightweight module while keeping dashboard-utils re-exports for compatibility.
Constraint: Preserve existing temperature text output and all dashboard-utils import compatibility.
Rejected: Split chart, pace, and model helpers in the same pass | those helpers have wider coupling and should move one boundary at a time.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: TypeScript diagnostics for temperature-utils, dashboard-utils, and OpportunityTable
Tested: npm run build
Not-tested: Bundle analyzer size comparison.
Opportunity and calendar cards now isolate item rendering behind memoized components. This preserves the current action grouping and copy while preventing selection or parent dashboard updates from re-rendering every dense card body.
Constraint: Do not change ranking, grouping, or product wording in this performance pass.
Rejected: Introduce virtual list dependency | the current list size can benefit from memo boundaries first without new dependencies.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: TypeScript diagnostics for CalendarView and OpportunityOverview
Tested: npm run build
Not-tested: Browser profiler capture with a large production city set.
City decision cards rendered Chart.js canvases and AI evidence bodies before the user could see or expand those sections. This keeps the card shell visible while delaying chart data/canvas work until the section nears the viewport and skipping the AI evidence body while its details panel is collapsed.
Constraint: Preserve existing decision-card layout and evidence copy.
Rejected: Add list virtualization in the same pass | card-level render costs should be reduced before changing list mechanics.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: npm run build
Not-tested: Runtime scroll benchmark on a large production city set.
Market scan state was still tracked as separate nullable payload and status strings. This moves the hook internals to RemoteData<MarketScan> so loading and error paths can preserve previous data while keeping the existing marketScan and marketStatus return values for current card consumers.
Constraint: Preserve the existing useCityMarketScan public compatibility fields.
Rejected: Update all card UI to consume marketRemote immediately | keeping the compatibility bridge avoids a broad rendering diff while the request state model lands.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: npm run build
Not-tested: Forced live market-scan failure with stale previous quote.
The city-card data hook still contained localStorage serialization and TTL eviction helpers alongside AI fallback and UI state transitions. Moving those helpers into scan-terminal-cache creates a reusable cache boundary for the scan terminal request layer without changing cache keys, TTLs, or payload shapes.
Constraint: Preserve existing localStorage keys and expiry behavior.
Rejected: Migrate all caches to RemoteData in this commit | cache-helper extraction is a safer intermediate step before query policy changes.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: npm run build
Not-tested: Browser private-mode quota edge cases beyond existing guarded behavior.
The AI city forecast hook still owned stream queueing and in-flight request dedupe after the first request-client pass. Moving that policy into scanTerminalClient keeps network concurrency, queued progress, and requestKey reuse in the request layer while leaving the hook responsible only for cached UI state and progress rendering.
Constraint: Preserve existing two-stream concurrency limit and queued user-facing progress copy.
Rejected: Move localStorage cache at the same time | cache policy should be separated from stream transport policy to keep this refactor reviewable.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: npm run build
Not-tested: Live multi-city SSE under production latency.
The scan terminal hooks were each carrying their own fetch, SSE parsing, error normalization, previous-data handling, and market request behavior. This adds a small scanTerminalClient plus RemoteData helpers so terminal data, city detail, market scans, and AI city streams share one request boundary without introducing React Query.
Constraint: Do not add dependencies or change backend API contracts.
Rejected: Introduce React Query immediately | too broad for this release and would force larger UI state rewrites.
Rejected: Move localStorage caches in the same pass | safer to first isolate network and stream IO before cache policy migration.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Live SSE cancellation against production latency.
The city card had accumulated independent checks for AI status, market availability, stale observations, breakouts, peak timing, model consensus, and next-bulletin waits. This introduces a CityDecisionState builder so UI components consume one coherent recommendation, urgency, evidence quality, AI status, market status, badges, and primary reason.
Constraint: Keep the current card copy and badge priority behavior while moving decision-state ownership out of the component.
Rejected: Refactor calendar and opportunity lanes in the same commit | the shared model should land behind the city card first, then other views can migrate with smaller visual-risk diffs.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Browser visual regression and dedicated unit tests for every decision-state combination.
Dashboard.module.css mixed base shell styles with the entire scan terminal surface, making every decision-card or calendar styling pass risky. This extracts the scan terminal layer into its own CSS module and attaches that module root beside the existing dashboard root so global class selectors keep their current behavior.
Constraint: Preserve existing global scan-* class names and visual cascade.
Rejected: Rename scan classes into scoped module keys | too much DOM churn for a behavior-preserving CSS split.
Rejected: Split card/calendar/mobile rules in the same pass | safer to establish the scan-terminal layer first before finer component CSS ownership.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Pixel-level browser comparison across dark and light themes.
AiPinnedForecastView had become a mini application that owned list state, card shell, freshness, market, AI evidence, and model evidence rendering in one place. This split keeps the workspace wrapper thin and gives the decision card explicit product-component seams for future recommendation reasons, risk levels, and mobile-specific layout work.
Constraint: Preserve existing weather, market, AI read, chart, refresh, remove, collapse, and mobile behavior.
Rejected: Rewrite the decision-state construction in the same pass | this component split should stay behavior-preserving before a state-model refactor.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Browser visual regression on physical mobile devices.
AiPinnedForecastView still owns the city-card data assembly, but its header, decision band, and AI evidence rendering now live in focused presentational sections. This keeps the next decision-state refactor smaller while preserving the current UI and interaction behavior.
Constraint: Keep weather, market, and AI evidence calculations unchanged in this pass.
Rejected: Move business-state construction at the same time | mixing behavior extraction with view extraction would make regressions harder to isolate.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Browser manual regression on mobile expand/collapse states.
ScanTerminalDashboard had accumulated terminal fetching, theme persistence, local clock updates, and AI pinned-city hydration in one component. Moving those responsibilities into focused hooks keeps the screen component as the composition layer while preserving the existing UI and data flow.
Constraint: Refactor must not change the current decision-card or scan-terminal behavior.
Rejected: Split visual card components in the same commit | too much surface area for one safe refactor pass.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Browser manual regression across map, calendar, and pinned-card interactions.
The AI helper extraction left a stale private helper import in the scan terminal service. Removing it keeps CI aligned with the current call graph without changing runtime behavior.
Constraint: CI runs ruff F401 as a blocking check.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: .\.codex-tmp\pydeps\bin\ruff.exe check .
Not-tested: Full backend test suite; change is import-only.
Users needed reassurance that unavailable quotes and long AI evidence are normal states, not broken systems. This adds a v1.5.5 upgrade announcement, softens market-unavailable copy, surfaces a one-line recommendation reason, and makes mobile cards prioritize observed temperature, expected high, peak timing, AI expansion, and a separate market line.
Constraint: Keep existing dashboard data contracts and avoid backend schema changes.
Rejected: Hide unavailable market rows entirely | users still need to know weather evidence remains usable without a quote.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Browser visual QA on physical mobile devices.
Users need to know whether a city can be acted on before reading the full explanation. City cards now expose METAR or official-observation freshness, model update timing, market quote freshness, and AI state in a dedicated trust block, while the calendar view groups rows into action-oriented timing buckets with a single reason per city.
Constraint: Keep the existing card and calendar data contracts; derive freshness and action reasons from fields already present in the frontend payload.
Rejected: Add another long explanatory paragraph | it would repeat the same trust problem instead of making the first glance clearer.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Live browser visual QA with stale METAR and delayed quote examples.
Users saw fast-rule evidence, partial streams, DeepSeek completion, and fallback states as one blended AI status. The city evidence panel now names the current stage directly so a loading stream reads as fast judgment complete, a successful response reads as AI bulletin read complete, and incomplete responses explain that rule evidence is being used.
Constraint: Keep the existing fast evidence path visible while DeepSeek streams in.
Rejected: Label fallback as an AI failure | that incorrectly implies the card is broken even when rule evidence is valid.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: npm run build
Not-tested: Manual browser timing of partial stream transitions.
City decision cards already had enough evidence, but the first screen still forced users to read the longer explanation before seeing why a card mattered. The header now caps status chips at three high-priority signals and uses product-facing labels for observed breakouts, stale METARs, AI loading, missing market prices, strong model agreement, and next-report waits.
Constraint: The card should stay lightweight and avoid another explanatory section.
Rejected: Keep six mixed freshness chips in the header | too much noise for first-glance scanning.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: npm run build
Not-tested: Browser visual QA across all city states.
Qingdao needs the same airport-settlement path as the other Wunderground-backed APAC cities, so the registry, aliases, timezone, prewarm, official links, market focus, and tests now point to ZSQD / Qingdao Jiaodong International Airport.
Constraint: User supplied Wunderground Qingdao/ZSQD settlement URL.
Rejected: Add a partial registry-only entry | it would show in APIs without frontend links, prewarm coverage, or alias support.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: pytest tests/test_country_networks.py tests/test_web_observability.py::test_cities_endpoint_includes_new_wunderground_cities -q
Tested: npm run build
Not-tested: Live Wunderground fetch for ZSQD in production.
The decision card now exposes deterministic guard state, data freshness, AI readiness and market sync directly in the card header so users can understand whether they are looking at fresh evidence, a fallback, or a stale/exception case before reading the full explanation.
Constraint: Keep the first useful read available while DeepSeek airport/HKO details are still streaming.\nRejected: Add another expanded evidence panel | header-level badges are faster to scan and avoid increasing card depth.\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nTested: npm run build\nNot-tested: Browser visual QA on production data.
The scan terminal service had grown into a 3.5k-line file that mixed endpoint orchestration, AI provider calls, fallback copy, JSON repair, and deterministic evidence guards. This extracts the city-AI helper layer into a focused module while preserving the old private names through imports for existing tests and callers.
Constraint: Keep behavior unchanged after the previous evidence-guard fixes and avoid a broad service rewrite.
Rejected: Split every scan-terminal concern at once | too much regression risk for this maintenance pass.
Confidence: high
Scope-risk: narrow
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build
DeepSeek can return a polished city forecast that conflicts with already-computed stale-observation, observed-break, or peak-window evidence. The completion path now carries the deterministic fallback guard state and overwrites only the critical fields when provider text or numbers contradict those local facts.
Constraint: Airport-read latency optimization keeps provider output narrow, so backend completion remains the authority for final highs and evidence conflicts.
Rejected: Trust provider final wording when present | it can reintroduce stale METAR anchors or miss observed high breaks.
Confidence: high
Scope-risk: narrow
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build
City-card fallback reads now stop using stale METAR or official observations as strong live anchors. A stale observation no longer forces high/low revisions, and both backend and browser AI cache keys include the observation fingerprint so updated report times, receipt times, temperatures, or stale status invalidate old AI text.
Constraint: Cached city AI reads must not survive a material observation update
Rejected: Let stale METAR trigger observed-break revisions | stale reports can be older than the active temperature path
Confidence: high
Scope-risk: moderate
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build
Fallback city-card reads now distinguish three cases that previously collapsed into the generic fast-evidence copy: observed highs above the model path, observed highs still lagging after the peak window, and low observations before the peak window that should wait for confirmation rather than down-revise immediately. The same pass removes three unused private helpers from the scan terminal service.
Constraint: Fallback output must be useful before the full AI airport-bulletin read returns
Rejected: Treat any low latest METAR as a down-revision | early-day observations can be below the forecast before the peak window
Rejected: Keep unused helper wrappers | they were unreferenced and added noise to an already large module
Confidence: high
Scope-risk: moderate
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build
Fast evidence mode should not say DEB and models support the center when the latest METAR has already exceeded that center or the model upper edge. The fallback now treats the live observation as a lower bound for the daily high and explains the upward revision pressure.
Constraint: Fallback output must remain useful before the full AI bulletin read returns
Rejected: Keep the original DEB center until AI completes | it can be lower than an already-observed temperature
Confidence: high
Scope-risk: narrow
Tested: pytest tests/test_web_observability.py::test_city_ai_fallback_revises_up_when_latest_metar_breaks_above_models tests/test_web_observability.py::test_city_ai_fallback_reasoning_identifies_fast_evidence_mode tests/test_web_observability.py::test_city_ai_stream_request_only_asks_provider_for_observation_read -q
Tested: npm run build
The city card only needs the provider to interpret the latest METAR or official observation. Deterministic fields such as the high-temperature center, model cluster note and fallback risks are already available server-side, so the stream request now asks DeepSeek for only the observation read and concise reasoning.
Constraint: City cards still need a complete payload for both Chinese and English UI modes
Rejected: Keep generating the full decision schema in the stream | too much model output for every card
Rejected: Retry failed streams by default | it can double latency and the fallback can use partial streamed text
Confidence: high
Scope-risk: moderate
Tested: pytest tests/test_web_observability.py::test_city_ai_stream_request_only_asks_provider_for_observation_read tests/test_web_observability.py::test_city_ai_fallback_reasoning_identifies_fast_evidence_mode tests/test_web_observability.py::test_city_ai_partial_json_trims_dangling_taf_clause tests/test_web_observability.py::test_city_ai_schema_completion_trims_dangling_taf_clause -q
Tested: npm run build
The city decision fallback path is generated when the full DeepSeek city-airport read has not completed, so the reasoning copy now labels the state as fast evidence mode instead of saying the AI read is normal.
Constraint: Fallback output may use only DEB, model cluster, and latest observation evidence
Rejected: Keep 'AI read normal' wording | it implies a completed AI interpretation when the fallback path is active
Confidence: high
Scope-risk: narrow
Tested: pytest tests/test_web_observability.py::test_city_ai_fallback_reasoning_identifies_fast_evidence_mode tests/test_web_observability.py::test_city_ai_partial_json_trims_dangling_taf_clause tests/test_web_observability.py::test_city_ai_schema_completion_trims_dangling_taf_clause -q
The prior label said the weather decision layer had no market price connected, which could be read as a broken market integration. The card now says weather-first read with market prices shown separately.
Constraint: Market price layer is already rendered below the weather decision band
Rejected: Keep 'no market price input' | accurate internally but misleading in user-facing Chinese copy
Confidence: high
Scope-risk: narrow
Tested: npm run build
Not-tested: Visual screenshot review
City AI can return a partially streamed JSON string when the provider truncates output. The fallback previously kept an unfinished clause such as '但TAF显示', which made the forecast explanation look broken even though earlier evidence was usable.
Constraint: Provider JSON can be truncated after useful fields have already streamed
Rejected: Drop all partial AI text | would lose valid METAR interpretation already returned before truncation
Confidence: high
Scope-risk: narrow
Tested: pytest city AI truncation regression tests
Tested: npm run build
Not-tested: Live DeepSeek provider response
The calendar local-time and AI airport-read fixes are a numbered product release rather than a temporary snapshot, so the changelog and package/version metadata now use 1.5.5 consistently.
Constraint: Existing snapshot tag was already pushed before the numbered release correction
Rejected: Keep snapshot label in changelog | user clarified this should be the 1.5.5 version
Confidence: high
Scope-risk: narrow
Tested: Reviewed git diff for CHANGELOG, VERSION, frontend package metadata
Not-tested: No runtime tests; metadata-only version correction
The release snapshot is no longer unreleased, so the changelog now names the published calendar-local-time tag and records the AI wording and actionable-window behavior.
Constraint: Existing snapshot tag was already pushed before this documentation correction
Confidence: high
Scope-risk: narrow
Tested: Documentation-only change reviewed with git diff
Not-tested: No runtime tests; changelog-only update