Skip to content

fix(core): record completed downloads into history via event bus bridge#120

Merged
mpiton merged 2 commits intomainfrom
fix/qa-history-recorder-bridge
Apr 27, 2026
Merged

fix(core): record completed downloads into history via event bus bridge#120
mpiton merged 2 commits intomainfrom
fix/qa-history-recorder-bridge

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented Apr 27, 2026

Summary

• History table remained empty after every completed download; History view, re-download-from-history (P0.9), and retention purge (P0.14) all silently no-op
• Implemented event bridge pattern (mirrors existing stats_recorder_bridge): subscribe to DownloadCompletedPersisted, project Download to HistoryEntry, persist to history table
• Timestamp conversion: ms→s with 1s floor for avg_speed; file_size with downloaded_bytes fallback
• 9 tests: projection, fallback, duration floor, filtering, missing-download, error swallowing; 2 tokio integration tests against real SQLite

Type

fix


Summary by cubic

Fixes empty History by recording completed downloads into the history table via an event-bus bridge and a one-time startup backfill. Unblocks the History view, redownload from history (P0.9), and retention purge (P0.14); fixes vortex-qa BUG #1.

  • Bug Fixes
    • Added history_recorder_bridge and exported spawn_history_recorder_bridge; wired in lib.rs.
    • Added a one-time startup backfill that scans Completed downloads, skips ones already in history, and writes missing rows.
    • DownloadCompletedPersisted now carries a DownloadCompletedSnapshot; both history and stats recorders project from it (no repo re-reads or races). Simplified spawn_stats_recorder_bridge to drop the download-repo dependency.
    • Projection rules: ms→s with a 1s floor for avg_speed; total_bytes prefers file_size with downloaded_bytes fallback; completed_at from updated_at (seconds).
    • Updated tauri and tray bridges to the new event shape; repo errors are logged with warn and never block the queue/UI.
    • Tests cover snapshot projection, fallbacks, duration floor, event filtering, error swallowing, event wiring, and SQLite integration.

Written for commit a91d5a3. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • History now reliably records completed downloads so History view features (grouping, filters, search, export), history-based redownload, and history purge function correctly.
  • New Features

    • Added a startup backfill that automatically repopulates History from previously completed downloads after upgrading, restoring missing entries without user action.

@github-actions github-actions Bot added documentation Improvements or additions to documentation rust labels Apr 27, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds an event-driven history recorder bridge that projects DownloadCompletedPersisted { id, snapshot } events into HistoryEntry rows and a startup backfill that replays completed downloads to repopulate missing history entries.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Adds Unreleased → Fixed entry documenting the history gap and the new recorder/backfill fix.
Event bridges
src-tauri/src/adapters/driven/event/history_recorder_bridge.rs, src-tauri/src/adapters/driven/event/stats_recorder_bridge.rs, src-tauri/src/adapters/driven/event/tauri_bridge.rs, src-tauri/src/adapters/driven/event/mod.rs
New history recorder bridge added and exported; stats bridge simplified to use the completion snapshot instead of repo lookups; tauri event matcher updated to the new DownloadCompletedPersisted { id, snapshot } shape.
Application wiring
src-tauri/src/lib.rs
Re-exports and wires spawn_history_recorder_bridge and backfill_history_for_completed_downloads; spawns the bridge and a non-blocking async backfill task at startup.
Backfill service
src-tauri/src/application/services/history_backfill.rs, src-tauri/src/application/services/mod.rs
New backfill function enumerates Completed downloads, skips those already in history, synthesizes snapshots→HistoryEntry and records them; per-download errors are logged and skipped.
Domain & Queue changes
src-tauri/src/domain/event.rs, src-tauri/src/application/services/queue_manager.rs
Adds DownloadCompletedSnapshot and projection helpers (to_history_entry, to_stats_record); DownloadCompletedPersisted now carries the snapshot; QueueManager builds and emits the snapshot on completion.
Adapters / tray
src-tauri/src/adapters/driven/tray/activity_tracker.rs
Event handling and tests updated to pattern-match DownloadCompletedPersisted { id, .. } including the snapshot field.
Tests / SQLite smoke
src-tauri/...
Unit and integration tests updated to assert projections from snapshot-bearing events; SQLite smoke tests adapted to verify projection from events rather than repo state.

Sequence Diagram

