Skip to content

Feat/downloads - #17

Merged
AlexAxthelm merged 39 commits into
mainfrom
feat/downloads
Sep 21, 2026
Merged

AlexAxthelm merged 39 commits into
mainfrom
feat/downloads

Conversation

@AlexAxthelm

@AlexAxthelm AlexAxthelm commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Enable episode download and storage, with simple queueing, and and delete/cancel operations. Controls accessible through list view, and on episode detail page


Download manager

Adds offline episode downloads to Pollux: a serial download queue with live
progress, cancel/delete, per-episode failure reasons, and resume-on-launch —
driven entirely from the shared Rust core with the iOS shell handling the actual
network + filesystem I/O.

Features

  • Serial download queue — one episode downloads at a time (model.downloading
    • download_queue); the rest wait FIFO. Queue entries carry the enclosure URL,
      so a download can start without its feed being loaded.
  • Live byte progress — the shell reports received/total bytes while a download
    runs; shown as a determinate bar (or an indeterminate spinner when the server
    sends no Content-Length). Transient/in-memory — never persisted.
  • Cancel — cancels the in-flight URLSession task and flushes any partial
    file; also drops a still-queued item. Available from the row menu, the leading
    swipe, and the episode detail page.
  • Delete — removes a downloaded file (idempotent).
  • Resume on launch — episodes left Downloading/Queued by a previous session
    are re-enqueued and resumed (partial files don't survive a quit, so each restarts
    from scratch).
  • Failure reasons — the shell's specific error (HTTP status, disk error,
    timeout) is surfaced beside Retry instead of a generic "failed".
  • Non-blocking op-failure notice — a failed delete or a persistence-write
    failure shows a dismissible banner scoped to its episode, leaving the list and
    its controls intact (unlike a list-load error).

How it works

  • New Download capability (shared/src/capabilities/download.rs):
    DownloadOperation (Download / Delete / Cancel) → DownloadResult
    (Completed / Deleted / Cancelled / Error).
  • The core owns when to download and the queue; the shell's DownloadManager
    (an actor) carries out one operation at a time and reports the outcome.
  • Progress ordering — byte-progress callbacks are funnelled through an
    AsyncStream drained by a single consumer, so DownloadProgress events reach
    the core in order; the terminal result lands only after the stream drains.
  • Serial storage — all storage writes go through one serial consumer so a
    download's status transitions (Queued → Downloading → terminal) can't reorder
    and leave a stale row.
  • URLSession detail — progress requires a session-level delegate (the async
    download(from:delegate:) convenience never fires didWriteData); the delegate
    is @unchecked Sendable with all mutable state under an NSLock.

UI

  • Episode rows show an inline status glyph (downloaded / queued / failed / removed)
    and a progress bar while downloading; the download action lives in the row's
    three-dots menu and a leading swipe.
  • The episode detail page has working Download / Cancel / Delete controls and shows
    the failure reason and op-failure notice.
  • All download UI uses the base16 themeColors palette.

Data model

  • UpdateDownloadState storage op persists status + file metadata (path, size).
    Download size is recorded from the delegate's byte count, not a re-stat.
  • Removed the unused download_progress column and Episode field — progress is
    transient, so it was dead end-to-end.
  • Download files live under Application Support/Downloads/, excluded from device
    backups (re-downloadable), named by a SHA-256 digest of the episode id so
    distinct ids can't collide.

Tests

  • Rust core: 81 unit tests covering queueing, serial ordering, progress overlay,
    resume, cancel/delete, the failure-notice lifecycle, and the persist sequence.
  • Shell: DownloadManager tests (stub URLProtocol) for success, non-2xx,
    cancellation cleanup, idempotent delete, progress delivery + throttle, and
    filename collision-resistance; EpisodeFormatting progress-formatter tests.

Known limitations / follow-ups

  • Queue order across relaunch isn't persisted — resume rebuilds by pub date
    (documented; tracked under a ROADMAP "Download queue management" item).
  • Feed refresh vs downloaded size — the episodes upsert would overwrite a
    downloaded file's real size with the feed's advertised length; flagged with a
    note(refresh) at the query since there's no refresh feature yet.
  • The pre-download storage-cap check is a seam (always allows today) for the
    Settings soft cap.

Migration

Pre-release: the download_progress column was removed by editing the v1_initial
migration rather than adding a v2, so existing local/simulator databases must be
wiped
(delete the app from the simulator) to match the schema.

🤖 Generated with Claude Code

