138 lines
7.0 KiB
Markdown
138 lines
7.0 KiB
Markdown
# Plan: queue de-duplication and sorting
|
||
|
||
Executes `architecture/queue-order.md` against the gates in
|
||
`quality/queue-order.md`. The wire and the stubs already exist (api-design);
|
||
what follows fills them in, in dependency order. Every task names how it is
|
||
verified.
|
||
|
||
## 1. The wire
|
||
|
||
- [x] **T1 — Proto: `DedupQueue`, `SortQueue`, `QueueSort`.** Two RPCs, the
|
||
strategy enum, `DedupQueueResponse.removed`, with the semantics documented at
|
||
the wire. *Verified by:* it compiles through `tonic-prost-build`; G21 by
|
||
reading.
|
||
- [x] **T2 — Rights matrix.** Both methods as `QueueOwner`, added to the pinned
|
||
method list. *Verified by:* `auth.rs::the_method_table_pins_every_rpc_of_the_service`
|
||
(G11).
|
||
|
||
## 2. Pure order logic (`crabidy-server/src/queue_order.rs`)
|
||
|
||
- [x] **T3 — `track_id`.** Provider-scoped item id, else the whole path.
|
||
*Verified by:* `a_provider_item_id_identifies_a_track_across_paths`,
|
||
`ids_are_scoped_to_their_provider`, `a_captured_copy_is_not_its_streaming_source`,
|
||
`without_an_id_the_path_is_the_identity` (G2).
|
||
- [x] **T4 — `duplicate_positions`.** Survivor = current, else earliest;
|
||
ascending, unique, never the current position; safe on an empty queue and an
|
||
out-of-range current. *Verified by:*
|
||
`duplicates_after_the_first_are_removed`, `the_playing_entry_survives_its_group`,
|
||
`nothing_to_do_is_an_empty_list`, `positions_come_back_ascending_and_are_unique`
|
||
(G1).
|
||
- [x] **T5 — `sort_permutation`.** Decorate–sort–undecorate with keys built
|
||
once; compound artist key; case-insensitive text; unknown-last in both
|
||
directions; `Reverse` ignores the direction; `Unspecified` is the identity.
|
||
*Verified by:* `every_strategy_returns_a_permutation`,
|
||
`sorting_by_artist_groups_albums_and_keeps_track_order`, `text_sorts_ignore_case`,
|
||
`descending_reverses_the_key_but_not_the_blanks`,
|
||
`unknown_durations_sort_last_in_both_directions`,
|
||
`reverse_flips_the_current_order_and_ignores_the_direction`,
|
||
`an_unspecified_strategy_changes_nothing`, `sorting_nothing_does_not_panic`
|
||
(G3, G4, G5).
|
||
|
||
## 3. Queue state (`crabidy-server/src/lib.rs`)
|
||
|
||
- [x] **T6 — `QueueManager::dedup`.** Delegates the removal to
|
||
`remove_tracks`; returns the count. *Verified by:*
|
||
`dedup_removes_later_copies_and_reports_the_count`,
|
||
`dedup_keeps_the_playing_track_playing`, `dedup_of_a_clean_queue_changes_nothing`,
|
||
`dedup_leaves_the_queue_playable` (G1, G6, G7).
|
||
- [x] **T7 — `QueueManager::sort`.** Permute `tracks`; shuffle off ⇒ identity
|
||
play order with `current_offset` on the current track, shuffle on ⇒ remap.
|
||
*Verified by:* `sorting_reorders_the_queue_and_follows_the_current_track`,
|
||
`sorting_without_shuffle_changes_what_plays_next`,
|
||
`sorting_with_shuffle_on_preserves_the_shuffled_play_sequence`,
|
||
`sorting_with_shuffle_on_still_reorders_the_visible_queue`,
|
||
`sorting_an_empty_or_single_queue_does_not_panic`,
|
||
`an_unspecified_sort_leaves_the_queue_alone` (G6, G5).
|
||
|
||
## 4. Server plumbing
|
||
|
||
- [x] **T8 — Playback loop arms.** Lock, mutate, broadcast; dedup replies with
|
||
its count and skips the broadcast when it removed nothing. *Verified by:*
|
||
`dedup_command_removes_duplicates_and_replies_with_the_count`,
|
||
`dedup_command_answers_zero_without_broadcasting`,
|
||
`dedup_command_survives_a_dropped_result_channel`,
|
||
`sort_command_reorders_the_queue_and_broadcasts_it`,
|
||
`queue_order_commands_reach_the_persist_channel` (G8, G9, G10, G12).
|
||
- [x] **T9 — RPC handlers.** `Unspecified`/unknown → `InvalidArgument` before
|
||
the loop sees it; the count passes through verbatim. *Verified by:*
|
||
`sort_queue_refuses_an_unnamed_strategy`, `sort_queue_forwards_the_strategy_and_direction`,
|
||
`dedup_queue_returns_the_loops_count` (G10, G12).
|
||
|
||
## 5. TUI
|
||
|
||
- [x] **T10 — Client RPC methods** (`dedup_queue`, `sort_queue`) and the
|
||
`MessageFromUi`/`MessageToUi` variants. *Verified by:* compiles; exercised
|
||
through the dispatch tests.
|
||
- [x] **T11 — Bindings.** `u` dedup, `S` sort menu, queue scope only.
|
||
*Verified by:* `queue_order_keys_are_bound_in_the_queue_only`,
|
||
`chords_are_unique_within_scope` (G17).
|
||
- [x] **T12 — `sort::choose` + the menu table.** Lowercase ascending,
|
||
uppercase descending, reverse case-insensitive, unclaimed keys unclaimed.
|
||
*Verified by:* `a_lowercase_key_sorts_ascending_and_its_capital_descending`,
|
||
`reverse_ignores_the_case`, `keys_the_menu_does_not_offer_are_not_claimed`,
|
||
`the_table_is_consistent` (G15).
|
||
- [x] **T13 — `sort::render` + `popup_area`.** Centered, `Clear`-backed,
|
||
clamped to the frame. *Verified by:*
|
||
`the_overlay_lists_every_strategy_with_its_key`, `a_tiny_frame_clamps_the_popup`.
|
||
- [x] **T14 — `App::handle_sort_key`.** Modal: sort-and-close, `Esc`/`q`/`S`
|
||
close, anything else ignored. *Verified by:*
|
||
`a_sort_menu_key_sends_the_strategy_and_closes_the_menu`,
|
||
`escape_closes_the_sort_menu_without_sorting`,
|
||
`an_unknown_key_leaves_the_sort_menu_open` (G15).
|
||
- [x] **T15 — Dispatch + event-loop routing + render hook.** Empty-queue
|
||
guards on both actions; the menu is checked before the bindings table.
|
||
*Verified by:* `dedup_asks_the_server_only_with_a_non_empty_queue`,
|
||
`the_sort_menu_opens_only_with_a_non_empty_queue`.
|
||
- [x] **T16 — The pane's dedup notice.** `show_dedup_result` + `notice()`
|
||
expiry + title precedence (visual > filter > notice > register).
|
||
*Verified by:* `the_dedup_result_appears_in_the_title`,
|
||
`the_dedup_result_expires`, `an_active_search_outranks_the_dedup_result`
|
||
(G18).
|
||
|
||
## 6. CLI
|
||
|
||
- [x] **T17 — `queue dedup` / `queue sort <key> [--desc]`.** `ValueEnum`
|
||
strategy. *Verified by:* `queue_dedup_takes_no_arguments`,
|
||
`queue_sort_parses_a_strategy_and_an_optional_direction` (G19).
|
||
- [x] **T18 — `wire_sort` / `sort_label` and the printed results.** Dedup
|
||
prints its count including `0`. *Verified by:* the parse tests plus reading
|
||
(G18, G19).
|
||
|
||
## 7. Web client
|
||
|
||
- [x] **T19 — RPC methods, `Dialog::Sort`, actions, keymap entries, toolbar
|
||
buttons, key routing.** *Verified by:* `queue_order_keys_match_the_tui`,
|
||
`the_sort_table_is_consistent` (G16, G17).
|
||
- [x] **T20 — `sort_menu_key`.** The web port of `sort::choose`, plus the
|
||
close keys. *Verified by:*
|
||
`the_sort_menu_maps_keys_to_strategies_and_closes_on_escape` (G15, G17).
|
||
- [x] **T21 — `SortMenu` component + `notify`.** Rows clickable and typeable,
|
||
dedup count in a toast. *Verified by:* the wasm build (`build-web`) plus
|
||
reading (G16, G18).
|
||
- [x] **T22 — Web help overlay entries** for `u` and `S`. *Verified by:*
|
||
`every_action_reachable_from_help_table` plus reading (G20).
|
||
|
||
## 8. Documentation
|
||
|
||
- [x] **T23 — `docs/src/queue.md`**: the two order operations in the queue
|
||
model, the duplicate rule, the strategies, and the shuffle interaction.
|
||
- [x] **T24 — `docs/src/clients/tui.md`** key table and
|
||
**`docs/src/clients/cli.md`** command list. *Verified by:* reading; the book
|
||
builds (G20).
|
||
|
||
## 9. Close-out
|
||
|
||
- [x] **T25 — Full check.** `cargo test` (workspace), `cargo clippy
|
||
--all-targets -D warnings`, `cargo fmt --check`, `cargo test -p cbd-tui
|
||
--no-default-features`, and the web build. Then `plan/summary.md`.
|