Add spinner/countdown-bar to stats panel; backfill on widened days_back

Stats panel: a persistent spinner marks whichever target is currently
being scanned/reacted to, and jittered waits show a live-ticking
countdown bar instead of a static "waiting Xs" line. Both the status
line and countdown row are always rendered (blank when idle) so their
appearing/disappearing no longer shifts the table's height.

Backlog: channel_scan_state now tracks oldest_covered_at alongside
newest_id_seen. If days_back is widened after a channel already has a
resume checkpoint, the forward-only diff would never look back far
enough to notice — now the newly-exposed gap gets backfilled first.
This commit is contained in:
2026-08-01 19:24:20 +02:00
parent 7b3c358b05
commit ef90ee6a0b
6 changed files with 159 additions and 31 deletions

View File

@@ -24,7 +24,7 @@ async def scan_channel_backlog(
react_fn: ReactFn,
display: Optional[StatsDisplay] = None,
) -> None:
"""Scan a channel's history once, resumably, in two phases.
"""Scan a channel's history, resumably, in two phases.
Phase 1 finds every message from the friend in the window first (so the
"found" count settles before any reacting starts, rather than climbing
@@ -38,6 +38,13 @@ async def scan_channel_backlog(
up again — this is independent of the newest_id_seen checkpoint below,
so nothing found is ever silently abandoned even if the checkpoint has
already moved past it.
The channel's resume state tracks not just newest_id_seen (for the
forward diff) but also oldest_covered_at — the earliest point in time
already fully scanned. If days_back has been widened since the last run
(cutoff is now older than oldest_covered_at), the newly-exposed gap
[cutoff, oldest_covered_at) is backfilled first — otherwise the forward-
only diff would silently never look back far enough to notice.
"""
channel_id = getattr(channel, "id", None)
if channel_id is None:
@@ -59,13 +66,16 @@ async def scan_channel_backlog(
state = await asyncio.to_thread(storage.get_scan_state, channel_id)
newest_id_seen = int(state["newest_id_seen"]) if state and state.get("newest_id_seen") else None
after = discord.Object(id=newest_id_seen) if newest_id_seen else cutoff
oldest_covered_at = (
datetime.datetime.fromisoformat(state["oldest_covered_at"])
if state and state.get("oldest_covered_at") else None
)
if display is not None:
display.set_status(f"Fetching message history for {chan_label}...")
try:
async for message in channel.history(after=after, oldest_first=True, limit=None):
async def scan_range(after, before, status_verb: str) -> None:
nonlocal newest_id_seen
if display is not None:
display.set_status(f"{status_verb} {chan_label}...")
async for message in channel.history(after=after, before=before, oldest_first=True, limit=None):
if newest_id_seen is None or message.id > newest_id_seen:
newest_id_seen = message.id
@@ -75,10 +85,27 @@ async def scan_channel_backlog(
storage.record_message_found, message.id, channel_id, target.target_id, friend_id
)
if display is not None:
display.set_status(f"Scanning {chan_label}{len(found_messages)} found so far...")
display.set_status(f"{status_verb} {chan_label}{len(found_messages)} found so far...")
await display.refresh()
await asyncio.to_thread(storage.set_scan_state, channel_id, target.target_id, newest_id_seen, True)
try:
if oldest_covered_at is None:
# Never scanned before: one pass across the whole configured window.
await scan_range(cutoff, None, "Fetching message history for")
oldest_covered_at = cutoff
else:
if cutoff < oldest_covered_at:
# days_back was widened since the last run — the forward-only
# diff below would never notice the newly-exposed older range,
# so backfill exactly that gap first.
await scan_range(cutoff, oldest_covered_at, "Backfilling older history in")
oldest_covered_at = cutoff
after = discord.Object(id=newest_id_seen) if newest_id_seen else cutoff
await scan_range(after, None, "Fetching new messages in")
await asyncio.to_thread(
storage.set_scan_state, channel_id, target.target_id, newest_id_seen, oldest_covered_at, True
)
except discord.Forbidden:
log.warning("No access to channel %s (%s), skipping", channel_id, getattr(channel, "name", ""))
return