fix(core,fuse,gui,cli): pair a Pixel raw with its JPEG, and stop refresh timing out

Three things the 12,445-photo library showed that the tests did not.

A Pixel raw capture is two files named after the role each plays —
`PXL_….RAW-01.COVER.jpg` and `PXL_….RAW-02.ORIGINAL.dng` — so their stems differ
and the pairing rule never saw one shot. The marker is now cut off before names
are compared, narrowly: digits, then one role word, so an ordinary name that
happens to contain `.raw-` keeps its stem. Fed the real library's first 40 rows
through the grouping, they collapse to 21 tiles.

A raw that has a JPEG beside it is now painted with that JPEG's thumbnail, which
the server usually already holds. The Raw tab costs no raw download and no raw
decode for those tiles. Raws are found in one batched read, so an ordinary page
pays nothing for the lookup.

Photos could not be moved to Trash at all: they live on the photo volume, which
the mount never holds, so no `nodes` row exists, every authority answered
`Unknown`, and the write check refused the whole library with "trash access: not
allowed". Membership of our own timeline is the authority that does exist for a
photo, and it now answers. An album photo on someone else's volume still fails
closed.

Refreshing the gallery then reported `io: Resource temporarily unavailable (os
error 11)` (B92). The daemon held the control reply until the whole timeline had
been re-read — 63 batches of 200 nodes for this library, minutes of work against
a 120 s read bound. The daemon now answers as soon as the refresh is running,
`PhotosRefreshStatus` reports when it is done, and both front ends follow it: the
gallery reloads the page when it clears, and `pdfs refresh photos` blocks so the
command still means what it says. Each refresh logs its photo count, batch count
and duration.

Released as 1.11.0.
This commit is contained in:
nvrl committed 2026-09-19 17:08:33 +02:00
1 parent 09c0684a5e
commit 376f3edb28
12 files changed
+378 -30

No files matched your search

