Skip to content

Keep filtered lists fresh; derive observed status per sky object#528

Open
brickbots wants to merge 4 commits into
mainfrom
fix/filter-freshness
Open

Keep filtered lists fresh; derive observed status per sky object#528
brickbots wants to merge 4 commits into
mainfrom
fix/filter-freshness

Conversation

@brickbots

Copy link
Copy Markdown
Owner

Summary

Follow-up to #526: the filter cache keys all reuse on dirty_time, which only advances when a filter parameter changes — but object state changes under an unchanged filter too. This PR keeps filtered lists correct in those cases, and makes observed status behave as a property of the sky object rather than of the catalog listing.

Four commits, one concern each:

  1. fix(catalogs): keep filtered lists fresh as objects are logged and the sky rotates

    • Logging an object invalidates the cache when an observed criterion is active (Catalogs.mark_logged), so an "Observed: No" list drops it on its next refresh.
    • With an altitude criterion active, verdicts age out (CatalogFilter.is_stale(), TTL 600 s ≈ 2.5° of sky drift, inside the 10°-step filter granularity), and the arrival of an alt/az fix triggers one refilter — closing the boot-time gap where pre-lock verdicts skipped the altitude test and everything "passed" forever. Staleness is promoted to a dirty bump in filter_catalogs() so both cache layers re-evaluate, and UIObjectList.update() polls it so a list screen left open refreshes in place.
    • Without an altitude criterion is_stale() short-circuits — perf(catalogs): skip re-filtering unchanged catalogs on list open #526's O(catalogs) list-open fast path is preserved.
  2. feat(observations): derive observed status per sky object, not per listing
    Logging M 31 marks NGC 224 observed — in-session, across restarts, and retroactively for historical logs. Log entries stay recorded per (catalog, sequence) (no schema change); the observed cache additionally maps each logged listing to its object_id at load, check_logged tests id membership for DB-backed objects, and mark_logged propagates to sibling composites. Virtual objects (planets/comets/coordinate entries with session-minted negative ids) keep the per-listing test — logging Mars must not mark Jupiter.

  3. feat(observations): show log entries from all sibling listings on details
    get_logs_for_object resolves a DB-backed object's sibling listings, so NGC 224's details show M 31's logs ("1 Logs") instead of contradicting the row's checkmark.

  4. fix(ui): keep the cursor on target across object-list refreshes
    Refreshes re-locate the previously selected object; if it was filtered out (just logged, or dropped below the altitude criterion), the cursor falls to the first surviving old successor — the natural next target — instead of resetting to the top. Matching is by (catalog_code, sequence), since CompositeObject.__eq__ compares object_id alone and would land on a sibling listing.

User-visible after upgrade (intended): historical observations retroactively mark sibling listings — an "Observed: No" NGC list may visibly shrink, sibling rows gain the observed checkmark, and a sibling's details show the combined log count.

Docs

  • ADR 0020 (docs/adr/0020-filter-freshness-staleness-promotion.md) records the freshness policy and the observed-identity decision, including rejected alternatives (timer thread, per-object invalidation, in-memory-only propagation, schema migration).
  • docs/ax/catalog/CONTEXT.md: Logged redefined per sky object, Log entry and Stale terms added; docs/ax/catalog.md §4 updated to match.

Test plan

  • 14 new unit tests across test_catalog_filter_cache.py, test_observed_identity.py, test_object_list_cursor.py (freshness triggers, sibling propagation, restart derivation, virtual-id fallback, sibling details logs, cursor next-target rule).
  • Full suite: 803 smoke+unit pass on top of current main; ruff + mypy clean.
  • Verified live (headless app) against a real observations DB: sibling checkmarks derived purely from historical logs (NGC 224/221/225 via M 31/M 32/Col 7 entries, unlogged neighbors unaffected), "1 Logs" on sibling details, a just-logged object dropping from an "Observed: No" list with the cursor landing on its successor, and the altitude filter applying live.

🤖 Generated with Claude Code

brickbots and others added 4 commits July 12, 2026 16:07
…e sky rotates

Two kinds of object state change under an unchanged filter and were
never picked up by the filter cache until a parameter changed:

- Logging an observation: Catalogs.mark_logged(obj) now sets obj.logged
  and bumps dirty_time when an observed criterion is active, so an
  "Observed: No" list drops the object on its next refresh (and a
  "Yes" list gains it). With observed == "Any" no verdict can change,
  so the cached lists survive. Logged status stays per catalog listing,
  matching check_logged's (catalog_code, sequence) keying.

- Altitude drift: with an altitude criterion active, verdicts age out
  as the sky rotates (<= 15 deg/hour). CatalogFilter.is_stale() reports
  staleness (TTL 600s, bounding drift to ~2.5 deg) and also fires once
  when an alt/az fix arrives after verdicts were computed without one —
  closing the boot-time gap where everything "passed" the skipped
  altitude test forever. Catalogs.filter_catalogs() promotes staleness
  to a dirty bump so both cache layers re-evaluate; is_dirty() surfaces
  it to the screen-activation refresh path, and UIObjectList.update()
  polls it so a list screen left open refreshes in place.

Without an altitude criterion is_stale() short-circuits, preserving the
O(catalogs) list-open fast path from #526.

Freshness policy rationale in docs/adr/0020; glossary updated
(docs/ax/catalog/CONTEXT.md: Dirty time, Stale).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sting

Log entries stay recorded per (catalog, sequence), but observed status
now keys on object_id for DB-backed objects: the observed cache maps
each logged listing to its object id at load, check_logged tests id
membership, and Catalogs.mark_logged propagates to sibling composites
in-session. Logging M 31 marks NGC 224 observed — durably across
restarts and retroactively for historical logs.

Virtual objects (negative, session-minted object_ids) keep the
per-listing test: id-keyed status would cross-mark the shared -1
default or vanish on reboot.

Supersedes ADR 0020's per-listing decision (amended in place —
unpublished).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ails

get_logs_for_object resolves a DB-backed object's sibling listings and
returns log entries recorded under any of them, so NGC 224's details
show M 31's logs instead of "Not Logged" while the row carries an
observed checkmark. Virtual objects keep the per-listing query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refresh_object_list() re-sorts and reset the cursor to the top, so
logging an object (or the altitude-staleness refresh on a left-open
list) dumped the user back at the start of the list. Now the cursor
re-locates the previously selected object; if it was filtered out, it
falls to the first surviving old successor — the natural next target —
clamping as a last resort. The NEAREST auto re-sort follows the same
rule, so the selection tracks the object through the distance ranking.

Matching is by (catalog_code, sequence): CompositeObject.__eq__
compares object_id alone and would land the cursor on a sibling
listing (M 31 == NGC 224).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant