Add a signed in-app updater, and gate CI on lint, format, and clippy - #214
Conversation
📝 WalkthroughWalkthroughThis 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. ChangesUpdater and release flow
Scan and scoring
Tooling and maintenance
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 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".
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
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.
223e221 to
4ab6654
Compare
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.
|
@codex review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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.
|
@coderabbitai full review |
|
@codex review |
✅ Action performedFull 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. |
There was a problem hiding this comment.
💡 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".
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.
|
@coderabbitai review |
|
@codex review |
✅ Action performedReview finished.
|
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 winMetrics double-count when a batch contains two updates for the same channel index.
positions.set(result.index, next.length)mutates the sharedpositionsmap immediately when a new index is first seen (line 104). If a second update for the sameresult.indexarrives later in the samebatch(e.g. an initialscan://channel-resultevent and the finalizedscan://channel-results-batchentry coalescing into one RAF flush), the second iteration findsposition != nullviapositions.get(...), butpreviousResultis still computed fromprevious.flatResults[position](line 82) — the pre-batch array, which doesn't have an entry at that brand-new position.previousResultincorrectly resolves tonull, sopresentCountgets incremented a second time for the same channel, andlowFpsCount/mislabeledCountskip the correct before/after comparison. Since these are running counters (not recomputed from scratch), the drift is permanent for the rest of the scan.
flatResultsitself stays correct (final push/replace uses the liveflatResultsvar), 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.tscovering a batch with two entries sharing the sameindex.🤖 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
⛔ Files ignored due to path filters (1)
src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (59)
.github/workflows/ci.yml.github/workflows/release.ymldocs/ui-performance-baseline.mdpackage.jsonscripts/benchmark-result-batching.tsscripts/patch-updater-manifest.shsrc-tauri/Cargo.tomlsrc-tauri/examples/backend_bench.rssrc-tauri/src/commands/history.rssrc-tauri/src/commands/mod.rssrc-tauri/src/commands/playlist.rssrc-tauri/src/commands/recent.rssrc-tauri/src/commands/saved.rssrc-tauri/src/commands/scan.rssrc-tauri/src/commands/server_test.rssrc-tauri/src/commands/settings.rssrc-tauri/src/commands/updater.rssrc-tauri/src/engine/cast_proxy.rssrc-tauri/src/engine/checker.rssrc-tauri/src/engine/chromecast.rssrc-tauri/src/engine/ffmpeg.rssrc-tauri/src/engine/macos_dmg_update.rssrc-tauri/src/engine/mod.rssrc-tauri/src/engine/parser.rssrc-tauri/src/engine/playlist_score.rssrc-tauri/src/engine/proxy_common.rssrc-tauri/src/engine/resume.rssrc-tauri/src/engine/stream_proxy.rssrc-tauri/src/engine/xtream.rssrc-tauri/src/lib.rssrc-tauri/src/models/scan.rssrc-tauri/src/models/settings.rssrc-tauri/src/state.rssrc-tauri/tauri.conf.jsonsrc-tauri/tauri.conf.release.jsonsrc-tauri/tests/scan_pipeline_integration.rssrc/App.tsxsrc/components/AppBanners.tsxsrc/components/ChannelTable.tsxsrc/components/PlaylistReportPanel.tsxsrc/components/SFSymbols.tsxsrc/components/SettingsPanel.tsxsrc/components/ThumbnailPanel.tsxsrc/hooks/usePlaylistSources.tssrc/hooks/useScan.helpers.tssrc/hooks/useScan.tssrc/hooks/useUpdateCheck.tssrc/index.csssrc/lib/perf.tssrc/lib/tauri.tssrc/lib/types.tssrc/lib/updateState.tssrc/store/index.tssrc/store/slices/scanSlice.tssrc/store/slices/settingsSlice.tssrc/store/slices/uiSlice.tssrc/store/types.tstests/updateState.test.tstests/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,
thiserrorfor error types, andserdefor serialization.
Files:
src-tauri/src/commands/mod.rssrc-tauri/src/engine/chromecast.rssrc-tauri/src/engine/playlist_score.rssrc-tauri/src/engine/checker.rssrc-tauri/src/commands/saved.rssrc-tauri/src/engine/mod.rssrc-tauri/src/commands/server_test.rssrc-tauri/src/engine/resume.rssrc-tauri/src/commands/recent.rssrc-tauri/src/engine/proxy_common.rssrc-tauri/src/commands/playlist.rssrc-tauri/src/state.rssrc-tauri/src/engine/parser.rssrc-tauri/tests/scan_pipeline_integration.rssrc-tauri/src/engine/xtream.rssrc-tauri/src/lib.rssrc-tauri/src/models/scan.rssrc-tauri/src/commands/history.rssrc-tauri/src/engine/stream_proxy.rssrc-tauri/src/commands/settings.rssrc-tauri/src/engine/macos_dmg_update.rssrc-tauri/src/commands/updater.rssrc-tauri/src/engine/cast_proxy.rssrc-tauri/src/engine/ffmpeg.rssrc-tauri/src/models/settings.rssrc-tauri/src/commands/scan.rs
src-tauri/src/**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
src-tauri/src/**/*.rs: Keep backend responsibilities organized intomodels,engine,commands,state.rs,error.rs, andlib.rsaccording to their documented roles.
Use event-driven scanning: emitscan://channel-resultevents per channel, with frontend event updates batched usingrequestAnimationFrame.
Files:
src-tauri/src/commands/mod.rssrc-tauri/src/engine/chromecast.rssrc-tauri/src/engine/playlist_score.rssrc-tauri/src/engine/checker.rssrc-tauri/src/commands/saved.rssrc-tauri/src/engine/mod.rssrc-tauri/src/commands/server_test.rssrc-tauri/src/engine/resume.rssrc-tauri/src/commands/recent.rssrc-tauri/src/engine/proxy_common.rssrc-tauri/src/commands/playlist.rssrc-tauri/src/state.rssrc-tauri/src/engine/parser.rssrc-tauri/src/engine/xtream.rssrc-tauri/src/lib.rssrc-tauri/src/models/scan.rssrc-tauri/src/commands/history.rssrc-tauri/src/engine/stream_proxy.rssrc-tauri/src/commands/settings.rssrc-tauri/src/engine/macos_dmg_update.rssrc-tauri/src/commands/updater.rssrc-tauri/src/engine/cast_proxy.rssrc-tauri/src/engine/ffmpeg.rssrc-tauri/src/models/settings.rssrc-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.rssrc-tauri/src/commands/saved.rssrc-tauri/src/commands/server_test.rssrc-tauri/src/commands/recent.rssrc-tauri/src/commands/playlist.rssrc-tauri/src/commands/history.rssrc-tauri/src/commands/settings.rssrc-tauri/src/commands/updater.rssrc-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.rssrc-tauri/src/engine/playlist_score.rssrc-tauri/src/engine/checker.rssrc-tauri/src/engine/mod.rssrc-tauri/src/engine/resume.rssrc-tauri/src/engine/proxy_common.rssrc-tauri/src/engine/parser.rssrc-tauri/src/engine/xtream.rssrc-tauri/src/engine/stream_proxy.rssrc-tauri/src/engine/macos_dmg_update.rssrc-tauri/src/engine/cast_proxy.rssrc-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.tsscripts/benchmark-result-batching.tssrc/components/SFSymbols.tsxsrc/lib/perf.tssrc/store/slices/uiSlice.tssrc/components/ChannelTable.tsxsrc/lib/types.tssrc/lib/updateState.tssrc/store/slices/scanSlice.tstests/useScan.helpers.test.tstests/updateState.test.tssrc/App.tsxsrc/hooks/usePlaylistSources.tssrc/components/SettingsPanel.tsxsrc/components/ThumbnailPanel.tsxsrc/lib/tauri.tssrc/components/AppBanners.tsxsrc/components/PlaylistReportPanel.tsxsrc/store/slices/settingsSlice.tssrc/store/types.tssrc/hooks/useScan.helpers.tssrc/hooks/useUpdateCheck.tssrc/hooks/useScan.ts
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use strict TypeScript with no unused locals or parameters.
Files:
src/store/index.tssrc/components/SFSymbols.tsxsrc/lib/perf.tssrc/store/slices/uiSlice.tssrc/components/ChannelTable.tsxsrc/lib/types.tssrc/lib/updateState.tssrc/store/slices/scanSlice.tssrc/App.tsxsrc/hooks/usePlaylistSources.tssrc/components/SettingsPanel.tsxsrc/components/ThumbnailPanel.tsxsrc/lib/tauri.tssrc/components/AppBanners.tsxsrc/components/PlaylistReportPanel.tsxsrc/store/slices/settingsSlice.tssrc/store/types.tssrc/hooks/useScan.helpers.tssrc/hooks/useUpdateCheck.tssrc/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.tsxsrc/components/ChannelTable.tsxsrc/components/SettingsPanel.tsxsrc/components/ThumbnailPanel.tsxsrc/components/AppBanners.tsxsrc/components/PlaylistReportPanel.tsx
src/components/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use a virtualized table via
@tanstack/react-virtualfor playlists containing 1000 or more channels.
Files:
src/components/SFSymbols.tsxsrc/components/ChannelTable.tsxsrc/components/SettingsPanel.tsxsrc/components/ThumbnailPanel.tsxsrc/components/AppBanners.tsxsrc/components/PlaylistReportPanel.tsx
src/lib/types.ts
📄 CodeRabbit inference engine (AGENTS.md)
Keep frontend types in
src/lib/types.tssynchronized 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 QualityNo change needed.
scripts/verify-production-binaries.shis present in the repo, so the release workflow step can run.
194-200: 🗄️ Data Integrity & IntegrationNo issue with the
.sigfile path.
tauri signer sign <file>produces the detached signature as<file>.sig, so the macOS and AppImagecat "${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 & AvailabilityNo change needed. The repo uses
dtolnay/rust-toolchain@stablefor the Rust toolchain, andErrorKind::CrossesDevicesis 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 CorrectnessNo change needed.
applyResultUpdatesalready returns a newflatResultsarray 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
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.
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 replacean AppImage — so
.deb/.rpminstalls are routed to manual instructions ratherthan 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.jsonholding signatures that no longer verify:.dmgis notarized and stapled afterwards, and is what users actuallydownload, so the updater should install it rather than the stock
.app.tar.gz;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 minisignverification, so the bytes are already trusted.
The Homebrew cask now declares
auto_updates true, the correct convention for aself-updating app.
Quality work
CI gates — there was no clippy, no
rustfmt --check, and no frontend linterat all. All three now run. Adding them surfaced two Rules-of-Hooks violations
React would have thrown on:
PlaylistReportPanelandThumbnailPanelbothreturned 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
flatResultsand in anindex-keyed object spread on every batch, making a whole scan quadratic.
Results now live only in
flatResultswith an append-only position map:Kept as
bun run perf:ui-batchingso it cannot silently regress.Preset fields —
ScanPresetConfiglisted its sixteen fields three times, soa 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
AppSettingslacks now fails to compile. Persisted JSON is unchanged.scan.rs—compute_playlist_scorewas pure math sitting in the scanorchestrator. Moved to
engine::playlist_scoreand tested at exact valuesrather 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
so the gate could land without hiding them. Clearing them is its own pass.
too_many_argumentsis allowed crate-wide and onelarge_enum_variantlocally,both with reasoning inline.
tauri-driverhas no macOS support, so it could not beverified 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, andbun run buildall 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 callsdownload, which doessignature verification only and no format inference.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests