Skip to content

Add a signed in-app updater, and gate CI on lint, format, and clippy - #214

Merged
kristofferR merged 13 commits into
mainfrom
feat/auto-updater-and-quality-gates
Jul 25, 2026
Merged

Add a signed in-app updater, and gate CI on lint, format, and clippy#214
kristofferR merged 13 commits into
mainfrom
feat/auto-updater-and-quality-gates

Conversation

@kristofferR

@kristofferR kristofferR commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Adds the auto-updater from #173, mirroring how Carrier does it, plus a batch of
engineering-quality work that the updater's own CI run made worth doing first.

Signed in-app updater

The app only ever polled the GitHub releases API and pointed users at a download
page. It now installs a discovered release in place via tauri-plugin-updater,
using the keypair whose secrets were already configured on this repo — only the
public half was ever missing from tauri.conf.json.

Discovery stays separate from installation: a check never downloads anything, and
installing always needs explicit confirmation. A background task checks on launch
and every six hours, behind a new Automatic update checks setting.

Not every copy may replace itself. AUR packages are pacman-owned even though
they reuse the signed .deb, and Tauri's Linux updater only knows how to replace
an AppImage — so .deb/.rpm installs are routed to manual instructions rather
than a failing install. (Carrier only detects the AUR case; IPTV Checker ships
deb/rpm too, so it needs the extra branch.)

Two release artifacts are rewritten after tauri-action signs them, which would
otherwise leave latest.json holding signatures that no longer verify:

  • the macOS .dmg is notarized and stapled afterwards, and is what users actually
    download, so the updater should install it rather than the stock .app.tar.gz;
  • the Linux AppImage is rewritten in place by harden-appimage-runtime.sh.

Both are now re-signed and their manifest entries repointed by a shared script,
which writes the installer-suffixed key as well as the bare one — the updater
resolves {os}-{arch}-{installer} first. macOS installs the verified DMG itself
(engine::macos_dmg_update); Tauri still does the download and minisign
verification, so the bytes are already trusted.

The Homebrew cask now declares auto_updates true, the correct convention for a
self-updating app.

Quality work

  • CI gates — there was no clippy, no rustfmt --check, and no frontend linter
    at all. All three now run. Adding them surfaced two Rules-of-Hooks violations
    React would have thrown on: PlaylistReportPanel and ThumbnailPanel both
    returned early above several hooks, so the hook count changed the moment a
    playlist loaded or a row was selected. Clippy also found a doc comment a helper
    extraction had stolen from profile_bitrate.

  • Store performance — results were kept twice, in flatResults and in an
    index-keyed object spread on every batch, making a whole scan quadratic.
    Results now live only in flatResults with an append-only position map:

    Channels Before After
    1,000 3.0 ms 0.4 ms
    10,000 445 ms 17.6 ms
    50,000 13,761 ms 213 ms

    Kept as bun run perf:ui-batching so it cannot silently regress.

  • Preset fieldsScanPresetConfig listed its sixteen fields three times, so
    a new scan setting silently failed to round-trip unless all three were updated.
    A macro derives the struct and both copy directions from one list; a field
    AppSettings lacks now fails to compile. Persisted JSON is unchanged.

  • scan.rscompute_playlist_score was pure math sitting in the scan
    orchestrator. Moved to engine::playlist_score and tested at exact values
    rather than the previous "greater than zero".

  • Integration test — parse → check → export across the real parser, checker
    (against a mock server serving one 404) and exporters, parsing the exported M3U
    back. No WebDriver, runs everywhere.

Notes

  • The ~60 pre-existing accessibility findings are set to warn, not suppressed,
    so the gate could land without hiding them. Clearing them is its own pass.
  • too_many_arguments is allowed crate-wide and one large_enum_variant locally,
    both with reasoning inline.
  • No GUI/WebDriver E2E: tauri-driver has no macOS support, so it could not be
    verified locally. The integration test covers the same spine below the UI.

Ref #173

Validation

cargo clippy --all-targets -- -D warnings, cargo fmt --check, 283 Rust tests,
biome check, tsc --noEmit, 105 frontend tests, and bun run build all pass.
CodeRabbit preflight run before pushing: two findings fixed (shared in-flight
install-mode detection, bounded macOS relaunch wait). One declined — it assumed
the macOS path calls download_and_install, but it calls download, which does
signature verification only and no format inference.

Summary by CodeRabbit

  • New Features

    • Added signed in-app updates, including automatic update checks, installation, and manual update guidance.
    • Added an “Automatic update checks” setting.
    • Improved macOS and Linux updater artifacts and Homebrew self-update support.
    • Added playlist performance benchmarking tools.
  • Bug Fixes

    • Improved scan-result handling and history ordering.
    • Fixed update banners and installation flows across supported platforms.
  • Documentation

    • Added performance benchmark instructions and baseline guidance.
  • Tests

    • Expanded frontend, Rust, updater, scan pipeline, and performance coverage.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds signed in-app updates across Tauri and the frontend, changes scan results to flattened collections with indexed positions, extracts playlist scoring, adds UI performance benchmarking, and strengthens CI/release validation for updater artifacts and code quality.