sequenceDiagram
    participant EB as EventBus
    participant Bridge as History Recorder Bridge
    participant HR as HistoryRepository
    participant DR as DownloadRepository (backfill)
    EB->>Bridge: Publish DownloadCompletedPersisted { id, snapshot }
    Bridge->>Bridge: snapshot.to_history_entry() (choose bytes, duration→s floor 1, avg_speed)
    Bridge->>HR: record(HistoryEntry)
    HR-->>Bridge: Ok / Err
    Note right of Bridge: on Err -> tracing::warn! (swallow)
    Note over DR,Bridge: Startup backfill task
    DR->>DR: find_by_state(Completed)
    DR-->>Bridge: list of Completed downloads
    Bridge->>HR: for each download: if no history -> record(synthesized HistoryEntry)
    HR-->>Bridge: Ok / Err (Err logged, continue)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"I nibble bytes and stitch the past,
Events hop in — I make them last.
Snapshots inked, history unfurled,
I stitch the downloads of the world. 🐇📜"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix(core): record completed downloads into history via event bus bridge' accurately summarizes the main change—it directly addresses the core issue (empty history table) and describes the solution (event bus bridge recording).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qa-history-recorder-bridge

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 12: The combined changelog entry merges two distinct changes into one
bullet; split them into two separate '-' bullets so the history-recorder note
(mentioning spawn_history_recorder_bridge, DownloadCompletedPersisted,
HistoryRepository::record, HistoryEntry projection rules) is one item and the
accessibility skip-link change (mentioning AppLayout, id="main-content",
tabIndex={-1}, sr-only reveal) is its own item; preserve the existing wording
for each but insert a newline and leading '-' before the skip-link paragraph to
make it a separate changelog bullet.

In `@src-tauri/src/adapters/driven/event/history_recorder_bridge.rs`:
- Around line 30-62: The subscriber record_for_event currently only reads id
from DomainEvent::DownloadCompletedPersisted and then re-queries download_repo
(using download_repo.find_by_id) which can race and produce missing or stale
data; update the fix by changing the event payload to carry the downloaded-row
projection (e.g., include the Download snapshot fields used by
derive_history_entry on DomainEvent::DownloadCompletedPersisted) or,
alternatively, move the history_repo.record call into the same completion/write
path that persists the Download so it happens atomically; update the event
construction site to populate the new fields and then change record_for_event to
use the provided snapshot instead of calling find_by_id, keeping references to
DomainEvent::DownloadCompletedPersisted, record_for_event, derive_history_entry,
and history_repo.record to locate and modify the code.

In `@src-tauri/src/lib.rs`:
- Around line 430-438: The startup currently only subscribes future
DownloadCompletedPersisted events via spawn_history_recorder_bridge, so
already-completed downloads lack history rows; add a one-time backfill run at
startup that queries the download repository for downloads with status Completed
(using download_repo_for_history_bridge or the same download repo API), for each
completed download check for an existing history row in history_repo_for_bridge
and insert a corresponding history record (or emit/handle a synthetic
DownloadCompletedPersisted into the same recorder logic) for any missing
entries; place this backfill invocation just before or after
spawn_history_recorder_bridge to ensure the same data-shaping/insert code is
reused and avoid duplicating logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfb1f3ee-38c2-4d35-a60d-031dda339471

📥 Commits

Reviewing files that changed from the base of the PR and between b8f73f3 and 4290c03.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src-tauri/src/adapters/driven/event/history_recorder_bridge.rs
  • src-tauri/src/adapters/driven/event/mod.rs
  • src-tauri/src/lib.rs

Comment thread CHANGELOG.md Outdated
Comment thread src-tauri/src/adapters/driven/event/history_recorder_bridge.rs
Comment thread src-tauri/src/lib.rs Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="CHANGELOG.md">

<violation number="1" location="CHANGELOG.md:12">
P3: This changelog item accidentally includes an unrelated skip-link paragraph after the history bridge fix, creating a duplicated/misfiled entry.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread CHANGELOG.md Outdated
### Fixed

