Split the nightly imagery sweep's still_there tally so that panos which came back are counted separately from panos that were never gone.
Why
pano_data.expired_at (#4932, evolution 358) is row state, not an event log: it is cleared when a re-check finds the imagery back, and cleared again by PanoDataTable.upsert whenever a user successfully views the pano. So an expiry that later reverses leaves no trace at all, and the /admin/street-status expiry chart quietly loses that week's count.
We decided on #4932 not to build an append-only expiry log yet — the chart is now labelled as the snapshot it is — on the condition that we'd revisit if reversals turned out to be common. This issue is what makes that condition checkable, because right now it isn't: the quantity we'd need to see is exactly the one nothing records.
Note that two columns (expired_at + returned_at) do not solve it either. A pano that expires, returns, and expires again overwrites both, so history still mutates — just later, and after looking stable long enough to be trusted. Only counting events survives that case, which is why this is the cheap measure and a full event log is the real fix.
This matters more than "imagery occasionally returns" suggests. Genuine returns are probably rare, since a re-drive replaces a pano rather than restoring its id. The other cause is false positives from transient provider errors (cf. #4918), and those recur on the same panos — so reversals, if they happen, concentrate in exactly the panos whose data we'd least trust.
What
PanoDataService.checkForImagery already re-checks a budget of already-expired panos alongside unexpired ones, so it knows when one comes back — it just folds that into stillThere:
ImageryCheckResult(
stillThere = responses.count(_.contains(true)),
gone = responses.count(_.contains(false)),
errors = responses.count(_.isEmpty)
)
Split stillThere into "unexpired, still fine" and "was expired, came back", add the latter to ImageryCheckResult and to the details JSON that JobRunService.record writes into background_job_run.
The value is that background_job_run is a table, not a dashboard reading: nobody has to watch it nightly, and the answer is available retroactively whenever we wonder —
SELECT sum((details->>'returned')::int)
FROM background_job_run
WHERE job_name = 'check-image-expiry-actor';
It also counts events, so a pano that flip-flops three times contributes three.
Implementation note
checkImageryBounded uses mapAsyncUnordered and documents its results as arriving in completion order, so the expired subset cannot be recovered by slicing the response list positionally. Each pano needs to carry a "was expired" flag through the stream (or the two subsets need to be tallied by separate calls).
Decision this feeds
If the count comes back meaningfully non-zero, go straight to an append-only pano expiry/return log and have the chart read that instead. If it stays near zero, the current snapshot semantics are fine as they are and nothing further is needed.
Follow-up to #4932 / #4928.
🤖 Generated with Claude Code (claude-opus-5[1m])
Split the nightly imagery sweep's
still_theretally so that panos which came back are counted separately from panos that were never gone.Why
pano_data.expired_at(#4932, evolution 358) is row state, not an event log: it is cleared when a re-check finds the imagery back, and cleared again byPanoDataTable.upsertwhenever a user successfully views the pano. So an expiry that later reverses leaves no trace at all, and the/admin/street-statusexpiry chart quietly loses that week's count.We decided on #4932 not to build an append-only expiry log yet — the chart is now labelled as the snapshot it is — on the condition that we'd revisit if reversals turned out to be common. This issue is what makes that condition checkable, because right now it isn't: the quantity we'd need to see is exactly the one nothing records.
Note that two columns (
expired_at+returned_at) do not solve it either. A pano that expires, returns, and expires again overwrites both, so history still mutates — just later, and after looking stable long enough to be trusted. Only counting events survives that case, which is why this is the cheap measure and a full event log is the real fix.This matters more than "imagery occasionally returns" suggests. Genuine returns are probably rare, since a re-drive replaces a pano rather than restoring its id. The other cause is false positives from transient provider errors (cf. #4918), and those recur on the same panos — so reversals, if they happen, concentrate in exactly the panos whose data we'd least trust.
What
PanoDataService.checkForImageryalready re-checks a budget of already-expired panos alongside unexpired ones, so it knows when one comes back — it just folds that intostillThere:Split
stillThereinto "unexpired, still fine" and "was expired, came back", add the latter toImageryCheckResultand to thedetailsJSON thatJobRunService.recordwrites intobackground_job_run.The value is that
background_job_runis a table, not a dashboard reading: nobody has to watch it nightly, and the answer is available retroactively whenever we wonder —It also counts events, so a pano that flip-flops three times contributes three.
Implementation note
checkImageryBoundedusesmapAsyncUnorderedand documents its results as arriving in completion order, so the expired subset cannot be recovered by slicing the response list positionally. Each pano needs to carry a "was expired" flag through the stream (or the two subsets need to be tallied by separate calls).Decision this feeds
If the count comes back meaningfully non-zero, go straight to an append-only pano expiry/return log and have the chart read that instead. If it stays near zero, the current snapshot semantics are fine as they are and nothing further is needed.
Follow-up to #4932 / #4928.
🤖 Generated with Claude Code (claude-opus-5[1m])