Changes

Updater and release flow

Layer / File(s) Summary
Signed updater implementation
src-tauri/src/commands/updater.rs, src-tauri/src/engine/macos_dmg_update.rs, src-tauri/src/state.rs
Adds platform-aware update discovery, installation guards, automatic checks, macOS DMG replacement, relaunch handling, and updater state coordination.
Frontend updater integration
src/hooks/useUpdateCheck.ts, src/components/AppBanners.tsx, src/lib/updateState.ts, src/lib/tauri.ts, src/store/slices/uiSlice.ts
Connects backend update events and commands to update banners, installation actions, phases, confirmation messages, and error presentation.
Release publishing
.github/workflows/release.yml, scripts/patch-updater-manifest.sh, src-tauri/tauri.conf.release.json, src-tauri/tauri.conf.json
Builds updater artifacts, serializes matrix jobs, notarizes and republishes macOS assets, re-signs hardened Linux assets, patches manifests, and enables Homebrew auto-updates.

Scan and scoring

Layer / File(s) Summary
Flattened scan collections
src/hooks/useScan.helpers.ts, src/hooks/useScan.ts, src/store/*, src/components/ChannelTable.tsx, src/App.tsx
Replaces indexed result objects with flatResults and position maps, then updates batching, selectors, synchronization, and UI lookups.
Playlist scoring and validation
src-tauri/src/engine/playlist_score.rs, src-tauri/src/commands/scan.rs, src-tauri/tests/scan_pipeline_integration.rs
Adds tested weighted playlist scoring and an integration test covering parsing, probing, filtering, and M3U/CSV export round trips.

Tooling and maintenance

Layer / File(s) Summary
Quality checks and benchmarks
.github/workflows/ci.yml, package.json, scripts/benchmark-result-batching.ts, docs/ui-performance-baseline.md
Adds frontend linting, Rust formatting and Clippy checks, and a documented result-batching benchmark command.
Rust cleanup and compatibility updates
src-tauri/src/commands/*, src-tauri/src/engine/*, src-tauri/src/models/*, src-tauri/src/lib.rs
Applies formatting and Clippy-oriented refactors, simplifies parsing and sorting expressions, updates defaults and validation, and adjusts ownership patterns without changing the primary behavior of unrelated modules.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit checks updates beneath the moon,
While flat results hop in a tidy tune.
Signed DMGs sparkle, AppImages fly,
Scores bloom bright as benchmarks race by.
“Lint!” cries the hare—“all green before dawn!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title accurately summarizes the two main changes: a signed in-app updater and stricter CI checks for lint, format, and clippy.
✨ 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 feat/auto-updater-and-quality-gates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@kristofferR

Copy link
Copy Markdown
Owner Author

@codex review

1 similar comment
@kristofferR

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56150a0423

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/commands/updater.rs Outdated
@kristofferR

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kristofferR

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

The app only ever polled the GitHub releases API and pointed users at a
download page; updating meant fetching an installer by hand. Wire up
tauri-plugin-updater so a discovered release can be installed in place.

Discovery stays separate from installation: a check never downloads
anything, and installing always needs explicit confirmation. A background
task checks on launch and every six hours while the preference is on.

Not every copy may replace itself. AUR packages are pacman-owned even
though they reuse the signed .deb, and Tauri's Linux updater only knows
how to replace an AppImage — so .deb/.rpm installs are routed to manual
instructions instead of a failing install.

Two release artifacts are rewritten after tauri-action signs them, which
would leave latest.json holding signatures that no longer verify:

  * the macOS .dmg is notarized and stapled afterwards (and is what users
    actually download, so the updater should install it rather than the
    stock .app.tar.gz);
  * the Linux AppImage is rewritten in place by the hardening script.

Both are now re-signed and their manifest entries repointed by a shared
script. It writes the installer-suffixed key as well as the bare one,
since the updater resolves "{os}-{arch}-{installer}" first.
CI type-checked, tested, built, and fuzzed the parser, but nothing
enforced lints or formatting: there was no clippy or rustfmt step, and
no frontend linter configured at all.

Adding the gates surfaced two Rules-of-Hooks violations that React would
have thrown on. PlaylistReportPanel returned early when no playlist was
loaded, and ThumbnailPanel when no channel was selected — both above
several useMemo/useCallback/useEffect calls, so the hook count changed
the moment a playlist loaded or a row was selected. The early returns now
sit below every hook.

Clippy also caught a doc comment that a helper extraction had stolen:
profile_bitrate's explanation of why it spawns ffmpeg directly had ended
up on spawn_stderr_tail_reader.

Two lints are allowed rather than fixed, with the reasoning inline:
too_many_arguments (crate-wide, on the scan/ffmpeg/cast worker functions)
and one large_enum_variant on a checkpoint line that is destructured
immediately after deserializing.

The ~60 pre-existing accessibility findings are set to warn rather than
suppressed, so the gate can land now without hiding them.
ScanPresetConfig listed its sixteen fields three times — the struct,
from_settings and apply_to_settings — so adding a scan setting meant
remembering all three or the setting silently failed to round-trip
through presets. A macro now derives the struct and both copy directions
from one list, and a field name that AppSettings does not have fails to
compile. The persisted JSON shape is unchanged; a test pins the preset
keys as a subset of the settings keys.

compute_playlist_score and its helpers were pure math sitting in the
middle of the scan orchestrator. Moved to engine::playlist_score, which
documents the weighting and covers the parts that were previously only
asserted to be "greater than zero" — the median, the codec tiers, the
resolution-text fallback, and the three subscores at exact values.
The UI held every result twice: once in `flatResults` and once in an
index-keyed lookup object. Folding a batch spread that object, so the
cost of applying one batch grew with the number of results already in —
a whole scan was quadratic.

Results now live only in `flatResults`, with an append-only map from
channel index to position. A result keeps its position for the lifetime
of a scan, so that map is mutated in place and never copied; readers
depend on `flatResults`, whose identity still changes once per batch.

Replaying a scan through applyResultUpdates, 16 results per batch:

     1,000 channels      3.0ms ->   0.4ms
    10,000 channels    445.0ms ->  17.6ms
    50,000 channels  13,761.5ms -> 213.0ms

The benchmark is kept as `bun run perf:ui-batching` so the fold does not
silently regress, and documented in the UI performance baseline.
Each stage had unit tests, but nothing exercised a playlist across all
three. This drives the real parser, the real checker against a mock
server that serves one 404 among the streams, and the real exporters,
then parses the exported M3U back to confirm the dead channel is absent
and the survivors kept their names.

It reuses the existing mock-server harness in this file, so it needs no
WebDriver and runs on every platform in CI.
Share the in-flight install-mode detection between concurrent callers, so
the startup effect and a manual check cannot both shell out to pacman.
The promise is cleared on failure so a later attempt still retries.

Bound the macOS relaunch helper's wait at ~60s. It polled for the old
process indefinitely, so an app that hung on quit would have left an
immortal /bin/sh behind; now the new bundle is opened either way.
The window returned by create_window_from_main_config is only consumed by
the macOS-only vibrancy setup, so on Linux and Windows the binding is
unused. macOS clippy never saw it and CI only runs clippy on Linux, so
the new gate surfaced it on the first run.

Allowed on the platforms where the binding genuinely has no consumer,
rather than renaming it to _window and reading oddly in the macOS block.
Non-Linux installs were all reported as self-updatable, so someone running
the portable .zip got the same Install button as an installed copy. Tauri
would have run the NSIS installer, which updates a separate installed app
and leaves the portable executable they launched untouched — the relaunch
would come back on the old version and offer the update again.

Detected by the uninstall.exe that Tauri's NSIS installer writes beside
the executable (WriteUninstaller "$INSTDIR\uninstall.exe"); the portable
zip ships only the app and its ffmpeg sidecars.

Also folds the duplicated platform detection in current_install_mode and
current_manual_url into one current_install_source.
@kristofferR
kristofferR force-pushed the feat/auto-updater-and-quality-gates branch from 223e221 to 4ab6654 Compare July 25, 2026 01:56
@kristofferR
kristofferR changed the base branch from main to chore/biome-formatting July 25, 2026 01:56
Never run install detection on the async runtime thread: open_manual_update
called current_install_mode_off_main and then re-derived the URL with a
direct current_install_source(), which shells out to pacman on Linux. One
off-main detection now serves both, and manual_url() being None is exactly
the built-in case the is_manual() check was testing.

Fall back to the privileged copy when the macOS bundle cannot be renamed
aside because the scratch dir is on another volume (CrossesDevices), not
just when permission is denied.

Guard installUpdate against re-entry. The banner disables its button while
installing, but a menu action could still re-enter and the backend would
reject it as an error rather than a no-op.

Two drive-bys in files this branch already touches: a test comment in
ffmpeg.rs that contradicted its own assertions, and a dead grid-cols-1 in
the settings panel that grid-cols-2 already overrode.
@kristofferR

Copy link
Copy Markdown
Owner Author

@codex review

@kristofferR

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: e8fc4a45a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The allow exempted only non-Linux/Windows targets, but every build leaves
some variant unconstructed: each is built on exactly one OS, so the Linux
build had no constructor for WindowsPortable and clippy failed there.

macOS reproduces the same lint for SystemPackage and WindowsPortable in a
non-test lib build, which is how this was verified rather than guessed at.
@kristofferR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@kristofferR

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 58 minutes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7d43a9ead

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/useUpdateCheck.ts Outdated
Comment thread src/hooks/useUpdateCheck.ts Outdated
Comment thread src-tauri/src/commands/updater.rs Outdated
Route macOS bundles on read-only volumes to a manual update. Launched
straight from the mounted .dmg, or from an App Translocation path, the
running bundle sits on a read-only mount; the privileged-copy fallback
cannot make that writable, so confirming an update always failed. Detected
with statfs MNT_RDONLY, which distinguishes a read-only mount from an
ordinary /Applications permission problem that the fallback does handle.

Stop a finishing update check from clearing an install that started while
it was in flight — the finally reset the phase to idle unconditionally,
re-enabling the button mid-download.

Subscribe to the discovery event before reading the snapshot. The backend
emits once and then remembers the version, so an event landing between the
snapshot returning and the listener attaching was lost for the session.
@kristofferR

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@kristofferR

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 11cc8803bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kristofferR

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useScan.helpers.ts (1)

80-112: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Metrics double-count when a batch contains two updates for the same channel index.

positions.set(result.index, next.length) mutates the shared positions map immediately when a new index is first seen (line 104). If a second update for the same result.index arrives later in the same batch (e.g. an initial scan://channel-result event and the finalized scan://channel-results-batch entry coalescing into one RAF flush), the second iteration finds position != null via positions.get(...), but previousResult is still computed from previous.flatResults[position] (line 82) — the pre-batch array, which doesn't have an entry at that brand-new position. previousResult incorrectly resolves to null, so presentCount gets incremented a second time for the same channel, and lowFpsCount/mislabeledCount skip the correct before/after comparison. Since these are running counters (not recomputed from scratch), the drift is permanent for the rest of the scan.

flatResults itself stays correct (final push/replace uses the live flatResults var), so only the derived UI metrics are affected — but they're user-visible (scan summary counts).

🐛 Proposed fix — read from the live, already-batch-updated array
   for (const result of batch) {
     const position = positions.get(result.index);
-    const previousResult = position == null ? null : (previous.flatResults[position] ?? null);
+    const previousResult = position == null ? null : (flatResults[position] ?? null);

Consider also adding a regression test in tests/useScan.helpers.test.ts covering a batch with two entries sharing the same index.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useScan.helpers.ts` around lines 80 - 112, Use the live,
already-updated flat-results array when deriving previous state for each item in
the batch, rather than always reading previous.flatResults. Update the lookup
around positions and previousResult in the batch-processing loop so a repeated
result.index compares against the first update’s stored result and does not
double-count present, low-FPS, or mislabeled metrics. Add a regression test in
useScan.helpers.test.ts covering two batch entries with the same index.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 138-139: In .github/workflows/release.yml#L138-L139, update the
DMG discovery command to use find with -print -quit and tolerate command failure
so missing directories or multiple matches reach the explicit “No .dmg found.”
check. In .github/workflows/release.yml#L156-L171, validate release_id before
constructing release asset URLs and fail or skip cleanup with a clear message
when it is empty.

In `@scripts/patch-updater-manifest.sh`:
- Around line 77-98: Move the latest.json read, jq update, replacement, and gh
release upload out of the per-platform macOS/Linux path and into a single
post-matrix finalizer. Have that finalizer apply all platform and installer
entries from the completed target outputs to one manifest, then upload
latest.json exactly once, preventing concurrent writers from overwriting each
other’s updates.

In `@src-tauri/src/commands/updater.rs`:
- Around line 280-306: Memoize the result of current_install_mode for the
process lifetime, using an appropriate thread-safe one-time or lazy cache around
the existing detection logic. Ensure update_install_mode, install_update, and
automatic checks reuse the cached UpdateInstallMode without rerunning the
pacman, paru, or yay subprocess checks, while preserving the existing
platform-specific current_install_mode_off_main behavior.

In `@src-tauri/src/engine/macos_dmg_update.rs`:
- Around line 40-51: In the update scratch-directory setup, replace the
create_dir_all call with exclusive directory creation so an existing file,
directory, or symlink at work_dir causes the operation to fail. Keep the
existing work_dir naming and subsequent dmg_path and mountpoint handling
unchanged.

In `@src-tauri/src/engine/xtream.rs`:
- Around line 349-355: In the category_id extraction within the group
construction, remove the no-op Option::or fallback around v.as_str(). Keep the
string conversion branch direct, preserving the separate numeric-ID handling
match below.

In `@src-tauri/src/lib.rs`:
- Around line 1005-1006: Add the Tauri updater permission/capability alongside
the existing default capability used by the updater setup around
spawn_automatic_update_checks. Ensure the configured capability includes
updater:default or permission:updater:default so Rust updater plugin APIs are
available, preferably reusing the existing capability configuration unless a
dedicated updater capability is required.

In `@src/hooks/useUpdateCheck.ts`:
- Around line 65-71: In checkForUpdates, guard the initial
setUpdatePhase("checking") call so an in-progress installing phase is preserved
and the update check exits before changing state. Reuse the existing
update-phase state from getStore and keep installUpdate’s installing re-entry
protection intact.

In `@src/lib/updateState.ts`:
- Around line 6-12: Move the Rust-mirrored UpdateInstallMode and
UpdateInstallKind declarations from updateState.ts into src/lib/types.ts
alongside UpdateCheckResult, then update imports and references such as tauri.ts
to use the consolidated types module. Leave only presentation helpers in
updateState.ts and preserve the existing type shapes.

---

Outside diff comments:
In `@src/hooks/useScan.helpers.ts`:
- Around line 80-112: Use the live, already-updated flat-results array when
deriving previous state for each item in the batch, rather than always reading
previous.flatResults. Update the lookup around positions and previousResult in
the batch-processing loop so a repeated result.index compares against the first
update’s stored result and does not double-count present, low-FPS, or mislabeled
metrics. Add a regression test in useScan.helpers.test.ts covering two batch
entries with the same index.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 2295238b-c2f6-4a50-9440-29bfa2fba63f

📥 Commits

Reviewing files that changed from the base of the PR and between 65450e1 and 11cc880.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (59)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • docs/ui-performance-baseline.md
  • package.json
  • scripts/benchmark-result-batching.ts
  • scripts/patch-updater-manifest.sh
  • src-tauri/Cargo.toml
  • src-tauri/examples/backend_bench.rs
  • src-tauri/src/commands/history.rs
  • src-tauri/src/commands/mod.rs
  • src-tauri/src/commands/playlist.rs
  • src-tauri/src/commands/recent.rs
  • src-tauri/src/commands/saved.rs
  • src-tauri/src/commands/scan.rs
  • src-tauri/src/commands/server_test.rs
  • src-tauri/src/commands/settings.rs
  • src-tauri/src/commands/updater.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/checker.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/ffmpeg.rs
  • src-tauri/src/engine/macos_dmg_update.rs
  • src-tauri/src/engine/mod.rs
  • src-tauri/src/engine/parser.rs
  • src-tauri/src/engine/playlist_score.rs
  • src-tauri/src/engine/proxy_common.rs
  • src-tauri/src/engine/resume.rs
  • src-tauri/src/engine/stream_proxy.rs
  • src-tauri/src/engine/xtream.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/models/scan.rs
  • src-tauri/src/models/settings.rs
  • src-tauri/src/state.rs
  • src-tauri/tauri.conf.json
  • src-tauri/tauri.conf.release.json
  • src-tauri/tests/scan_pipeline_integration.rs
  • src/App.tsx
  • src/components/AppBanners.tsx
  • src/components/ChannelTable.tsx
  • src/components/PlaylistReportPanel.tsx
  • src/components/SFSymbols.tsx
  • src/components/SettingsPanel.tsx
  • src/components/ThumbnailPanel.tsx
  • src/hooks/usePlaylistSources.ts
  • src/hooks/useScan.helpers.ts
  • src/hooks/useScan.ts
  • src/hooks/useUpdateCheck.ts
  • src/index.css
  • src/lib/perf.ts
  • src/lib/tauri.ts
  • src/lib/types.ts
  • src/lib/updateState.ts
  • src/store/index.ts
  • src/store/slices/scanSlice.ts
  • src/store/slices/settingsSlice.ts
  • src/store/slices/uiSlice.ts
  • src/store/types.ts
  • tests/updateState.test.ts
  • tests/useScan.helpers.test.ts
💤 Files with no reviewable changes (1)
  • src-tauri/examples/backend_bench.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
src-tauri/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use Rust conventions: snake_case naming, 4-space indentation, thiserror for error types, and serde for serialization.

Files:

  • src-tauri/src/commands/mod.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/playlist_score.rs
  • src-tauri/src/engine/checker.rs
  • src-tauri/src/commands/saved.rs
  • src-tauri/src/engine/mod.rs
  • src-tauri/src/commands/server_test.rs
  • src-tauri/src/engine/resume.rs
  • src-tauri/src/commands/recent.rs
  • src-tauri/src/engine/proxy_common.rs
  • src-tauri/src/commands/playlist.rs
  • src-tauri/src/state.rs
  • src-tauri/src/engine/parser.rs
  • src-tauri/tests/scan_pipeline_integration.rs
  • src-tauri/src/engine/xtream.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/models/scan.rs
  • src-tauri/src/commands/history.rs
  • src-tauri/src/engine/stream_proxy.rs
  • src-tauri/src/commands/settings.rs
  • src-tauri/src/engine/macos_dmg_update.rs
  • src-tauri/src/commands/updater.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/ffmpeg.rs
  • src-tauri/src/models/settings.rs
  • src-tauri/src/commands/scan.rs
src-tauri/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src-tauri/src/**/*.rs: Keep backend responsibilities organized into models, engine, commands, state.rs, error.rs, and lib.rs according to their documented roles.
Use event-driven scanning: emit scan://channel-result events per channel, with frontend event updates batched using requestAnimationFrame.