AlexAxthelm and others added 11 commits September 15, 2026 10:03
Add a download manager: episodes can be queued for download, fetched one at
a time, and deleted. The core owns the queue and status transitions; a new
Download shell capability performs the file I/O.

- shared: Download capability (fetch/delete), a Download effect, and an
  UpdateDownloadState storage op. The core tracks a serial queue
  (download_queue + downloading) and drives NotDownloaded -> Queued ->
  Downloading -> Downloaded/Failed, persisting each transition and updating
  the in-memory episode so the details page reflects it without a reload.
- iOS: DownloadManager fetches to Application Support/Downloads (stores a
  relative path) with an idempotent delete; core.swift handles the Download
  effect; DatabaseManager implements UpdateDownloadState (row mapping split
  into DatabaseManagerRows.swift to stay under the file-length limit); the
  episode row and detail page get a working download / delete / retry control.

Status-only for now (no live percentage) — the capability and the URLSession
seam are structured so live progress is an additive change. The storage
soft-cap is deferred to the Settings screen; device-full surfaces as a failed
download.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Report byte progress while an episode downloads: a percentage on the
episode row and a determinate bar with a size label ("12 MB / 37 MB") on
the detail page, falling back to an indeterminate indicator when the
server reports no size. Progress is transient runtime state — a partial
download can't resume — so it is kept in-memory only, never persisted.

- shared: replace the placeholder DownloadResult::Progress with an
  Event::DownloadProgress carrying received/total bytes. The core stores
  it in Model.active_download_progress (cleared when a download starts
  or ends) and overlays it onto the active episode in view();
  EpisodeSummary swaps its download_progress (u8 percent) for
  received/total byte fields.
- iOS: byte progress comes from a session-level
  URLSessionDownloadDelegate. The async download(from:delegate:)
  convenience never fires didWriteData, so DownloadManager now uses a
  dedicated URLSession + a continuation, moving the finished temp file
  in didFinishDownloadingTo. core.swift forwards progress as
  DownloadProgress events; updates are coalesced to whole-percent steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the downloaded indicator to a small filled-download glyph inline before
