Skip to content

TUI: match ui_kits/tui concept across every page#15

Merged
frankea merged 7 commits into
mainfrom
claude/match-tui-design-f7vJT
Apr 24, 2026
Merged

TUI: match ui_kits/tui concept across every page#15
frankea merged 7 commits into
mainfrom
claude/match-tui-design-f7vJT

Conversation

@frankea

@frankea frankea commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Rebuild every Textual view to mirror the design bundle at design/ui_kits/tui/index.html: new topbar, sidebar, ActionBar (replacing stock Footer), and PageHeader chrome; redesigned Aircraft / Flights / Events / Spoof / Map / Status / Ops views; new JumpToHex (:) and Help (?) modal screens.
  • Add count_trace_bytes to back the traces field in the topbar, and extend status_snapshot with the derived indicator counts the new status card grid needs.
  • Wire pytest-asyncio into the dev extra so CI actually runs the new tests/test_tui_app.py smoke tests (they were silently skipping before). Split long SQL and kbd-hint strings to clear ruff's 120-col limit.

Test plan

  • uv run ruff check .
  • uv run ruff format --check .
  • uv run pytest (545 tests pass, including the new TUI smoke tests)
  • Visual check: uv run python -m adsbtrack.cli tui and cycle all seven views plus the JumpToHex and Help modals

claude and others added 7 commits April 21, 2026 21:49
Reworks the Textual TUI to mirror the design bundle at
`design/ui_kits/tui/index.html` on every surface:

- Topbar: brand · db · flights · aircraft · traces · job · UTC clock,
  dot-separated, right-aligned trailing. `count_trace_bytes` backs the
  traces field.
- Sidebar: tinted active row + cyan left marker + page-bg kbd pill,
  Views / Operations / Session groups.
- ActionBar replaces Textual's stock Footer with a single-row kbd hint
  strip and trailing `<page> · normal` mode indicator.
- PageHeader: dot-separated crumb with right-aligned trailing detail,
  consistent across all seven views.
- Aircraft: TYPE dim, cyan `last_seen ↓` sort indicator, `(unknown)`
  fallback, dashed flags.
- Flights: TYPE column replaces LAND; airport codes colour-coded
  (ok / amber / red / dim) per concept; mission magenta; header uses
  dot separators.
- Events: outlined pills, TYPE column dropped (the pill is the type),
  severity counts rendered as trailing pills.
- Spoof: PER-SOURCE column built from reason_detail.source_rates;
  inline detail panel with violet left border.
- Map: braille canvas plus HUD strips (layers + cursor info on top,
  scalebar + scrubber on bottom) mirroring the SVG concept.
- Status: full card-grid rebuild with 4 stat cards, weighted
  position-source bars, mission-mix bars, indicators, signal tier +
  52-week sparkline, FAA registry card. `status_snapshot` gains the
  derived indicator counts.
- Ops: 2-col card grid with progress bars parsed from stdout.
- New JumpToHex (`:`) and Help (`?`) modal screens.

Smoke tests (`tests/test_tui_app.py`) spin the full app headless and
cycle the view bindings so future regressions are caught by CI.

https://claude.ai/code/session_01MW6KrntUD9LTXQwArjKYCd
Branch landed with 13 ruff errors and 3 failing tests (test_tui_app's
async tests couldn't run because pytest-asyncio was missing from the
dev extras that CI installs via `uv sync --extra dev`).

- Add pytest-asyncio to project.optional-dependencies.dev (the group CI
  uses). Drop the stray [dependency-groups] entry `uv add --dev` wrote
  earlier so deps live in one place.
- Declare asyncio_mode = "strict" under pytest so the explicit module
  pytestmark in test_tui_app stays load-bearing.
- Split three long SQL CASE expressions in status_snapshot across lines
  to stay under the 120-col ruff limit.
- Split the long keyboard-shortcut row in jump.HelpScreen the same way.
- Split the ops job hint string: one f-string per kbd hint.
- Apply the SIM108 ternary suggestion on status._bar_row and
  map.MapTraceInfoStrip.set_point.
- Drop the unused `total` local in status._build_missions_body (it was
  computed but never read; `top` drives the bar ratio instead).