Files:

  • src-tauri/src/commands/mod.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/playlist_score.rs
  • src-tauri/src/engine/checker.rs
  • src-tauri/src/commands/saved.rs
  • src-tauri/src/engine/mod.rs
  • src-tauri/src/commands/server_test.rs
  • src-tauri/src/engine/resume.rs
  • src-tauri/src/commands/recent.rs
  • src-tauri/src/engine/proxy_common.rs
  • src-tauri/src/commands/playlist.rs
  • src-tauri/src/state.rs
  • src-tauri/src/engine/parser.rs
  • src-tauri/src/engine/xtream.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/models/scan.rs
  • src-tauri/src/commands/history.rs
  • src-tauri/src/engine/stream_proxy.rs
  • src-tauri/src/commands/settings.rs
  • src-tauri/src/engine/macos_dmg_update.rs
  • src-tauri/src/commands/updater.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/ffmpeg.rs
  • src-tauri/src/models/settings.rs
  • src-tauri/src/commands/scan.rs
src-tauri/src/commands/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Implement Tauri IPC commands in the commands module, organized by playlist, scan, export, and settings responsibilities.

Files:

  • src-tauri/src/commands/mod.rs
  • src-tauri/src/commands/saved.rs
  • src-tauri/src/commands/server_test.rs
  • src-tauri/src/commands/recent.rs
  • src-tauri/src/commands/playlist.rs
  • src-tauri/src/commands/history.rs
  • src-tauri/src/commands/settings.rs
  • src-tauri/src/commands/updater.rs
  • src-tauri/src/commands/scan.rs