the date/duration instead of its own badge line, and replace that line with a
progress bar (or a spinner with byte count when the size is unknown) while an
episode is downloading — dropping the separate status line and the trailing
percentage so the row reads less busy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A download left mid-flight when the app quit was stranded: its status
stayed Downloading in the DB but nothing was running, so the row showed
a permanent spinner with no way to recover. On Started, query every
episode still marked Downloading/Queued (new ListPendingDownloads
storage op), rebuild the serial queue, and resume them from scratch
(partial files don't survive a quit). The download queue now carries the
enclosure URL per entry so a download can start without its feed's
episodes being loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn the row's stubbed ellipsis into a real menu whose first item is the
context-sensitive download action (Download / Retry Download / Delete
Download; a disabled indicator while queued/downloading/removed). Drop the
dedicated inline download button so the row is just play + ⋯, giving titles
more room. Download status stays inline (meta-line glyph and progress bar) —
only the action moved. Swipe shortcuts and configurable row layout are left
for later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the spec's default leading swipe action (short swipe-right) on
episode rows: Download when not downloaded, Retry when failed, Delete
when downloaded (destructive). Full-swipe commits Download/Retry but not
Delete, so a stray long swipe can't remove a file;
queued/downloading/removed rows have no swipe (no cancel yet). Fixed
behavior for now — configurable swipes arrive with the Settings screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a cancel action for the downloading/queued states (three-dots menu +
swipe). Cancelling a queued episode just drops it from the queue;
cancelling the active one asks the shell to cancel the URLSession task,
and the in-flight Download request then resolves as Cancelled —
resetting the episode to not-downloaded (not failed), clearing progress,
and draining the queue to the next item. The shell tracks the active
task so Cancel can reach it, and flushes any partial bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a cancel button beside the progress bar (and on the queued row) in
the episode detail view, so an in-flight or queued download can be
stopped without going back to the list. Delete was already available
there for a downloaded episode; this fills in the missing cancel
affordance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restrict DownloadEpisode to the NotDownloaded/Failed states; every other
state (Downloaded, Queued, Downloading, RemovedFromFeed, or an unknown id) is
now an explicit no-op, so a stray or repeated event can't re-fetch a file we
already have or double-enqueue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unordered persistence can leave stale download state, while failure details and queued/failed row status are not adequately exposed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Adds serial episode downloads across the Rust core and iOS shell, including persistence, progress, cancellation, deletion, and UI controls.

Changes:

  • Adds download capability, queue management, progress, and restart recovery.
  • Implements iOS file downloading and database state updates.
  • Adds download controls, status UI, and tests.
File Description
shared/​src/​view_model.rs Exposes live byte progress.
shared/​src/​model.rs Adds queue and active-download state.
shared/​src/​effect.rs Registers download effects.
shared/​src/​capabilities/​storage.rs Adds pending-list and state-update operations.
shared/​src/​capabilities/​mod.rs Exports download capability.
shared/​src/​capabilities/​download.rs Defines download operations and results.
shared/​src/​app.rs Implements queue lifecycle and tests.
PLAN.md Documents the download-manager plan.
iOS/​PolluxTests/​DatabaseManagerTests.swift Tests download persistence queries.
iOS/​Pollux/​SubscriptionDetailScreen.swift Wires row and swipe actions.
iOS/​Pollux/​EpisodeRow.swift Adds progress and download controls.
iOS/​Pollux/​EpisodeFormatting.swift Formats byte progress.
iOS/​Pollux/​EpisodeDetailView.swift Adds live detail-page controls.
iOS/​Pollux/​DownloadManager.swift Implements file download, cancel, and delete.
iOS/​Pollux/​DatabaseManagerRows.swift Extracts database row conversion.
iOS/​Pollux/​DatabaseManager.swift Persists and restores download state.
iOS/​Pollux/​core.swift Executes download effects and progress events.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread shared/src/app.rs
Comment thread iOS/Pollux/EpisodeRow.swift
Comment thread shared/src/app.rs Outdated
Comment thread PLAN.md Outdated
AlexAxthelm and others added 7 commits September 19, 2026 16:37
The download delegate's continuation was written from the
DownloadManager actor (via attach) but read/cleared on URLSession's
delegate queue — an unsynchronized cross-thread access that @unchecked
Sendable hid. Guard all of the delegate's mutable state with an NSLock;
snapshot under the lock and resume the continuation outside it so
resuming the awaiting task can't deadlock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route byte progress through a single AsyncStream drained by one
consumer, instead of spawning a MainActor Task per callback (which could
reorder and made the displayed percentage briefly regress). Extract the
download effect handling into handleDownload(_:requestId:) to keep
processEffect within the body-length limit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moving the download action into the menu left queued, failed, and removed
episodes looking identical to a plain not-downloaded row. Add a small status
glyph before the date (clock / warning-in-error-color / xmark; the existing
download glyph for downloaded), so those states read at a glance. (Copilot #2.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keep the shell's specific failure reason (disk full, HTTP status,
filesystem error) per episode instead of discarding it, and show it
beside Retry — on the detail page under "Download failed", and inline on
the row in place of the date. Stored in-memory and cleared when the
episode leaves the Failed state. (Copilot #3.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each storage effect was run in its own unstructured Task, so writes for one
episode (Queued → Downloading → terminal) could apply out of order — a fast
failure or cancel could persist its terminal state before the Downloading
write, leaving a stale status that re-downloads the episode after restart.
Funnel storage through a single serial AsyncStream consumer so ops execute in
submission order. (Copilot #1.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AlexAxthelm AlexAxthelm self-assigned this Sep 20, 2026
AlexAxthelm and others added 2 commits September 20, 2026 09:54
fileSize(at:) re-read the file's attributes after a completed download and
returned 0 if that read failed, recording size_bytes = 0 for a real file. Use
the delegate's totalBytesWritten (the completed file's byte count) instead, so
a transient stat failure can't understate storage usage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Current State" section still described the counter-app scaffolding.
Update it — plus the stale Event/Effect descriptions, the FFI/shell
dispatch notes, and the repo layout — to reflect the implemented
feed/library/download-manager core and base16 theming, and fix a "no no"
typo in the CI section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread iOS/Pollux/DatabaseManager.swift Outdated
Comment thread iOS/Pollux/DatabaseManagerRows.swift

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread iOS/Pollux/DatabaseManager.swift
Comment thread iOS/Pollux/DownloadManager.swift Outdated
Comment thread iOS/Pollux/DownloadManager.swift Outdated
Comment thread shared/src/app.rs Outdated
Comment thread iOS/Pollux/DownloadManager.swift
Comment thread iOS/Pollux/EpisodeFormatting.swift
Comment thread iOS/Pollux/EpisodeRow.swift Outdated
Comment thread iOS/Pollux/SubscriptionDetailScreen.swift Outdated
AlexAxthelm and others added 2 commits September 20, 2026 10:18
The download UI used hardcoded system colors
(.green/.blue/.orange/.secondary) instead of the base16 themeColors the
merged theming system established, so it would render off-palette under
a custom theme (Solarized/Nord). Swap them for themeColors
(success/accent/warning/secondaryText); destructive Delete/Cancel keep
the idiomatic system red via their role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address two review nits:

- Guard the failed-download reason overlay with is_empty() so a feed with no
  failures skips a per-episode HashMap lookup on every view().
- Make the cancel flow easier to follow. notify_shell isn't viable here (the FFI
  resolve() panics on a non-resolvable request and the shell resolves every
  download request), so the Cancel request/ack stays — but DownloadFinished's
  Cancelled arm is now documented as the sole owner of the state reset, and the
  DownloadCanceled ack is an explicit no-op sink (dropping a dead, misleading
  cancel-error-to-banner branch). Extract reset_to_not_downloaded, the identical
  set-NotDownloaded + persist idiom shared by the cancel and delete paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AlexAxthelm and others added 8 commits September 20, 2026 20:05
The menu (EpisodeRow.downloadMenuItem) and leading-swipe
(SubscriptionDetailScreen.downloadSwipeButton) both grew a Cancel action for
queued/downloading episodes, but their doc comments still said cancellation
wasn't available. Describe the actual state-dependent behavior instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Downloaded episode files live under Application Support, which is backed
up to iCloud by default. Since the audio is reproducible
(re-downloadable from the feed), a large offline library would
needlessly consume the user's backup quota and inflate their backups.
Set isExcludedFromBackup on the Downloads directory when creating it;
the flag is best-effort so it never fails a download.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The download shell (network + filesystem + cancellation) had no
automated coverage. Add test seams to DownloadManager.init — an
injectable storage root, URLSession configuration, and progress-report
interval, all defaulted so production call sites are unchanged — and a
DownloadManagerTests suite driven by a stub URLProtocol: successful
download (stored file + reported size), non-2xx failure with no leftover
file, in-flight cancellation cleanup, idempotent delete, and progress
delivery. The delegate's throttle interval is now injected rather than a
hardcoded constant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EpisodeFormatting.downloadProgress had no coverage despite the other formatters
being tested. Add cases for nil received bytes, a known total (fraction +
combined label), unknown and zero totals (received-only label, no fraction),
received exceeding total (fraction clamped to 1.0), and UInt64.max inputs (no
overflow via Int64 clamping). Byte labels are checked against a mirror of the
production .file formatter to stay locale-independent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed persistence write or file removal set detail_error — the list-load
error channel — so SubscriptionDetailScreen replaced the entire episode list
(and every download/retry/cancel control) with an error view, and later
successful writes never cleared it. Route these through a new transient,
non-blocking download_notice instead: it renders as a banner above the intact
list, and clears on a successful persist, any fresh download action, or a feed
switch. detail_error goes back to meaning only "the episode list failed to
load."

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
listPendingDownloads rebuilds the queue by pub_date on relaunch, so enqueue
order isn't preserved across restarts (and a Queued item can start before the
one that was actively Downloading). Document this as a known, accepted MVP
limitation at the query, and track the fix — a persisted enqueue order, active
item restored first — under a Phase 5 "Download queue management" roadmap item
alongside the downloads page / re-ordering / parallel-download work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Download progress became transient in-memory state (a partial download
can't resume across a restart), leaving download_progress dead end to
end: the Episode domain field was only ever written None and never read,
the ViewModel never carried it, and the DB
column/UpdateDownloadState.progress param only ever persisted NULL.
Remove it everywhere — Rust domain + storage op, regenerated Swift
types, the SQLite column (edited into v1_initial), and the shell
read/write paths — and delete the obsolete out-of-range-progress test.

Pre-release, so no migration: existing local databases must be wiped (delete the
app from the simulator) to match the updated v1 schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fill the gaps left by two earlier changes: assert the unknown-size
progress throttle caps reporting at the initial callback (a large
chunked body with no Content-Length and a long interval yields ≤1
report, total nil), and that a failed delete surfaces through
download_notice while leaving the row Downloaded and detail_error
untouched. Also cover the notice clearing on subscription switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Download recovery and persistence failures can leave stuck or silently inconsistent state, and detail-page operation errors are not displayed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
Resolved since last review (8)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Handle launch-time storage query failures

shared/​src/​app.rs:59

Handle StorageResult::Error instead of silently continuing. If this launch-time query fails after subscriptions load, persisted Queued/Downloading rows are never added to the in-memory manager; when their feed is opened they still render as pending, but both retry and cancel are unavailable/no-ops until another relaunch. Retry the query or move those rows into a recoverable state and surface the failure.

Comment thread iOS/Pollux/EpisodeDetailView.swift
Comment thread shared/src/app.rs Outdated
AlexAxthelm and others added 8 commits September 20, 2026 22:24
DownloadStatePersisted cleared download_notice on any successful state write,
so a concurrent download completing (its Downloaded persist) could dismiss an
unrelated episode's delete/persist-failure message before the user saw it —
deletes and cancels run independently of the serial download queue, so the
overlap is reachable. Only set the notice on Error now; it still clears on the
next user download action or a feed switch, so it stays transient without being
wiped out of band.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On launch, PendingDownloadsLoaded persisted Queued for every re-enqueued
episode, then maybe_start_next immediately re-persisted the head item as
Downloading — so the head's Queued write was written and instantly
superseded. Skip it for the item that starts this pass (guarded by
downloading.is_none()); items waiting behind it still normalize to
Queued. A test pins the persist sequence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
download_notice rendered only on the subscription list, so a delete or
cancel that failed from an episode's detail page (which has its own
Delete/Cancel controls) left the user with no feedback until they
navigated back. Extract the banner into a shared DownloadNoticeBanner
view and render it above the download controls on the detail page too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sert

