Signal counts kept current on the page: after a read, and when Jev picks something out (#258)

The page fetched the Signals' unread counts only with the feed list, so reading an item, here or
in its own feed, left its Signal's badge as it was, and so did Jev adding one. The Apple Signal said
2 unread over a page with none. The page now fetches them again after a read or unread lands, and
on a new signal_hits event, which the judge sends once a pass for each person it found items for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 17:54:42 +00:00
parent 20f53183ac
commit 1ae91170fd
6 changed files with 24 additions and 1 deletions

View File

@@ -34,6 +34,8 @@ pub enum Event {
TorrentDeferred { feed: String, url: String },
/// DeadAir finished a file's copy (#171), so a page showing it as in progress can stop.
DeadairDone { feed: String, enclosure: i64, saved_s: i64 },
/// Jev found items for someone's Signals (#258), so their page fetches the counts again.
SignalHits { user: i64, hits: usize },
Reaped { path: String, bytes: u64 },
/// Terminal: a client that asked for work stops reading here.
ScanDone {
@@ -90,6 +92,7 @@ impl Event {
Event::FeedStart { feed } => format!("{feed}: checking"),
Event::TorrentDeferred { feed, .. } => format!("{feed}: torrent deferred"),
Event::DeadairDone { feed, saved_s, .. } => format!("{feed}: DeadAir found the silences, {saved_s} s shorter"),
Event::SignalHits { hits, .. } => format!("{hits} item(s) picked out for Signals"),
Event::ScanDone { feeds, .. } => format!("scan complete, {feeds} feed(s)"),
})
}

View File

@@ -52,6 +52,7 @@ async fn pass(ctx: &Ctx) -> Result<bool> {
let span = tracing::info_span!("signals", entries = items.len());
async {
let mut refused = HashSet::new();
let mut hits_for = std::collections::BTreeMap::<i64, usize>::new();
for item in &items {
if refused.contains(&item.user_id) {
continue;
@@ -60,6 +61,7 @@ async fn pass(ctx: &Ctx) -> Result<bool> {
Ok(verdicts) => {
let hits = verdicts.iter().filter(|(_, h)| *h).count();
if hits > 0 {
*hits_for.entry(item.user_id).or_default() += hits;
tracing::info!(ev = "signal", user = item.user_id, feed = %item.feed_id, new = hits, "an item fits a Signal");
}
ctx.db.record_verdicts(&item.feed_id, &item.guid, &verdicts).await?;
@@ -80,6 +82,10 @@ async fn pass(ctx: &Ctx) -> Result<bool> {
Err(e) => tracing::warn!(user = item.user_id, feed = %item.feed_id, error = %format!("{e:#}"), "Jev could not judge an item"),
}
}
// Once a pass, not once an item: a page fetches its counts again on each.
for (user, hits) in hits_for {
ctx.out.emit(crate::ipc::Event::SignalHits { user, hits });
}
let some_refused = !refused.is_empty();
ctx.signals_refused.lock().unwrap().extend(refused);
// A pass that only failed would otherwise go straight round again on the same items.