src-tauri/src/engine/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Place core IPTV processing logic, including parsing, checking, ffmpeg, proxy, and resume functionality, in the engine module.

Files:

  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/playlist_score.rs
  • src-tauri/src/engine/checker.rs
  • src-tauri/src/engine/mod.rs
  • src-tauri/src/engine/resume.rs
  • src-tauri/src/engine/proxy_common.rs
  • src-tauri/src/engine/parser.rs
  • src-tauri/src/engine/xtream.rs
  • src-tauri/src/engine/stream_proxy.rs
  • src-tauri/src/engine/macos_dmg_update.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/ffmpeg.rs
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Define agent interfaces and implementations in TypeScript for agent systems
Use clear naming conventions for agent classes and methods
Document agent behavior and responsibilities in comments or docstrings
Implement error handling in agent methods

Files:

  • src/store/index.ts
  • scripts/benchmark-result-batching.ts
  • src/components/SFSymbols.tsx
  • src/lib/perf.ts
  • src/store/slices/uiSlice.ts
  • src/components/ChannelTable.tsx
  • src/lib/types.ts
  • src/lib/updateState.ts
  • src/store/slices/scanSlice.ts
  • tests/useScan.helpers.test.ts
  • tests/updateState.test.ts
  • src/App.tsx
  • src/hooks/usePlaylistSources.ts
  • src/components/SettingsPanel.tsx
  • src/components/ThumbnailPanel.tsx
  • src/lib/tauri.ts
  • src/components/AppBanners.tsx
  • src/components/PlaylistReportPanel.tsx
  • src/store/slices/settingsSlice.ts
  • src/store/types.ts
  • src/hooks/useScan.helpers.ts
  • src/hooks/useUpdateCheck.ts
  • src/hooks/useScan.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use strict TypeScript with no unused locals or parameters.