The episodes upsert preserves download_status/local_path on conflict but
overwrites file_size_bytes with the feed's advertised enclosure length,
which would clobber the real on-disk size a completed download records.
No feed refresh exists yet so it can't trigger; leave a FIXME(refresh)
at the line so it's addressed when refresh lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The warning triangle in DownloadNoticeBanner is decorative — the message text
carries the meaning — so mark it accessibilityHidden to keep VoiceOver from
announcing the icon's name ahead of the message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The op-failure notice was subscription-wide and rendered on every
episode's detail page, so a delete/cancel that failed on episode A
appeared on episode B's page (the message names no episode, and
navigating between episodes doesn't clear it). Tag the notice with its
episode: download_notice becomes a DownloadNotice { episode_id, message
} through the model, ViewModel, and the DownloadStatePersisted event.
The detail page shows it only when it's about that episode; the list
still shows it for any.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Lossy filename generation can make distinct episode downloads overwrite and delete the same file.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment thread iOS/Pollux/DownloadManager.swift Outdated
fileName reduced an episode id to alphanumerics, mapping other
characters to "_", so distinct ids like `ep-1` and `ep_1` produced the
same file — the second download would overwrite the first, and deleting
either would remove the shared file. Ids are generated UUIDs today so it
can't collide in practice, but the helper accepted arbitrary strings.
Base the name on a SHA-256 digest of the id (keeping the URL extension)
so distinct ids always get distinct files; add a collision regression
test and stop the existing tests hard-coding the name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AlexAxthelm
AlexAxthelm marked this pull request as ready for review September 21, 2026 15:24
Copilot AI review requested due to automatic review settings September 21, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

