Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ section to the released version and opens a fresh empty one (see `docs/RELEASING
- **Poll config reads share one immutable snapshot instead of deep-cloning per iteration (#893).** `Config` now holds `RwLock<Option<Arc<AppConfig>>>` with a `Config::snapshot()` getter that clones the pointer, not the document; `run_inner` and the exit-path cleanup take their config through it and never hold the read guard across the iteration body, so a save racing an in-flight poll completes without blocking and the iteration never observes a partially updated config. Writers publish a new `Arc` under the write guard (`Arc::make_mut` for the in-place keychain-stamp path). The Spotify bundle, write-clock snapshot, and tray dedup clones are untouched. No on-disk format change.
- **Config `u64` fields typed as `number`, not `bigint`, in generated types (#765).** `PollingConfig`, `LoggingConfig`, patch types, `ConfigSummary` counters, snooze minutes and stage-progress bytes carry `#[ts(type = "number")]`, matching the `serde_json` wire shape (JS numbers) and the existing `TrackInfo` override idiom. The `BIGINT_SECTIONS` normaliser, `BigInt(...)` defaults and bigint-aware stringify paths are gone; the store round-trips plain numbers. No value change.
- **Polling session state moves off module statics into `AppState::session` (#758, PARTIAL — polling half).** `SessionState` owns the write-decision clocks (with the D11 generation guard), the quiet/snooze latches, the last-now-playing cache, the preferred-presence session, the exit snapshot, and the #863 failure/gate mirrors; `poll_once`/`loop`/`state`/`sync`/`diagnostics` thread `&session` instead of touching statics. `global_state_lock` is deleted and each test constructs its own `SessionState::new()` (new `test_two_sessions_do_not_share_clocks_latches_or_caches` proves isolation). Tray/config `AppCaches` (slice 2) stay static for now, so `QUARANTINE_TEST_LOCK` and `LOCALE_TEST_LOCK` survive and the issue stays open. No behaviour change in the single-session path.
- **Tray/config caches move off module statics into `AppState::caches` (#758, PARTIAL — slice 2).** `AppCaches` owns the throttled devices/queue caches, post-action fetch instants, dedup snapshot, window/playing/mode mirrors, delayed-refresh guard, and the config quarantine/conflict flags; `load_config`/`quarantine_corrupt_config`/`emit_spotify_secret_conflict_once`, the tray rebuild/paint helpers, and every window/config/updater/snooze caller thread `&state.caches` instead of touching statics. `CONFIG_QUARANTINED`, `CONFLICT_EVENT_SENT`, and the tray cache statics are deleted and each test constructs its own `AppCaches::new()` (new `test_two_app_states_do_not_share_caches` proves isolation). `QUARANTINE_TEST_LOCK` is deleted; `LOCALE_TEST_LOCK` survives because the locale table (`i18n::CURRENT`) stays process-wide per the issue's keeps, so the issue stays open. No behaviour change in the single-session path.
### Refactor
- **Monolithic `lib.rs` split into `app`/`cli`/`deep_link`/`state` modules (#757).** The ~4.9k-line `lib.rs` is now a 34-line module registry + state re-export shim; the Tauri `run()`/setup wiring lives in `app.rs` (with `setup_*` helpers), CLI parsing/dispatch in `cli.rs`, the Spotify-callback exchange in `deep_link.rs`, and `AppState` + token-commit seams in `state.rs`. Six source-scanner guards retargeted to the new homes (`generate_handler!` → `app.rs`, `handle_spotify_callback` → `deep_link.rs`, `forward_launch_to_running_instance` → `app.rs`, `polling::run_oneshot` call site → `cli.rs`, log-permission setup → `setup_log_permissions` helper, `macos_deeplink` module decl → `lib.rs`), and the redaction-literal sweep now covers the four new modules. No behaviour change.
- **Config split into 7-slice mod with re-exported surface (#755).** `src-tauri/src/config.rs` (8313 ln) becomes `src-tauri/src/config/{schema,clamp,snooze,patch,migrate,io,transfer}.rs` plus a 52-line re-export header in `mod.rs` that keeps every `crate::config::X` path stable. All 121 config tests remain centralized in `config/mod.rs` (identical set, 565 asserts) — the slices carry no `#[test]`; source-scan guards read the slices through one `concat!(include_str!(…))`, `redact.rs` aggregates the 8 slice sources, `LoggingConfig` lives only in `schema.rs`. `cargo check --all-targets` plus `cargo test --lib` (config 121/121, full 910/910) plus `clippy -D warnings` plus `fmt --check` are all clean.
Expand Down
5 changes: 3 additions & 2 deletions docs/STATE-OF-FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ end-to-end; the few rows that can't be sourced inline are explicitly flagged
| Multi-window detach for Logs/Settings (C7) | ✅ | `src/routes/detached/[pane]/+page.svelte` renders the Logs, Settings, and unknown-pane branches; `src/lib/stores/detach.ts` calls Rust's fixed-table `detach_pane` command, and the main capability no longer grants webview-window creation. Detached labels receive minimal mirrored permissions in `src-tauri/capabilities/detached.json`, including `core:window:allow-close` for Pop back in; a refused close leaves the badge alone. `+layout.svelte` listeners remain window-label-guarded, and the app still boots single-window. |
| IPC guard matrix enforced by test (#771) | ✅ | `src-tauri/src/commands/mod.rs` caller-location matrix covers all 54 `generate_handler!` commands with a guarded / main-only-by-caller-location / detached-legit justification each; `test_guard_matrix_covers_every_registered_command` brace-counts the handler list out of `lib.rs` and fails on any registered-without-entry or listed-without-registration drift. `check_for_update` + `cancel_deferred_update` stay main-only by caller location (UpdatePrompt mounts only under `{#if isMainWindow}`); adding a `window` param + guard is the recorded follow-up. |
| Deep-link navigate UX (C2) | ✅ | `navigate` event emitted from `handle_deep_link` / Teams auth success (`dashboard` / `settings`); listener in `+page.svelte` defers while Onboarding owns the view. |
| Tray tooltip + native CheckMenuItems + dock badge (C4) | ✅ | `tray/mod.rs` + `tray/dedup.rs` — live tooltip `Artist — Track (▶|⏸)` on each rebuild; Play/Pause, Shuffle and Repeat as native CheckMenuItems driven by `LAST_PLAYING_STATE` / `LAST_SHUFFLE_STATE` / `LAST_REPEAT_STATE`; macOS-only (`#[cfg]`) presence-gated dock badge wired into the polling loop (`b82f515`). The menu also carries a **Pause / Resume Sync** item (IDs `ID_PAUSE_SYNC` / `ID_RESUME_SYNC`) and, since 4.7.0 (#677), a **Pause sync for…** snooze submenu plus **Resume sync now** while a snooze is active. The tray's own status line is `sync_status_line` ("Syncing — Artist — Track", "Paused — …", "Not syncing", "Syncing — nothing playing"), and the tooltip is `{status_line} · {track_tooltip}`; the words come from the `i18n::Strings` table and a snooze replaces the line with `snooze_status_line` (remaining time + local deadline). |
| Tray tooltip + native CheckMenuItems + dock badge (C4) | ✅ | `tray/mod.rs` + `tray/dedup.rs` — live tooltip `Artist — Track (▶|⏸)` on each rebuild; Play/Pause, Shuffle and Repeat as native CheckMenuItems driven by `playing_flag` / `shuffle_flag` / `repeat_flag`; macOS-only (`#[cfg]`) presence-gated dock badge wired into the polling loop (`b82f515`). The menu also carries a **Pause / Resume Sync** item (IDs `ID_PAUSE_SYNC` / `ID_RESUME_SYNC`) and, since 4.7.0 (#677), a **Pause sync for…** snooze submenu plus **Resume sync now** while a snooze is active. The tray's own status line is `sync_status_line` ("Syncing — Artist — Track", "Paused — …", "Not syncing", "Syncing — nothing playing"), and the tooltip is `{status_line} · {track_tooltip}`; the words come from the `i18n::Strings` table and a snooze replaces the line with `snooze_status_line` (remaining time + local deadline). |
| Settings dirty-state + clamp feedback + reset (C9) | ✅ | `Settings.svelte` — unsaved-changes banner via BigInt-safe deep compare; per-section Reset-to-default buttons using `defaultConfig`. Clamp feedback lives in the cards (`PollingCard` min>max + pause-backoff hints, `StatusFormatCard` lexicon hint, `RulesCard` replacement-length hints) styled by the shared `.clamp-hint` rule in `src/app.css`. |
| Settings per-card split (#750) | ✅ | `src/lib/components/settings/` holds `SettingsCard` shell + all thirteen cards (`RulesCard` with `$bindable()` slices + `onreset`/`onchange` and a header `actions` Undo; `PresenceCard`/`PollingCard` with `$bindable()` slice + `onreset`; `LoggingCard`/`UpdatesCard`/`ShortcutsCard` with `$bindable()` slice and no `onreset`; `StatusFormatCard` with `$bindable()` slice + `onreset` + lexicon/preview callbacks; `SpotifyCard`/`TeamsCard` with connection props + reconnect callbacks and a header `actions` badge; `ProfilesCard` with `$bindable()` slices + `saveMessage` + `onchange`; `AppearanceCard` with `$bindable()` slices + `onreset` + `onAutostartError`; `NotificationsCard`/`BackupCard` store- or callback-driven with no `$bindable()` slice); `Settings.svelte` keeps draft state, save/discard, `pendingNav`, footer. |
| Config module split, one concern per file (#755) | ✅ | `src-tauri/src/config/mod.rs` re-exports `schema`/`clamp`/`snooze`/`patch`/`migrate`/`io`/`transfer` (one concern per file, every `crate::config::X` path stable); all 121 config tests remain centralized in `mod.rs` (identical set) — the slices carry no `#[test]`; `redact.rs` aggregates the 8 slice sources through one `concat!`; `LoggingConfig` lives only in `schema.rs`; `cargo check --all-targets` plus `cargo test --lib` (config 121/121, full 910/910) plus `clippy -D warnings` plus `fmt --check` all clean. |
Expand Down Expand Up @@ -83,7 +83,7 @@ end-to-end; the few rows that can't be sourced inline are explicitly flagged
| Maintained path/rand crates | ✅ | `dirs` → `directories` 6, `rand` 0.9 with `try_fill_bytes` propagation; single keyring feature set (#418). |
| Status template placeholders ({device}/{playlist}/{progress}/{shuffle}/{repeat}) | ✅ | `src-tauri/src/spotify.rs::format_status_with_context` — one 13-token table (`placeholder_values`) filled per render and substituted in a single left-to-right pass (`substitute_placeholders`), so a value that arrived from Spotify is never re-scanned and re-expanded (#341; `format_status_does_not_expand_data_inserted_tokens`, #580). `{context}` is a literal alias of `{playlist}`; `{shuffle}`/`{repeat}` are icon-only (🔀/🔁 or `""`); `{progress}` is `M:SS` with no hour rollover (`90:00` for a 90-minute episode) and **empty** when `progress_ms` is `None` (#165). UI hint keys `settings.placeholdersHint` / `onboarding.placeholdersHint` list the music tokens. |
| Podcast & audiobook episodes | ✅ | `src-tauri/src/spotify.rs::map_media_item` reads the item's own `type` against the documented `oneOf(track, episode)` union: an episode's `show.name` takes the `artist` slot and `show.publisher` the `album` slot, and `EpisodeInfo` carries `show_name`/`publisher`. `poll_once::process_track` selects `DEFAULT_EPISODE_STATUS_FORMAT` (`🎙️ {show} - {episode}`) whenever `now.episode.is_some()`, so a music template is never applied to an episode (#581). Adverts are unchanged: the envelope's `currently_playing_type` is gated and `Ad | Unknown` returns `None` from the mapper — "nothing playing" (#161). The queue mapper feeds episodes into tray *Up Next* and drops ads (#583). Note: the episode template is a built-in constant — there is **no** `teams.episode_status_format` config key. |
| Tray Shuffle / Repeat toggles | ✅ | `src-tauri/src/tray/dedup.rs` — both are native CheckMenuItems whose marks read `LAST_SHUFFLE_STATE` / `LAST_REPEAT_STATE`, written by `note_playback_modes` from the poll body (no extra request) and optimistically on a successful tray toggle before `force_tray_refresh`. Shuffle targets the inverse of the last known state; Repeat targets `RepeatState::next()`, i.e. `off → context → track → off`, and its label spells the mode out because a check mark cannot distinguish the last two (#582). A rejected command records nothing, so the tray never claims a state Spotify refused; the error surfaces on `playback-error` as an in-app toast. |
| Tray Shuffle / Repeat toggles | ✅ | `src-tauri/src/tray/dedup.rs` — both are native CheckMenuItems whose marks read `shuffle_flag` / `repeat_flag`, written by `note_playback_modes` from the poll body (no extra request) and optimistically on a successful tray toggle before `force_tray_refresh`. Shuffle targets the inverse of the last known state; Repeat targets `RepeatState::next()`, i.e. `off → context → track → off`, and its label spells the mode out because a check mark cannot distinguish the last two (#582). A rejected command records nothing, so the tray never claims a state Spotify refused; the error surfaces on `playback-error` as an in-app toast. |
| Live stage progress + cancel for install-on-quit | ✅ | `updater_bg.rs` binds progress, completion, and cancellation to the exact stage request id and generation. Cancel invalidates an in-flight completion, removes an already-committed payload, or tombstones cancellation that arrives before staging begins; a losing download cannot stage or install, and queued terminal events are suppressed. |
| Keychain-unavailable surface (locked vs absent) | ✅ | `keychain.rs::KeychainPresence` preserves `Present` / `Absent` / `Unavailable(help)`. Config loads stamp both derived fields through `with_keychain_flags` and the warm presence cache; a fresh `Present` observation is reused for 30 seconds, while a cold or expired cache falls back to the direct namespaced/legacy probe. Explicit user-action checks remain uncached. |
| Log viewer on-disk backfill | ✅ | `src-tauri/src/commands/logs.rs::get_recent_logs` reads the tail on `spawn_blocking`, clamped twice — `MAX_LOG_LINES` 500 and `LOG_TAIL_MAX_BYTES` 256 KiB — dropping the partial first line; a missing file is `Ok(empty)`, not an error. `LogViewer.svelte` registers `log://log` first and then awaits the seed, prepending it and re-clamping to `MAX_BUFFER`. The seed is **raw, not redacted** by explicit decision (#595): redaction belongs to the paste-able snapshot path. **Clear** sets `seedCancelled` so history cannot reappear. |
Expand Down Expand Up @@ -129,6 +129,7 @@ end-to-end; the few rows that can't be sourced inline are explicitly flagged
| Polish CLDR few/many plurals (#1154) | ✅ | Both plural keys (`logs.count`, `dashboard.snoozeStatusStart`) carry a `{key}_few` entry in all eight dictionaries — real Polish nominative plurals ("2 wpisy", "2 minuty"; model-written, human review pending per #984) with `_other`-mirroring `_few` in the seven locales whose CLDR never selects `few`. No `_many`: covered-noun `many` ("5 wpisów", "5 minut") is the genitive plural `_other` already carries. `tests/i18n.test.ts` pins the real few/many forms (1/2–4/5/0/12/22/25) plus the snooze trio (1/2/5); key-coverage + placeholder gates cover the `_one`/`_other`/`_few` trio. |
| Five more UI locales + follow-system language (#984) | ✅ | Webview ships es/it/pl/pt(BR)/nl beside en/de/fr (`src/lib/i18n/*.ts`, `Dict`-typed parity), the Rust tray/menu tables carry the same eight (`i18n.rs::ES/IT/PL/PT/NL`), detection prefix-matches every code longest-tag-first (`pt-BR` → `pt`), and Settings carries the follow-system toggle. Frontend suite 427/427 green (incl. `tests/i18n.test.ts` 26 tests); Rust `cargo test --lib i18n` 10/10, `config::` 168/168, `commands::config` 19/19. The five new locales are model-written with human review pending (see CONTRIBUTING). |
| Polling session state owned by `AppState` (#758, PARTIAL — polling half) | ⚠ Partial | `polling/state.rs::SessionState` owns the write-decision clocks (D11 generation guard), quiet/snooze latches, last-now-playing cache, preferred-presence session, exit snapshot, and #863 failure/gate mirrors; `poll_once`/`loop`/`state`/`sync`/`diagnostics` thread `&session`. `global_state_lock` deleted; each test builds `SessionState::new()` and `test_two_sessions_do_not_share_clocks_latches_or_caches` proves isolation. Remainder: tray/config `AppCaches` (slice 2) still static, so `QUARANTINE_TEST_LOCK` + `LOCALE_TEST_LOCK` survive. `cargo test --lib` 910/910, clippy `-D warnings` clean. |
| Tray/config caches owned by `AppState` (#758, PARTIAL — slice 2) | ⚠ Partial | `state.rs::AppCaches` owns the throttled devices/queue caches, post-action fetch instants, dedup snapshot, window/playing/mode mirrors, delayed-refresh guard, and quarantine/conflict flags; `load_config`/tray rebuild/paint callers thread `&state.caches`. `CONFIG_QUARANTINED`/`CONFLICT_EVENT_SENT`/tray cache statics deleted, `QUARANTINE_TEST_LOCK` deleted, `test_two_app_states_do_not_share_caches` proves isolation. Remainder: `LOCALE_TEST_LOCK` survives (`i18n::CURRENT` is an issue-blessed keep). `cargo test --lib` 913/913, clippy `-D warnings` clean. |
| Monolithic `lib.rs` split into `app`/`cli`/`deep_link`/`state` (#757) | ✅ | `lib.rs` is a 34-line module registry + state re-export shim; `run()`/setup wiring in `app.rs` (`setup_*` helpers), CLI parse/dispatch in `cli.rs`, Spotify-callback exchange in `deep_link.rs`, `AppState` + token-commit seams in `state.rs`. Six scanner guards retargeted (`generate_handler!` → `app.rs`, `handle_spotify_callback` → `deep_link.rs`, `forward_launch_to_running_instance` → `app.rs`, `polling::run_oneshot` call site → `cli.rs`, log-permission setup → `setup_log_permissions`, `macos_deeplink` decl → `lib.rs`); redaction sweep covers the four new modules. `cargo test --lib` 910/910, clippy `-D warnings` clean. |

| Desktop notification classes (v4.7.0) | ✅ | `config.rs::NotificationsConfig` (`AppConfig.notifications`): `track_change` (dispatched from `Dashboard.svelte`, 5 s throttle + replace-in-place id), `sync_stopped` (from the always-mounted `routes/+layout.svelte`, and only for `payload?.self_terminated === true`, so a Pause Sync you clicked stays quiet), `auth_required` (from `teams-reconnect-required` unless `payload?.user_initiated === true` — the one user-initiated emitter is `commands/onboarding.rs::reconnect_teams`) and `update_staged` (from `update-stage-complete { version }`, emitted once per successful stage by `updater_bg.rs`). All four default on and each has its own Settings toggle; `stores/notifications.ts` also mirrors preferences across windows. |
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture/storage-and-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ settings:
- **Corrupt-file quarantine (#379):** a `config.json` that fails
`serde_json::from_str` is renamed beside itself to `config.json.bak` (fixed
name, never timestamped), a `[CFG] corrupt config … quarantined to …` warning is
logged, `CONFIG_QUARANTINED` is raised, and the app boots on
logged, the per-`AppState` quarantine flag (`AppCaches`, issue #758 slice 2)
is raised, and the app boots on
`AppConfig::default()`. The rename is best-effort: a failure is logged and
swallowed, and the flag is raised either way, so the original file is never
truncated. A *schema-version* mismatch is **not** a quarantine — it goes through
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/tray-and-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ matrix builds **aarch64 macOS only** — Intel Macs never receive updates
`tray/mod.rs` builds the menu natively, from in-process state:

- **Shuffle / Repeat are real toggles (#582).** Both are
`CheckMenuItemBuilder` items; Shuffle's mark reads `LAST_SHUFFLE_STATE` and
Repeat's reads `LAST_REPEAT_STATE` plus a mode-spelling label
`CheckMenuItemBuilder` items; Shuffle's mark reads `shuffle_flag` and
Repeat's reads `repeat_flag` plus a mode-spelling label
(`Repeat: Off` / `Repeat: Context` / `Repeat: Track` — a check mark alone cannot
tell the last two apart). Those atoms are written by `note_playback_modes` from
the poll body itself (no extra request, no new scope) and optimistically by a
Expand Down
Loading
Loading