Files:

  • src/store/index.ts
  • src/components/SFSymbols.tsx
  • src/lib/perf.ts
  • src/store/slices/uiSlice.ts
  • src/components/ChannelTable.tsx
  • src/lib/types.ts
  • src/lib/updateState.ts
  • src/store/slices/scanSlice.ts
  • src/App.tsx
  • src/hooks/usePlaylistSources.ts
  • src/components/SettingsPanel.tsx
  • src/components/ThumbnailPanel.tsx
  • src/lib/tauri.ts
  • src/components/AppBanners.tsx
  • src/components/PlaylistReportPanel.tsx
  • src/store/slices/settingsSlice.ts
  • src/store/types.ts
  • src/hooks/useScan.helpers.ts
  • src/hooks/useUpdateCheck.ts
  • src/hooks/useScan.ts
src/components/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Implement components as functional React components using hooks, and use Tailwind CSS for styling instead of CSS-in-JS.

Files:

  • src/components/SFSymbols.tsx
  • src/components/ChannelTable.tsx
  • src/components/SettingsPanel.tsx
  • src/components/ThumbnailPanel.tsx
  • src/components/AppBanners.tsx
  • src/components/PlaylistReportPanel.tsx
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use a virtualized table via @tanstack/react-virtual for playlists containing 1000 or more channels.