+39
View File
@@ -12,6 +12,45 @@ Conventions:
---
## B92 — Refreshing the gallery reported "Resource temporarily unavailable"
**Status:** Fixed (unverified) — the daemon carrying the change has not been driven against a real
library yet.
**Found:** 2026-09-19, while checking why a 12,445-photo timeline still showed the pre-grouping
layout after pressing Refresh. `pdfs refresh photos` failed after about two minutes with:
```
Caused by:
0: io: Resource temporarily unavailable (os error 11)
1: Resource temporarily unavailable (os error 11)
```
**Where:** `crates/pdfs-fuse/src/control.rs` (`RefreshScope::Photos`),
`crates/pdfs-core/src/control.rs` (`READ_TIMEOUT`, the new `PhotosRefreshStatus`),
`crates/pdfs-gui/src/app/main.rs` (`refresh_then`), `crates/pdfs-cli/src/main.rs` (`cmd_refresh`).
**Cause.** `Refresh { Photos }` awaited `refresh_timeline()` before replying, on the reasoning that
somebody who presses Refresh wants the truth rather than a stale page. But the refresh resolves
every photo's node in batches of `TIMELINE_ENRICH_CHUNK` (200), so a 12,445-photo library is 63
server round-trips — minutes of work. The client's read bound is `READ_TIMEOUT` = 120 s, so the
socket timed out with `EAGAIN` while the refresh was running perfectly. In the GUI the timeout was
invisible: `refresh_then` ignores the reply and loads the page anyway, which re-read the *old*
timeline and made Refresh look like a dead button.
**What changed.** The daemon starts the refresh (`spawn_timeline_refresh`, which already dedupes)
and replies `refreshing` at once. `Request::PhotosRefreshStatus` reports whether one is running;
the gallery polls it every 2 s and reloads the page when it clears, and `pdfs refresh photos`
polls in the same way so the command still means "the timeline is up to date when I return".
`refresh_timeline` now logs `photos`, `chunks` and `elapsed_ms`.
**Still open.** Every refresh re-resolves every photo, whatever changed. The metadata the resolve
learns (name, media type, content hash, photo relation) is already kept across refreshes, so the
pass could be limited to uids that have none — but the favourite tag is read from the same node,
and remote favourite changes arrive as `NodeUpdated` events that only ask for "a refresh". Making
the refresh incremental means deciding where favourites come from first.
---
## B91 — The daemon hung overnight, and nothing could say what it was waiting on
**Status:** Open — the hang itself is undiagnosed. What is fixed (unverified) is the
+25 -5
View File
@@ -9,7 +9,7 @@ release ships. Migrations are forward-only — a database written by a newer cli
refuse-to-open, not a downgrade, so rolling back a release means restoring the cache from
scratch (user data in `staging/` and `recovery/` is never touched by this).
## [Unreleased]
## [1.11.0] — 2026-09-19
Schema: **30** (`photos.content_hash` / `main_uid` / `group_key`); SDK bumped to `proton-sdk` /
`proton-drive-rs` **0.6.5**.
@@ -27,10 +27,11 @@ Schema: **30** (`photos.content_hash` / `main_uid` / `group_key`); SDK bumped to
holding 1500 textures instead of 600, and the rows just past the edge of the window are
fetched ahead of the scroll direction. Scrolling fast through a large timeline no longer
leaves a screen of blank tiles behind.
- **Refresh on the Photos page really refreshes.** It now waits for the timeline to come back
from the server before it answers, instead of only clearing the freshness stamp and letting
the next read serve the stale page. The timeline is also considered stale after 60 s rather
than 5 minutes.
- **Refresh on the Photos page really refreshes.** Instead of only clearing the freshness stamp
and letting the next read serve the stale page, it starts the timeline re-read and follows it:
the page paints from what the daemon holds, then paints again when the new timeline lands. The
timeline is also considered stale after 60 s rather than 5 minutes. `pdfs refresh photos`
blocks until the re-read is done, as it reads.
- **Restore puts back the whole tree.** The trash is a flat list, but you delete a shape: a
folder, and often things you had already deleted inside it. Restoring a folder now also
@@ -66,6 +67,25 @@ Schema: **30** (`photos.content_hash` / `main_uid` / `group_key`); SDK bumped to
daemon that stops answering is restarted instead of sitting there looking healthy (B91).
### Fixed
- **A Pixel raw capture groups with its JPEG.** Pixel names the two files of one shot
`PXL_….RAW-01.COVER.jpg` and `PXL_….RAW-02.ORIGINAL.dng`, so their stems differ and the pairing
rule never saw them as one shot. The part-of-a-shot marker is now cut off before the names are
compared, and the grid shows the cover JPEG once. The grouping is computed by a timeline
refresh, so an existing library pairs up after the next one.
- **The Raw tab no longer decodes a raw it does not have to.** A raw file that was written next
to a JPEG of the same shot is now given that JPEG's thumbnail, which the server usually already
has. No raw download and no raw decode for those tiles.
- **Photos can be moved to Trash from the gallery again.** Photos live on the photo volume, which
the mount never holds, so the write check found no node to consult and refused every photo with
"trash access: not allowed". Membership of our own timeline now answers for them; an album photo
on someone else's volume is still refused.
- **A photos refresh no longer fails with "Resource temporarily unavailable" (B92).** Re-reading a
twelve-thousand-photo library resolves the nodes in batches of 200 and takes minutes, but the
daemon held the control reply until it had finished — well past the client's 120 s read bound.
The caller therefore saw `io: Resource temporarily unavailable (os error 11)` for a refresh that
was running perfectly, and the gallery reloaded from the old timeline. The daemon now answers as
soon as the refresh is running, and the new `PhotosRefreshStatus` request reports when it is
done. Each refresh also logs its photo count, batch count and duration.
- **A worker no longer panics during shutdown (B91).** The FUSE worker pool is joined before the
tokio runtime is dropped, so a worker still inside a job does not find a runtime that is going
away underneath it.