- Keyboard accessibility: the app now renders a `SkipLink` ("Skip to main content" / "Aller au contenu principal") as the first focusable element in `AppLayout`, paired with `id="main-content"` + `tabIndex={-1}` on the `<main>` landmark. The link stays visually hidden via `sr-only` until focused, then reveals itself in the top-left corner; activating it programmatically focuses the main content (with `event.preventDefault()` so the URL hash stays clean for SPA routing). This addresses the WCAG 2.1 "bypass blocks" success criterion and gives keyboard users a single-Tab shortcut over the 10-item Sidebar. Note: the QA report (#116) reproduced via `tauri-pilot press Tab` is a known false positive — synthetic `KeyboardEvent` dispatched by the pilot does not trigger the webview's native focus-traversal behaviour, so the issue title's "first Tab keypress leaves focus on BODY" is a measurement artefact rather than a real-user bug. The skip-link is shipped anyway because it is a genuine a11y improvement (issue #116).
- History view stayed empty even after dozens of completed downloads: `HistoryRepository::record` had no production caller, so the `history` table was never written. The Statistics view (which reads the `statistics` table via `spawn_stats_recorder_bridge`) showed the right numbers, but every history-driven feature — the History view's group/filter/search/export, `download_redownload {sourceKind:"history"}` (P0.9), and the `history_purge_older_than` worker (P0.14) — had nothing to operate on. A new `spawn_history_recorder_bridge` mirrors the stats recorder pattern: it subscribes to `DownloadCompletedPersisted` on the event bus, looks up the persisted `Download` aggregate via the write repo, projects it into a `HistoryEntry` (`total_bytes` prefers `file_size` and falls back to `downloaded_bytes`; `updated_at - created_at` is converted from milliseconds to seconds with a `1`-second floor so an instant completion does not divide by zero in `avg_speed`; `completed_at` is the `updated_at` epoch in seconds, matching the Unix-seconds contract documented on `HistoryFilter::date_from`/`date_to`) and calls `record`. The bridge swallows download-repo and history-repo failures with `tracing::warn!` so a write glitch never propagates back into the queue/UI flow. Wired in `lib.rs` alongside the existing tauri/notification/download-log/progress/stats bridges (issue: vortex-qa BUG #1). ("Skip to main content" / "Aller au contenu principal") as the first focusable element in `AppLayout`, paired with `id="main-content"` + `tabIndex={-1}` on the `<main>` landmark. The link stays visually hidden via `sr-only` until focused, then reveals itself in the top-left corner; activating it programmatically focuses the main content (with `event.preventDefault()` so the URL hash stays clean for SPA routing). This addresses the WCAG 2.1 "bypass blocks" success criterion and gives keyboard users a single-Tab shortcut over the 10-item Sidebar. Note: the QA report (#116) reproduced via `tauri-pilot press Tab` is a known false positive — synthetic `KeyboardEvent` dispatched by the pilot does not trigger the webview's native focus-traversal behaviour, so the issue title's "first Tab keypress leaves focus on BODY" is a measurement artefact rather than a real-user bug. The skip-link is shipped anyway because it is a genuine a11y improvement (issue #116).
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 27, 2026

Choose a reason for hiding this comment

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

P3: This changelog item accidentally includes an unrelated skip-link paragraph after the history bridge fix, creating a duplicated/misfiled entry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CHANGELOG.md, line 12:

<comment>This changelog item accidentally includes an unrelated skip-link paragraph after the history bridge fix, creating a duplicated/misfiled entry.</comment>

<file context>
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 
-- Keyboard accessibility: the app now renders a `SkipLink` ("Skip to main content" / "Aller au contenu principal") as the first focusable element in `AppLayout`, paired with `id="main-content"` + `tabIndex={-1}` on the `<main>` landmark. The link stays visually hidden via `sr-only` until focused, then reveals itself in the top-left corner; activating it programmatically focuses the main content (with `event.preventDefault()` so the URL hash stays clean for SPA routing). This addresses the WCAG 2.1 "bypass blocks" success criterion and gives keyboard users a single-Tab shortcut over the 10-item Sidebar. Note: the QA report (#116) reproduced via `tauri-pilot press Tab` is a known false positive — synthetic `KeyboardEvent` dispatched by the pilot does not trigger the webview's native focus-traversal behaviour, so the issue title's "first Tab keypress leaves focus on BODY" is a measurement artefact rather than a real-user bug. The skip-link is shipped anyway because it is a genuine a11y improvement (issue #116).
+- History view stayed empty even after dozens of completed downloads: `HistoryRepository::record` had no production caller, so the `history` table was never written. The Statistics view (which reads the `statistics` table via `spawn_stats_recorder_bridge`) showed the right numbers, but every history-driven feature — the History view's group/filter/search/export, `download_redownload {sourceKind:"history"}` (P0.9), and the `history_purge_older_than` worker (P0.14) — had nothing to operate on. A new `spawn_history_recorder_bridge` mirrors the stats recorder pattern: it subscribes to `DownloadCompletedPersisted` on the event bus, looks up the persisted `Download` aggregate via the write repo, projects it into a `HistoryEntry` (`total_bytes` prefers `file_size` and falls back to `downloaded_bytes`; `updated_at - created_at` is converted from milliseconds to seconds with a `1`-second floor so an instant completion does not divide by zero in `avg_speed`; `completed_at` is the `updated_at` epoch in seconds, matching the Unix-seconds contract documented on `HistoryFilter::date_from`/`date_to`) and calls `record`. The bridge swallows download-repo and history-repo failures with `tracing::warn!` so a write glitch never propagates back into the queue/UI flow. Wired in `lib.rs` alongside the existing tauri/notification/download-log/progress/stats bridges (issue: vortex-qa BUG #1). ("Skip to main content" / "Aller au contenu principal") as the first focusable element in `AppLayout`, paired with `id="main-content"` + `tabIndex={-1}` on the `<main>` landmark. The link stays visually hidden via `sr-only` until focused, then reveals itself in the top-left corner; activating it programmatically focuses the main content (with `event.preventDefault()` so the URL hash stays clean for SPA routing). This addresses the WCAG 2.1 "bypass blocks" success criterion and gives keyboard users a single-Tab shortcut over the 10-item Sidebar. Note: the QA report (#116) reproduced via `tauri-pilot press Tab` is a known false positive — synthetic `KeyboardEvent` dispatched by the pilot does not trigger the webview's native focus-traversal behaviour, so the issue title's "first Tab keypress leaves focus on BODY" is a measurement artefact rather than a real-user bug. The skip-link is shipped anyway because it is a genuine a11y improvement (issue #116).
 - `plugin_report_broken` errored on every official plugin because their `plugin.toml` manifests do not declare a `repository` field, while the handler in `application/commands/report_broken_plugin.rs` requires `manifest.repository_url()` to be `Some` to build the GitHub issue URL. The handler now falls back to the local plugin store cache (`PluginStoreEntry.repository`, populated from `registry.toml` via `plugin_store_refresh`) when the loaded manifest is missing the field, so the feature works for the four official plugins without requiring a re-publish. The validation message now points at the actual TOML field (`[plugin].repository` in `plugin.toml`) instead of the internal Rust field name `repository_url`, which only exists on `PluginInfo` (issue #115).
 - Statistics view KPIs derived from the `statistics` table (total files, daily volume, average and peak speed) stayed at zero even when completed downloads existed: `StatsRepository::record_completed` had no production caller, so the daily rollup table was never written. A new `spawn_stats_recorder_bridge` subscribes to `DownloadCompletedPersisted` on the event bus, looks up the persisted `Download` aggregate, derives `(bytes, avg_speed)` (`file_size` falling back to `downloaded_bytes`; `updated_at - created_at` is converted from milliseconds to seconds with a `1`-second floor so an instant completion does not divide by zero) and calls `record_completed`. The bridge swallows repo and stats failures with `tracing::warn!` so a stats glitch never propagates back into the queue/UI flow. Wired alongside the existing `tauri`/`notification`/`download_log`/`progress` bridges in `lib.rs`. KPIs sourced from `downloads` (`success_rate`, `top_hosts`) keep their existing read path so no migration is needed (issue #114).
</file context>
Suggested change
- History view stayed empty even after dozens of completed downloads: `HistoryRepository::record` had no production caller, so the `history` table was never written. The Statistics view (which reads the `statistics` table via `spawn_stats_recorder_bridge`) showed the right numbers, but every history-driven feature — the History view's group/filter/search/export, `download_redownload {sourceKind:"history"}` (P0.9), and the `history_purge_older_than` worker (P0.14) — had nothing to operate on. A new `spawn_history_recorder_bridge` mirrors the stats recorder pattern: it subscribes to `DownloadCompletedPersisted` on the event bus, looks up the persisted `Download` aggregate via the write repo, projects it into a `HistoryEntry` (`total_bytes` prefers `file_size` and falls back to `downloaded_bytes`; `updated_at - created_at` is converted from milliseconds to seconds with a `1`-second floor so an instant completion does not divide by zero in `avg_speed`; `completed_at` is the `updated_at` epoch in seconds, matching the Unix-seconds contract documented on `HistoryFilter::date_from`/`date_to`) and calls `record`. The bridge swallows download-repo and history-repo failures with `tracing::warn!` so a write glitch never propagates back into the queue/UI flow. Wired in `lib.rs` alongside the existing tauri/notification/download-log/progress/stats bridges (issue: vortex-qa BUG #1). ("Skip to main content" / "Aller au contenu principal") as the first focusable element in `AppLayout`, paired with `id="main-content"` + `tabIndex={-1}` on the `<main>` landmark. The link stays visually hidden via `sr-only` until focused, then reveals itself in the top-left corner; activating it programmatically focuses the main content (with `event.preventDefault()` so the URL hash stays clean for SPA routing). This addresses the WCAG 2.1 "bypass blocks" success criterion and gives keyboard users a single-Tab shortcut over the 10-item Sidebar. Note: the QA report (#116) reproduced via `tauri-pilot press Tab` is a known false positive — synthetic `KeyboardEvent` dispatched by the pilot does not trigger the webview's native focus-traversal behaviour, so the issue title's "first Tab keypress leaves focus on BODY" is a measurement artefact rather than a real-user bug. The skip-link is shipped anyway because it is a genuine a11y improvement (issue #116).
- History view stayed empty even after dozens of completed downloads: `HistoryRepository::record` had no production caller, so the `history` table was never written. The Statistics view (which reads the `statistics` table via `spawn_stats_recorder_bridge`) showed the right numbers, but every history-driven feature — the History view's group/filter/search/export, `download_redownload {sourceKind:"history"}` (P0.9), and the `history_purge_older_than` worker (P0.14) — had nothing to operate on. A new `spawn_history_recorder_bridge` mirrors the stats recorder pattern: it subscribes to `DownloadCompletedPersisted` on the event bus, looks up the persisted `Download` aggregate via the write repo, projects it into a `HistoryEntry` (`total_bytes` prefers `file_size` and falls back to `downloaded_bytes`; `updated_at - created_at` is converted from milliseconds to seconds with a `1`-second floor so an instant completion does not divide by zero in `avg_speed`; `completed_at` is the `updated_at` epoch in seconds, matching the Unix-seconds contract documented on `HistoryFilter::date_from`/`date_to`) and calls `record`. The bridge swallows download-repo and history-repo failures with `tracing::warn!` so a write glitch never propagates back into the queue/UI flow. Wired in `lib.rs` alongside the existing tauri/notification/download-log/progress/stats bridges (issue: vortex-qa BUG #1).
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src-tauri/src/domain/event.rs (1)

118-120: Make the snapshot the single source of truth for the download ID.

DownloadCompletedPersisted now carries id twice: once as the outer field and again inside snapshot.id. The Tauri bridge reads the outer field, while the history/stats bridges use the snapshot, so a mismatched constructor would send different parts of the system to different downloads. I'd drop the outer id or derive it from snapshot everywhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/domain/event.rs` around lines 118 - 120,
DownloadCompletedPersisted currently duplicates the download ID (outer id:
DownloadId and snapshot.id: DownloadCompletedSnapshot.id); remove the outer id
field from the DownloadCompletedPersisted enum/struct and update all
constructors and matchers to only store the snapshot
(DownloadCompletedSnapshot), then update consumers (the Tauri bridge and
history/stats bridges) to derive the DownloadId from snapshot.id (or add a small
accessor on DownloadCompletedSnapshot if helpful) so every code path reads the
ID from the snapshot single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/application/services/queue_manager.rs`:
- Around line 284-287: The code unconditionally republishes
DownloadCompletedPersisted when download.state() == DownloadState::Completed;
change the guard to ensure at-most-once emission by checking a persisted flag or
DB marker before publishing: e.g., call a new or existing accessor like
download.is_completion_persisted() (or query the downloads repository by id) and
only call self.event_bus.publish(DomainEvent::DownloadCompletedPersisted { id,
snapshot }) if that check is false, then immediately mark the download as
completion-persisted (e.g., download.mark_completion_persisted() or
repository.update_completion_persisted(id, true)) after publishing; keep using
build_completed_snapshot for the event payload and adjust any model/repo methods
accordingly so downstream history_recorder_bridge and stats_recorder_bridge
receive the event exactly once.

---

Nitpick comments:
In `@src-tauri/src/domain/event.rs`:
- Around line 118-120: DownloadCompletedPersisted currently duplicates the
download ID (outer id: DownloadId and snapshot.id:
DownloadCompletedSnapshot.id); remove the outer id field from the
DownloadCompletedPersisted enum/struct and update all constructors and matchers
to only store the snapshot (DownloadCompletedSnapshot), then update consumers
(the Tauri bridge and history/stats bridges) to derive the DownloadId from
snapshot.id (or add a small accessor on DownloadCompletedSnapshot if helpful) so
every code path reads the ID from the snapshot single source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 808c3a8b-57c3-42f1-99eb-96debdb482d2

📥 Commits

Reviewing files that changed from the base of the PR and between 4290c03 and a52330e.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src-tauri/src/adapters/driven/event/history_recorder_bridge.rs
  • src-tauri/src/adapters/driven/event/stats_recorder_bridge.rs
  • src-tauri/src/adapters/driven/event/tauri_bridge.rs
  • src-tauri/src/adapters/driven/tray/activity_tracker.rs
  • src-tauri/src/application/services/history_backfill.rs
  • src-tauri/src/application/services/mod.rs
  • src-tauri/src/application/services/queue_manager.rs
  • src-tauri/src/domain/event.rs
  • src-tauri/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
  • src-tauri/src/application/services/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src-tauri/src/lib.rs
  • CHANGELOG.md

Comment thread src-tauri/src/application/services/queue_manager.rs
mpiton added 2 commits April 27, 2026 17:52
`HistoryRepository::record` had no production caller, leaving the
`history` table empty after every completed download. The History
view, redownload-from-history (P0.9), and the retention purge worker
(P0.14) all silently no-op as a result.

Mirror the existing `spawn_stats_recorder_bridge` pattern: subscribe
to `DownloadCompletedPersisted`, load the `Download` aggregate, derive
a `HistoryEntry` (file_size with downloaded_bytes fallback, ms→s
conversion with 1s floor for avg_speed) and call
`HistoryRepository::record`. Swallow repo errors with warn-level
tracing so a history write failure never propagates into the queue.

Tests:
- 7 unit tests covering the projection, fallback, ms→s conversion,
  duration floor, non-completed-event filtering, missing-download path,
  and error swallowing on both sides.
- 2 tokio integration tests (one against `RecordingHistoryRepo` via
  `TokioEventBus`, one against `SqliteHistoryRepo` via `setup_test_db`)
  to lock down the regression end-to-end.

Refs: vortex-qa BUG #1
@mpiton mpiton force-pushed the fix/qa-history-recorder-bridge branch from a52330e to a91d5a3 Compare April 27, 2026 15:54
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src-tauri/src/application/services/queue_manager.rs (1)

284-287: ⚠️ Potential issue | 🟠 Major

DownloadCompletedPersisted still needs at-most-once semantics.

Line 284 only checks whether the row is currently Completed, so a second DownloadCompleted for the same download republishes DownloadCompletedPersisted. The history bridge inserts unconditionally and the stats bridge increments unconditionally, so this duplicates history rows and double-counts stats. Please make this publish path idempotent, or make the downstream writes idempotent by download_id.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/application/services/queue_manager.rs` around lines 284 - 287,
The current code publishes DomainEvent::DownloadCompletedPersisted whenever
download.state() == DownloadState::Completed, causing duplicate persisted
events; change the logic in the block that calls
build_completed_snapshot(&download) and self.event_bus.publish(...) so it only
publishes once per download: either (a) detect the completion transition by
checking the previous state (publish only when state just changed to
DownloadState::Completed), or (b) add and check an explicit persisted flag on
the Download (e.g., download.is_completed_persisted() /
download.mark_completed_persisted()) and only publish when that flag is false,
setting it atomically before/after publish; alternatively ensure downstream
idempotency by including download_id as an idempotency key in
DomainEvent::DownloadCompletedPersisted and performing upsert-by-download_id in
the history and stats handlers—use the symbols DownloadState::Completed,
build_completed_snapshot, self.event_bus.publish, and
DomainEvent::DownloadCompletedPersisted to locate and update the code.
🧹 Nitpick comments (1)
src-tauri/src/domain/event.rs (1)

118-120: Use one source of truth for the download ID.

DownloadCompletedPersisted now carries both id and snapshot.id. src-tauri/src/adapters/driven/event/tauri_bridge.rs emits the outer id, while src-tauri/src/adapters/driven/event/history_recorder_bridge.rs and src-tauri/src/adapters/driven/event/stats_recorder_bridge.rs use snapshot.id. One mismatched publish would refresh one download in the UI and record a different one in history/stats. Prefer removing the outer id and reading snapshot.id everywhere, or enforce equality at construction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/domain/event.rs` around lines 118 - 120, The
DownloadCompletedPersisted variant currently carries both an outer id and
snapshot.id which can diverge; fix by removing the outer id and using
snapshot.id as the single source of truth: update the enum variant
DownloadCompletedPersisted to only hold snapshot: DownloadCompletedSnapshot
(remove DownloadId), then change all constructors and pattern matches (e.g., the
code in tauri_bridge.rs that emits the outer id, and history_recorder_bridge.rs
/ stats_recorder_bridge.rs that read snapshot.id) to stop passing/expecting the
outer id and instead read the id from snapshot.id; alternatively if you prefer
to keep both, add a single constructor/new function that enforces id ==
snapshot.id and use that everywhere to validate equality (return Result or
panic) so they never diverge.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src-tauri/src/application/services/queue_manager.rs`:
- Around line 284-287: The current code publishes
DomainEvent::DownloadCompletedPersisted whenever download.state() ==
DownloadState::Completed, causing duplicate persisted events; change the logic
in the block that calls build_completed_snapshot(&download) and
self.event_bus.publish(...) so it only publishes once per download: either (a)
detect the completion transition by checking the previous state (publish only
when state just changed to DownloadState::Completed), or (b) add and check an
explicit persisted flag on the Download (e.g., download.is_completed_persisted()
/ download.mark_completed_persisted()) and only publish when that flag is false,
setting it atomically before/after publish; alternatively ensure downstream
idempotency by including download_id as an idempotency key in
DomainEvent::DownloadCompletedPersisted and performing upsert-by-download_id in
the history and stats handlers—use the symbols DownloadState::Completed,
build_completed_snapshot, self.event_bus.publish, and
DomainEvent::DownloadCompletedPersisted to locate and update the code.

---

Nitpick comments:
In `@src-tauri/src/domain/event.rs`:
- Around line 118-120: The DownloadCompletedPersisted variant currently carries
both an outer id and snapshot.id which can diverge; fix by removing the outer id
and using snapshot.id as the single source of truth: update the enum variant
DownloadCompletedPersisted to only hold snapshot: DownloadCompletedSnapshot
(remove DownloadId), then change all constructors and pattern matches (e.g., the
code in tauri_bridge.rs that emits the outer id, and history_recorder_bridge.rs
/ stats_recorder_bridge.rs that read snapshot.id) to stop passing/expecting the
outer id and instead read the id from snapshot.id; alternatively if you prefer
to keep both, add a single constructor/new function that enforces id ==
snapshot.id and use that everywhere to validate equality (return Result or
panic) so they never diverge.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a91ebb2-2939-453d-b67a-e1ce21ab167f

📥 Commits

Reviewing files that changed from the base of the PR and between a52330e and a91d5a3.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • src-tauri/src/adapters/driven/event/history_recorder_bridge.rs
  • src-tauri/src/adapters/driven/event/mod.rs
  • src-tauri/src/adapters/driven/event/stats_recorder_bridge.rs
  • src-tauri/src/adapters/driven/event/tauri_bridge.rs
  • src-tauri/src/adapters/driven/tray/activity_tracker.rs
  • src-tauri/src/application/services/history_backfill.rs
  • src-tauri/src/application/services/mod.rs
  • src-tauri/src/application/services/queue_manager.rs
  • src-tauri/src/domain/event.rs
  • src-tauri/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
  • src-tauri/src/application/services/mod.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src-tauri/src/adapters/driven/event/mod.rs
  • src-tauri/src/adapters/driven/tray/activity_tracker.rs
  • CHANGELOG.md
  • src-tauri/src/application/services/history_backfill.rs

@mpiton mpiton merged commit 1223f5d into main Apr 27, 2026
7 checks passed
@mpiton mpiton deleted the fix/qa-history-recorder-bridge branch April 27, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation rust

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant