Wide-window feed, Discord downloads named and dated correctly, and a repair for the broken ones #253

Merged
bvandeusen merged 3 commits from dev into main 2026-09-13 20:57:06 -04:00
Owner

Three commits, merged at the operator's request. CI passed on the head commit 9b82a95 (ci run 6754: lint, unit, frontend, integration 1299 passed; build run 6753).

Wide-window Latest feed (98c3b74, milestone #407 options A, D, E)

  • Filmstrip cards. At a card width of 1100px or more, the hero image is capped at clamp(260px, 34vh, 460px) tall, with a 2×2 thumbnail grid beside it ("+N" on the last tile).
  • Day groups. Posts are grouped into consecutive days (Today / Yesterday / weekday / date) with post and artist counts; the day label sits in a sticky gutter on wide windows.
  • Filter rail. At 1600px and above, the filters and status ribbon move into a sticky left rail. Below 1600px the layout is unchanged.

Discord naming (dc840fe, issue #3999)

Every Discord download had landed as None/<date>_None_<name>, next to sidecars named after the original attachment. None of them got a Post, so their dates fell back to download time.

  • Cause: the file and folder patterns named keys that gallery-dl's Discord extractor doesn't emit ({channel[name]}, {id}).
  • Now: files are saved as {channel}/{date:%Y%m%d}_{message_id}_{num:>02}_{filename}.{extension}, with a sidecar named to match.
  • Guard: a test renders the patterns against a sanitized real sidecar, so a pattern that names a missing key fails CI.

Discord repair (9b82a95)

New card: Settings → Maintenance → "Repair Discord downloads" (preview first, then apply). The apply:

  • deletes the broken discord/None/…_None_… images and their files, and sweeps leftover sidecars;
  • clears gallery-dl's discord% archive entries;
  • re-arms a backfill on every Discord source.

It defaults to preview and is operator-triggered only.

Deploy

No migrations and no stack changes.

After deploy, run Settings → Maintenance → Repair Discord downloads: Preview, then Apply. The Discord backfills then re-download everything with posts and correct dates.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9

Three commits, merged at the operator's request. CI passed on the head commit `9b82a95` (ci run 6754: lint, unit, frontend, integration 1299 passed; build run 6753). ## Wide-window Latest feed (`98c3b74`, milestone #407 options A, D, E) - **Filmstrip cards.** At a card width of 1100px or more, the hero image is capped at `clamp(260px, 34vh, 460px)` tall, with a 2×2 thumbnail grid beside it ("+N" on the last tile). - **Day groups.** Posts are grouped into consecutive days (Today / Yesterday / weekday / date) with post and artist counts; the day label sits in a sticky gutter on wide windows. - **Filter rail.** At 1600px and above, the filters and status ribbon move into a sticky left rail. Below 1600px the layout is unchanged. ## Discord naming (`dc840fe`, issue #3999) Every Discord download had landed as `None/<date>_None_<name>`, next to sidecars named after the original attachment. None of them got a Post, so their dates fell back to download time. - **Cause:** the file and folder patterns named keys that gallery-dl's Discord extractor doesn't emit (`{channel[name]}`, `{id}`). - **Now:** files are saved as `{channel}/{date:%Y%m%d}_{message_id}_{num:>02}_{filename}.{extension}`, with a sidecar named to match. - **Guard:** a test renders the patterns against a sanitized real sidecar, so a pattern that names a missing key fails CI. ## Discord repair (`9b82a95`) New card: **Settings → Maintenance → "Repair Discord downloads"** (preview first, then apply). The apply: - deletes the broken `discord/None/…_None_…` images and their files, and sweeps leftover sidecars; - clears gallery-dl's `discord%` archive entries; - re-arms a backfill on every Discord source. It defaults to preview and is operator-triggered only. ## Deploy No migrations and no stack changes. After deploy, run Settings → Maintenance → **Repair Discord downloads**: Preview, then Apply. The Discord backfills then re-download everything with posts and correct dates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
bvandeusen added 3 commits 2026-09-13 20:56:59 -04:00
feat: the Latest feed uses a wide window — filmstrip cards, a day gutter and a filter rail (407 A, D, E)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 6s
Build images / build-ml (push) Successful in 6s
CI / frontend-build (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m1s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m10s
98c3b74260
On the operator's 3432px window the feed was a 900px column, 26% of the width. They picked three options from a to-scale layout study.

A — filmstrip card (PostCard.vue):
- A card measures itself with a ResizeObserver. At 1100px or wider its hero gets a fixed height, clamp(260px, 34vh, 460px), and the extra images move into a 2-column grid of squares beside it. The grid cells are sized from the hero height, so the grid ends flush with the hero.
- The rail cap is 4 cells in this layout (2×2, the last becoming "+N") and 5 in the narrow layout, which is unchanged.
- The description clamp drops to 4 lines, because long reads happen in the expanded view.
- The hero has a height, not a width, so a wide card can't grow into a full-screen post. That was the operator's constraint.

D — day gutter (PostsView.vue):
- The normal feed groups consecutive posts by local day (Today, Yesterday, a weekday, or a date), with post and artist counts for what has loaded.
- Runs rather than date buckets, because the sort key includes resurfaced_at, which the payload doesn't carry. A resurfaced grouping gets its own heading where it actually appears, instead of being pulled out of order.

E — filter rail (PostsView.vue):
- At 1600px and wider, the filters and status ribbon stack in a sticky 280px left rail, and each day's heading sits in a sticky 150px gutter beside its posts.
- Below 1600px the layout is exactly the old one, including the 900px column.

The in-context (post_id) view gets the wide column but no rail or day grouping, so anchor scrolling is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
fix: Discord downloads land as None/<date>_None_<name> with no post — name them from the keys gallery-dl really emits
CI / lint (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
CI / extension-version (push) Successful in 2s
Build images / build-agent (push) Successful in 5s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 35s
Build images / build-web (push) Successful in 1m17s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 2m15s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m34s
dc840feec7
Operator report: a Discord source showed the wrong post time. Listing the real downloads found every Discord folder on the instance (7 artist folders, ~1,600 media) in a directory named `None`, as `<date>_None_<original name>`, next to sidecars named `<original name>.json`.

The real sidecar settles why:
- gallery-dl's discord extractor emits `channel` as a plain string, the message as `message_id`, and the attachment position as `num`. It has no `id` key.
- The patterns asked for `{channel[name]}` and `{id}`. gallery-dl renders a missing field as "None" and carries on.
- The sidecar was named `{filename}.json`, the attachment's ORIGINAL name. find_sidecar never pairs that with `<date>_None_<name>.png`, so no Discord file ever got a Post or a post date. The card fell back to downloaded_at.
- Every `image.png` in a channel also overwrote the same `image.json`.

Fix (gallery_dl.py):
- The directory is `{channel}`.
- The filename is `{date:%Y%m%d}_{message_id}_{num:>02}_{filename}.{extension}`, unique per attachment.
- A discord-scoped metadata postprocessor names the sidecar exactly like the media minus its extension, so find_sidecar's first candidate matches. A per-source filename override re-derives the sidecar name. save_metadata=False drops it.

Guard (tests/test_gallery_dl_naming.py) renders the patterns through Python's formatter against a sanitized copy of the real sidecar (same keys and types, invented values). A missing key or a subscript into a string raises, which is the loud failure gallery-dl doesn't give. A positive control shows both shipped patterns fail it.

Existing broken downloads are NOT repaired by this. gallery-dl's archive already records them, so a re-run skips them, and their collided sidecars no longer describe them. That repair is a separate, destructive step for the operator to decide on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
feat: Settings can re-download the Discord images the None naming broke (3999)
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
CI / extension-version (push) Successful in 2s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m9s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 2m3s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m35s
9b82a95b7e
The operator chose a clean re-download over relinking in place. The ~1,600 broken files can't be relinked reliably: their message ids are gone, and their sidecars collided.

Settings → Maintenance → "Repair Discord downloads" previews, then applies:
- Deletes every image whose path is `…/discord/None/<8 digits>_None_…`. Both the folder and the name are required, because that pair is only what the bug produced. It reuses cleanup_service.delete_images for the record and file deletes.
- Sweeps the leftover collided sidecars from those folders and removes the emptied folders.
- Only then clears gallery-dl's archive rows `discord%`, excluding `discordasset_%`. Upstream keys message attachments as `discord{message_id}_{num}`. Since the broken files lost their message ids, per-source forgetting is impossible. Every pre-fix Discord download is broken, and files fetched after the fix still exist on disk, so gallery-dl's `skip` won't re-fetch them.
- Arms a fresh backfill on every Discord source.

The apply defaults to preview at both the route and the task, runs on maintenance_long, and is never on a beat. The card uses the confirm-dialog pattern of AttachmentReclaimCard.

Supporting refactors, with no behaviour change:
- gallery_dl.archive_path() is the single definition of the archive location.
- source_service.arm_backfill() is the mutation start_backfill already did, now shared with the sync repair.

Tests (tests/test_discord_repair.py):
- The archive clear leaves other platforms and Discord assets alone, and counting mutates nothing.
- Case-twin artist folders are both found.
- The folder sweep works.
- An integration run shows only the broken image goes. A correctly named Discord file and a `None` folder under Patreon survive, and only Discord sources are re-armed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
bvandeusen merged commit c3dac235a9 into main 2026-09-13 20:57:06 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledCurator#253