Files:

  • src/components/SFSymbols.tsx
  • src/components/ChannelTable.tsx
  • src/components/SettingsPanel.tsx
  • src/components/ThumbnailPanel.tsx
  • src/components/AppBanners.tsx
  • src/components/PlaylistReportPanel.tsx
src/lib/types.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep frontend types in src/lib/types.ts synchronized with the Rust models.

Files:

  • src/lib/types.ts
🔇 Additional comments (79)
src/components/SFSymbols.tsx (1)

45-50: LGTM!

src/lib/perf.ts (1)

37-37: LGTM!

src/index.css (1)

534-534: LGTM!

src/components/ThumbnailPanel.tsx (1)

205-258: LGTM!

Also applies to: 502-502

src/components/PlaylistReportPanel.tsx (1)

163-166: LGTM!

Also applies to: 183-208, 229-241

.github/workflows/ci.yml (1)

43-44: LGTM!

Also applies to: 73-74, 86-92

src-tauri/src/commands/history.rs (1)

182-186: LGTM!

Also applies to: 384-384, 465-465, 521-521, 591-591

src-tauri/src/commands/playlist.rs (1)

187-191: LGTM!

Also applies to: 401-402

src-tauri/src/commands/recent.rs (1)

396-396: LGTM!

src-tauri/src/engine/proxy_common.rs (1)