All 545 tests pass, ruff check + format --check both clean.
…n tests)

Five-agent review on PR #15 surfaced three critical issues, six
important issues, and four in-scope polish items. Type-design refactors
(Hex24 newtype, StatusSnapshot dataclass, DataTable row-key pattern)
were explicitly deferred to a follow-up PR.

Critical:
- app.on_mount: replace the blanket contextlib.suppress(Exception) that
  wrapped all three DB counts with per-call sqlite3.DatabaseError
  handling that surfaces the error via self.notify. A schema mismatch
  or migration regression previously rendered 0 flights / 0 aircraft /
  0 B silently; now the user sees the exception.
- status._build_missions_body: drop the dead first-pass loop that built
  rows via a brittle _bar_row().replace() hack and was immediately
  thrown away by the rebuild below it. One loop, no string surgery.
- app.tcss: delete the #map-body / MapCanvas / .map-hud / .map-layers /
  .map-info / .map-scalebar / .map-scrubber rules. They referenced an
  ID and classes the MapView compose tree never produced; the map
  rendered by accident of each HUD widget's inline DEFAULT_CSS. Layout
  is now unambiguously the Vertical / _HudRow / canvas / _HudRow tree
  documented in map.py.

Important:
- widgets._widget_width: narrow bare except Exception to
  (RuntimeError, NoMatches). RuntimeError is what Textual raises from
  DOM resolution when the widget is not yet mounted; NoMatches covers
  the detached-tree path. A broken widget.size API would now surface
  as AttributeError instead of silently returning a 120-col fallback.
- tests/test_tui_app.py: the original smoke test pressed 5/6 on an
  empty DB, which short-circuited via the _current_icao guard and
  never touched FlightsView / MapView / StatusView set_icao paths.
  Add test_app_navigates_after_selecting_aircraft that opens the
  seeded ccc333 and cycles every view with switcher.current
  assertions.
- tests/test_tui_queries.py: the previous indicator test only asserted
  zeros. Add per-branch coverage for each SUM(CASE WHEN ...) in
  status_snapshot (emergency, go_around, long_hover, signal_lost,
  off_airport, confirmed) plus a test_status_snapshot_unknown_icao
  that pins the None-stats shape.
- map.MapTraceInfoStrip / MapScrubberStrip: docstrings claimed a
  "trace cursor" and "playback scrubber" that the code does not
  implement (the strips only ever render the last point and a full-
  span progress bar). Rewrite to match behaviour; rename the "CURSOR"
  label to "LAST"; "(no cursor)" fallback becomes "(no trace)".
- jump.action_accept: replace silent dismiss(None) on empty-match
  Enter with a user-visible notification naming the query.
- widgets.pill_markup: docstring claimed an "outlined" pill with a
  simulated border; the implementation is plain tinted-bg coloured
  text. Rewrite honestly.

Suggestions:
- ops._render_job: drop the cmd_tail conditional that stripped a
  ['uv', 'run', 'python', '-m'] prefix that _launch never added.
- status._build_signal_body: hoist the mid-function import math to
  module scope; relabel "weekly uptime, 52 weeks" to
  "activity strip (placeholder, not real uptime)" so the label no
  longer overstates what the deterministic sparkline is showing.
- status._build_sources_body: drop the hardcoded ADS-R 0.0 row
  (queries never populated it) and add an OTHER row backed by the
  already-computed sources['other']. The card no longer misrepresents
  coverage.
- widgets.Sidebar: delete the orphaned _ROW_WIDTH constant and the
  three-line comment above _LABEL_WIDTH that restated the self-
  documenting code.

Tests: tests/conftest.py now hosts the seeded_db fixture so the TUI
app smoke tests and query tests share one definition.

All 548 tests pass, ruff check and format --check clean, mypy clean
on the touched TUI files.
…rings

Second review pass on PR #15 flagged two important + five suggestion
items after the first fix commit. Addressing all of them in one go.

Important:
- tests/test_tui_app.py: the nav test previously only asserted
  switcher.current. A regression where set_icao silently swallowed an
  exception and rendered empty widgets would still pass. Now the test
  also asserts FlightsView's DataTable has the one seeded row,
  Sidebar._active tracks the current view after each key press, and
  the rendered StatusView grid contains the seeded "N111AA" from
  aircraft_registry (so a regression in the registry-merge path in
  status_snapshot would fail the test).