A failed pending-download load leaves persisted episodes stuck without an in-session recovery path.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (4)

In code that hasn't changed since last review

Medium severity Handle failed pending-download loads without leaving episodes stuck

shared/​src/​app.rs:60

A failed ListPendingDownloads is silently treated as an empty queue, but the database rows remain Queued/Downloading. If their feed is opened later, the UI offers Cancel while CancelDownload cannot find the episode in downloading or download_queue, and DownloadEpisode refuses those statuses, leaving the episode stuck for the session. Retry/reconcile this load or provide a recovery path that resets or requeues those rows.

Low severity Correct documentation for the enabled download menu

iOS/​Pollux/​EpisodeRow.swift:7

This header still says the more-actions control is disabled, but moreMenu below is now an enabled download action. Update the documentation so it distinguishes the working download menu from the remaining playback placeholder.

Low severity Update documentation for the implemented swipe download shortcut

iOS/​Pollux/​EpisodeRow.swift:154

The PR adds a leading-swipe download shortcut in SubscriptionDetailScreen, so saying swipe shortcuts are a later addition now contradicts the implemented UI. Describe the menu as an additional action surface instead.

Low severity Fix docstring to reflect separate download progress tracking

shared/​src/​app.rs:576

This docstring still claims progress is updated by set_download_state, but progress was removed from Episode and is managed separately through active_download_progress. Remove that claim so the helper's contract matches its parameters and behavior.

@AlexAxthelm
AlexAxthelm merged commit 95f3461 into main Sep 21, 2026
10 checks passed
@AlexAxthelm
AlexAxthelm deleted the feat/downloads branch September 21, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants