The backtest script carried stale imports that made ruff fail even though runtime behavior did not depend on them. Removing the unused imports keeps CI lint checks green without changing script logic.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: python -m ruff check .
Not-tested: full application test suite
Mobile review found that account payment rows could collapse labels vertically and several secondary links had small touch targets on narrow phones. The patch keeps the existing visual system but allows account rows to stack on mobile, improves tap height, and tightens the Scan Terminal narrow-screen container.
Constraint: Must keep the current desktop layout and avoid adding dependencies
Rejected: Rebuild the account page layout wholesale | too broad for a targeted mobile audit
Confidence: high
Scope-risk: narrow
Directive: Keep long account/payment identifiers breakable on narrow screens
Tested: iPhone SE and iPhone 12 Playwright mobile audits show no horizontal overflow on checked pages
Tested: npx tsc --noEmit --pretty false --project frontend/tsconfig.json
Tested: npm run build
Tested: npm run test:business
Not-tested: Authenticated /ops data state because local Supabase/admin gating redirects to the public shell
Proxy routes now share one upstream-error adapter so client-actionable statuses such as auth, entitlement, validation, and rate limits survive the BFF instead of becoming opaque 502s. The scan terminal mobile overrides also load last and remove desktop rail constraints so phones get a single readable column.
Constraint: Mobile users reported the dashboard was unreadable, and BFF proxy errors were masking expected client states.
Rejected: Let every route keep bespoke error JSON | continued inconsistent status codes and production detail leakage.
Confidence: high
Scope-risk: moderate
Directive: Keep ScanTerminalMobile.module.css imported after desktop scan-terminal CSS so mobile breakpoints win.
Tested: npx tsc --noEmit --pretty false --project frontend/tsconfig.json
Tested: npm run build
Tested: npm run test:business
Tested: Chrome mobile smoke test at 390px with no horizontal overflow
Not-tested: Real device Safari/Android manual QA
The dashboard had several oversized orchestration, component, and CSS files that made product-copy changes and mobile/performance work risky. This refactor preserves behavior while splitting scan terminal CSS, opportunity helpers, future forecast panels, history/detail charts, and probability/model sections into smaller ownership boundaries.
Constraint: No user-visible version bump because this batch is architecture and performance cleanup, not a release announcement.
Rejected: Rewrite dashboard state management in the same batch | too broad for a safe upload after CSS and component splitting.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep new component/CSS boundaries instead of moving product copy back into the large dashboard files.
Tested: npm run build; npm run test:business; git diff --check
Not-tested: Browser visual smoke test after push
The scan terminal service had accumulated cache, payload, filtering, AI prompt, AI merge, METAR gate, ranking, and city-row construction details in one file. This splits those stable responsibilities into focused modules while preserving the endpoint payload shape and existing behavior.
Constraint: User-visible behavior and release version must remain unchanged for this internal refactor
Rejected: Rewrite the terminal scan flow around a new abstraction | too risky while production behavior is being stabilized
Confidence: high
Scope-risk: moderate
Directive: Keep scan_terminal_service.py as orchestration; add detailed rule changes to the focused modules instead of re-growing the service file
Tested: py_compile for extracted modules; ruff check .; pytest tests/test_scan_terminal_modules.py tests/test_web_observability.py; full pytest; npm run test:business; npm run build; git diff --cached --check
The scan terminal had grown into overlapping CSS, request-state, AI-provider, and city-card data responsibilities. This refactor separates those boundaries without changing product behavior: CSS modules are split by surface, city AI prompt/provider/fallback logic is isolated, and scan terminal request state now has reusable RemoteData adapters plus business-state tests.
Constraint: Preserve existing global scan-terminal class names and API responses during the refactor
Constraint: No new dependencies; keep this as a file-boundary cleanup
Rejected: Introduce React Query now | higher migration risk than the requested lightweight query-client path
Rejected: Rewrite AI stream behavior | progressive/fallback states are product-sensitive and were only adapter-split
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep AI stream state changes covered by business snapshots before changing fallback/cache wording
Tested: npm run test:business; npx tsc --noEmit; npm run build; python pytest -q; ruff check; py_compile targeted city AI modules
Not-tested: Live DeepSeek provider network replay and browser visual QA
The dashboard risk is now mostly contradictory state combinations rather than styling. This extracts calendar action grouping into a pure utility and adds a lightweight TypeScript business-state runner so key decision states can be asserted without adding a test dependency.
Constraint: No new dependencies; use the existing TypeScript package for a local runner.
Rejected: Only rely on Next build/typecheck | it cannot catch product-language regressions such as fallback AI being labeled complete.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run test:business
Tested: npm run build
Not-tested: Browser-rendered mobile fold interaction snapshots.
Phone users need the city card to answer what matters first instead of inheriting the full desktop analysis hierarchy. This adds a MobileDecisionCard that leads with city, observed temperature, expected high, peak window, one decision reason, status tags, freshness, and a separate market-price row, while keeping AI, model evidence, and the chart behind collapsible sections.
Constraint: Preserve the existing desktop city-card layout and decision state semantics.
Rejected: Continue relying only on CSS hide/show | it keeps mobile coupled to the desktop information architecture.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: TypeScript diagnostics for AiPinnedCityCard and MobileDecisionCard
Tested: npm run build
Not-tested: Device screenshot QA across iOS/Android viewport sizes.
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.