- adsbtrack/tui/views/map.py: MapScrubberStrip docstring tail
  ("reserved for a future scrubber") was the same aspirational-rot
  profile we just cleaned up on the other map widgets. Drop the
  forward-looking clause; keep the behavioural description.

Suggestions:
- adsbtrack/tui/widgets.py: the new _widget_width except-block comment
  was restating the narrowed exception tuple; the function docstring
  already explains the pre-mount "why". Delete the redundant comment.
- adsbtrack/tui/views/map.py: MapLayersStrip / MapTraceInfoStrip /
  MapScalebarStrip / MapScrubberStrip no longer carry classes="map-*"
  kwargs since the matching CSS rules were removed last commit - the
  orphan class attributes were noise for future greps.
- tests/test_tui_queries.py: add three tests that pin behaviour the
  prior suite didn't cover:
    * days_with_data correctly counts distinct trace_days dates
      (including across multiple sources that share a date).
    * sources weighted-average math: a 900-point ADS-B-heavy flight
      and a 100-point MLAT-heavy flight weight correctly (82%/18%).
    * missions null-filter drops None mission_type and the LIMIT 6
      clause holds against seven distinct missions.
- tests/test_tui_queries.py: add a one-line note that off_airport
  is a subset of confirmed landings so future readers don't "fix"
  the == 4 assertion down to == 3.
Third review pass on PR #15 surfaced one important item (missing
cross-ICAO isolation in the days_with_data test) plus a handful of
polish suggestions. Addressing them in one commit.

Important:
- tests/test_tui_queries.py: test_status_snapshot_days_with_data_counts_trace_days
  now seeds a second ICAO with a third date. A regression that drops the
  WHERE icao = ? predicate in queries.py would previously have passed the
  test; it now fails loudly.

Polish:
- tests/test_tui_queries.py: missions test seeds the "training" mission
  twice so its count is 2, pinning the ORDER BY n DESC head. Adds an
  assertion on snap["missions"][0] so future regressions in the order-by
  or LIMIT are caught instead of riding on SQLite's unspecified
  tie-breaking.
- tests/test_tui_queries.py: new test_status_snapshot_sources_returns_none_when_all_points_zero
  pins the NULLIF zero-guard path in status_snapshot - every flight with
  data_points=0 should resolve sources to None rather than crashing or
  returning a zero-denominator result.
- tests/test_tui_queries.py: tighten the off_airport overlap comment to
  the load-bearing sentence.
- adsbtrack/tui/views/map.py: MapScrubberStrip docstring no longer
  overclaims; describes both the loaded and unloaded states.
- tests/test_tui_app.py: trim two comment blocks that narrated the test
  flow; keep only the regression-class-explaining sentences.
CI installs only --extra dev (no textual). The tui/__init__.py eager
`from .app import AdsbtrackApp` pulled textual in at package-load time,
which broke collection of any test that imports `adsbtrack.tui.queries`
or `adsbtrack.gui_export` in a dev-only environment:

  ModuleNotFoundError: No module named 'textual'
    at tests/test_tui_queries.py and tests/test_gui_export.py

`adsbtrack/tui/queries.py` is pure sqlite3 and is the module
`adsbtrack/gui_export.py` depends on for its query layer; it does not
need Textual at all. The single consumer of `adsbtrack.tui.AdsbtrackApp`
(the `tui` CLI subcommand) already has an ImportError guard and now
imports from the submodule directly.

Also adds a pytest.importorskip("textual") to tests/test_tui_app.py is
already in place; the queries and gui_export tests no longer drag
textual in, so they collect successfully under --extra dev alone.

Verified: `uv sync --extra dev` + `uv run pytest` = 548 passed, 2
skipped. With tui extra: 552 passed, 1 skipped.
@frankea frankea merged commit f5d8e32 into main Apr 24, 2026
2 checks passed
@frankea frankea deleted the claude/match-tui-design-f7vJT branch April 24, 2026 02:13
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.

2 participants