88-94: LGTM!

src-tauri/src/engine/resume.rs (1)

78-82: LGTM!

Also applies to: 189-189

src-tauri/src/engine/stream_proxy.rs (1)

422-446: LGTM!

Also applies to: 904-904, 1114-1120, 1431-1431

src-tauri/src/commands/saved.rs (1)

80-84: LGTM!

src-tauri/src/commands/server_test.rs (1)

704-717: LGTM!

src-tauri/src/engine/cast_proxy.rs (1)

42-44: LGTM!

Also applies to: 169-169, 567-573, 681-681, 859-863, 954-955, 1543-1552, 1572-1575, 1657-1664, 1683-1705, 1714-1717, 1885-1887

src-tauri/src/engine/checker.rs (1)

332-332: LGTM!

src-tauri/src/engine/chromecast.rs (1)

243-243: LGTM!

src-tauri/src/engine/ffmpeg.rs (1)

555-558: LGTM!

Also applies to: 1499-1505, 1630-1640, 1652-1652, 2277-2277, 2517-2519

src-tauri/src/engine/parser.rs (1)

473-475: LGTM!

Also applies to: 492-492

.github/workflows/release.yml (4)

19-22: LGTM!


305-308: LGTM!


102-106: 📐 Maintainability & Code Quality

No change needed.

scripts/verify-production-binaries.sh is present in the repo, so the release workflow step can run.


194-200: 🗄️ Data Integrity & Integration

No issue with the .sig file path.

tauri signer sign <file> produces the detached signature as <file>.sig, so the macOS and AppImage cat "${release_asset}.sig" reads are valid.

src-tauri/Cargo.toml (1)

39-39: LGTM!

src-tauri/src/commands/mod.rs (1)

11-11: LGTM!

src-tauri/src/commands/updater.rs (9)

19-66: LGTM!


68-141: LGTM!


143-232: LGTM!


308-339: LGTM!


341-356: LGTM!


358-423: LGTM!


425-456: LGTM!


458-508: LGTM!


510-587: LGTM!

Also applies to: 589-694

src-tauri/src/engine/macos_dmg_update.rs (6)

15-28: LGTM!

Also applies to: 60-73


75-106: LGTM!


108-131: LGTM!


133-152: LGTM!

Also applies to: 162-200


202-240: LGTM!


153-161: 🩺 Stability & Availability

No change needed. The repo uses dtolnay/rust-toolchain@stable for the Rust toolchain, and ErrorKind::CrossesDevices is available on stable.

src-tauri/src/engine/mod.rs (1)

7-10: LGTM!

src-tauri/src/lib.rs (3)

1-5: LGTM!


192-194: LGTM!


562-562: LGTM!

Also applies to: 873-887

src/lib/updateState.ts (1)

22-55: LGTM!

src/lib/tauri.ts (1)

22-26: LGTM!

Also applies to: 313-341

src/hooks/useUpdateCheck.ts (5)

2-28: LGTM!


37-63: LGTM!


72-109: LGTM!


111-147: LGTM!


149-196: LGTM!

src-tauri/src/state.rs (1)

65-65: LGTM!

Also applies to: 84-96, 111-114

src-tauri/tauri.conf.json (1)

89-96: LGTM!

src-tauri/tauri.conf.release.json (1)

