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.
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.
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
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
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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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)clamp(260px, 34vh, 460px)tall, with a 2×2 thumbnail grid beside it ("+N" on the last tile).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.{channel[name]},{id}).{channel}/{date:%Y%m%d}_{message_id}_{num:>02}_{filename}.{extension}, with a sidecar named to match.Discord repair (
9b82a95)New card: Settings → Maintenance → "Repair Discord downloads" (preview first, then apply). The apply:
discord/None/…_None_…images and their files, and sweeps leftover sidecars;discord%archive entries;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
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_01SHQB1YukL3VyvMK8rcbmV9The 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