1-6: LGTM!

src-tauri/src/models/scan.rs (1)

21-23: LGTM!

Also applies to: 78-78, 94-95

src-tauri/src/models/settings.rs (1)

10-23: LGTM!

Also applies to: 38-40, 78-136, 201-201, 217-233, 273-287

src-tauri/src/commands/settings.rs (1)

200-205: LGTM!

Also applies to: 735-744, 929-929, 993-993

src/store/slices/settingsSlice.ts (1)

38-38: LGTM!

src/lib/types.ts (1)

324-324: LGTM!

Also applies to: 462-467

src/components/SettingsPanel.tsx (1)

698-715: LGTM!

Also applies to: 1128-1128

src/store/types.ts (2)

18-18: LGTM!

Also applies to: 55-57, 138-138, 160-160, 186-186


74-76: 🎯 Functional Correctness

No change needed.

applyResultUpdates already returns a new flatResults array when a batch changes results, so the store reference changes for React/Zustand subscriptions.

			> Likely an incorrect or invalid review comment.
src/store/slices/uiSlice.ts (1)

34-34: LGTM!

Also applies to: 100-100

src/App.tsx (1)

49-49: LGTM!

Also applies to: 134-134, 567-567, 1166-1166, 1458-1458

src/components/AppBanners.tsx (1)

1-10: LGTM!

Also applies to: 33-49, 151-166

tests/updateState.test.ts (1)

1-95: LGTM!

src-tauri/tests/scan_pipeline_integration.rs (1)

4-6: LGTM!

Also applies to: 283-408

package.json (1)

18-19: LGTM!

scripts/benchmark-result-batching.ts (1)

1-89: LGTM!

docs/ui-performance-baseline.md (1)

20-47: LGTM!

Also applies to: 76-76

src/hooks/useScan.helpers.ts (1)

9-48: LGTM!

src/hooks/useScan.ts (1)

27-28: LGTM!

Also applies to: 54-80, 97-98, 118-150, 425-425, 539-542, 555-555, 608-609

src/store/index.ts (1)

1-12: LGTM!

src/store/slices/scanSlice.ts (1)

15-34: LGTM!

src/components/ChannelTable.tsx (1)

13-13: LGTM!

Also applies to: 136-137, 1421-1428

src/hooks/usePlaylistSources.ts (1)

47-47: LGTM!

Also applies to: 464-464

tests/useScan.helpers.test.ts (1)

2-7: LGTM!

Also applies to: 54-111

src-tauri/src/engine/playlist_score.rs (1)

1-301: LGTM!

src-tauri/src/commands/scan.rs (1)

92-103: LGTM!

Also applies to: 179-676, 822-890, 1408-1481, 1487-1523, 1530-1625, 1829-1868, 1872-1955, 1960-1984, 2056-2513, 2700-2753

Comment thread .github/workflows/release.yml Outdated
Comment thread scripts/patch-updater-manifest.sh
Comment thread src-tauri/src/commands/updater.rs
Comment thread src-tauri/src/engine/macos_dmg_update.rs
Comment thread src-tauri/src/engine/xtream.rs Outdated
Comment thread src-tauri/src/lib.rs
Comment thread src/hooks/useUpdateCheck.ts
Comment thread src/lib/updateState.ts Outdated
Fix a real regression from the result-batching rework: metrics
double-counted a channel when one batch carried two updates for the same
index. previousResult was read from the pre-batch array, but an index
first seen earlier in the same batch already had a position past that
array's end, so it read as new twice. It now reads through the working
array. Covered by a test that reproduces the coalesced-event case.

Stop a manual check from stealing the phase from a running install; the
finally already avoided clobbering it, but the entry did not, which also
defeated installUpdate's re-entry guard.

Create the macOS scratch directory exclusively, so a pre-planted directory
or symlink at that path cannot be reused as the mountpoint whose .app gets
copied over the installed bundle.

Memoize the install source: detection spawns up to three subprocesses on
Linux and cannot change while the process runs.

Move the Rust-mirrored UpdateInstallMode/UpdateInstallKind into types.ts,
so the whole updater IPC contract lives with the other Rust mirrors and
updateState.ts keeps only presentation helpers.

Drop a no-op Option::or that cargo clippy --fix left behind in the Xtream
category lookup; numeric ids are handled by the match below.
Under set -o pipefail, `find ... | head -1` fails the whole step when the
bundle directory is absent, so the notarization step died with an opaque
error instead of the "No .dmg found" message right below it. -print -quit
with a `|| true` lets the guard do its job.
Base automatically changed from chore/biome-formatting to main July 25, 2026 04:39
@kristofferR
kristofferR merged commit 9f50499 into main Jul 25, 2026
9 checks passed
@kristofferR
kristofferR deleted the feat/auto-updater-and-quality-gates branch July 25, 2026 04:40
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.

1 participant