Commit Graph

163 Commits

Author SHA1 Message Date
Edouard Vanbelle 8637ccfc4c feat(drag&drop): no drop zone on search page 2026-07-26 18:05:42 +02:00
Edouard Vanbelle 761a203d7c feat(drag&drop): dropzone is the while page 2026-07-26 13:27:17 +02:00
Edouard Vanbelle 825a5d1b67 feat(sidebar): permit drop in trash / favorite 2026-07-22 23:10:47 +02:00
Dionisio Pozo ef3e3f45b3 Merge pull request #521 from BCNelson/feat/external-file-mounts
feat(mounts): external file mounts — pluggable provider, read-write, WebDAV/NextCloud, admin UI
2026-07-22 06:52:43 +02:00
Dionisio Pozo 86268049ff Merge pull request #641 from EdouardVanbelle/refactor/front-resource-list
feat(fileDto, folderDto): add is_favorite + is_shared
2026-07-22 06:51:28 +02:00
Bradley Nelson 3ad38ddde7 test(mounts): admin CRUD hurl coverage + frontend mounts-tab tests
Close the two coverage gaps in the external-mounts feature — the admin
REST surface and the admin UI tab both previously had no automated tests.

Backend (hurl):
- tests/api/external_mounts.hurl — black-box coverage of the admin mount
  endpoints (list / create / delete): input validation (empty name,
  non-existent host path → 400), create → 201 with the full view, the
  mount appears in the list, delete → 204 then 404, and the /api/admin
  middleware denies a non-admin on every verb → 403.
- server.env: enable OXICLOUD_ENABLE_EXTERNAL_MOUNTS for the test server;
  run.sh: register the new scenario.

Frontend (vitest):
- admin/page.test.ts — opening the mounts tab loads + lists mounts,
  the create form calls createExternalMount, and delete goes through the
  confirm modal to deleteExternalMount.

Tooling:
- devenv.nix: add b3sum (used by tests/api/run.sh's storage-integrity check).
2026-07-21 18:19:03 -06:00
DioCrafts d66956824c Optimize storage, GC, and upload hot paths 2026-07-22 02:06:29 +02:00
Edouard Vanbelle d9987782c4 feat(fileDto, folderDto): add is_favorite + is_shared
- provide is_shared and is_favorite information in DTO, information propagated as badge/buttons per items
      regarding performances I try to be minimalis on SQL to prevent any perf regression

    - remove old set of sharedids and favoriteids (was not functionnal anymore)
    - fix date picker (no past selection) in grant
    - fix contextmenu close on /files section
2026-07-22 01:55:10 +02:00
Bradley Nelson 9f4bc54113 Merge upstream/main into feat/external-file-mounts
Resolve conflicts between the external-file-mounts feature and upstream's
D5/D7 refactor (per-file provenance, keyset pagination, cross-drive move
gates, resource-access hook, folder-cascade lifecycle hook).

Key resolutions:
- FolderService::new now takes (repo, authz, file_lifecycle, mount_router);
  all callers + DI updated.
- FileRetrievalService / FileManagementService keep both the mount_router
  and the new resource_access_hook / drive_repo / storage_usage wiring.
- list_files_batch_with_perms: adapt the mount branch from offset- to
  keyset (after_name) pagination, mirroring paginate_mount_entries.
- download_file_impl: keep upstream's &HeaderMap + `impl IntoResponse + use<>`
  signature, retain the mount-download branch.
- Mount DTOs: the retired `owner_id` field maps onto created_by/updated_by
  (the mount owner) — the fields the frontend now uses for owner display.
- admin/+page.svelte: keep upstream's user-delete modal + the 'mounts' tab.
- Bump memmap2 0.9.10 -> 0.9.11 (RUSTSEC critical advisory fix) and
  regenerate Cargo.lock against the merged Cargo.toml.
2026-07-21 17:09:36 -06:00
Edouard Vanbelle 383f640920 feat(drag&drop): add support of copy or move 2026-07-20 23:19:44 +02:00
Edouard Vanbelle e1d379b9dd feat(drag&drop): normalize the drag & drop
- dragged items are always doing the same rendering
 - add copy/move option
 - highlight breadcrumb destination
2026-07-20 23:19:44 +02:00
Edouard Vanbelle 0cc77f7a36 feat(ui): show a notification if user try to drop a file in another section than /files 2026-07-20 23:19:44 +02:00
Edouard Vanbelle c286eed3b2 feat(ui): add a dropzone when uploading files from system
- add a dropzone on the whole screen
 - correct z-index according design system
2026-07-20 23:19:12 +02:00
Edouard Vanbelle a520afcf7c feat(ui:items): uploading an item with a swimlane display
restore the legacy display with new element uploaded, when swimlane is in place
    as the sort is done by server side just add new element in a "new elements" swimlane.
    if user continue to scroll down in cursor/pages and item is found from server,
    UI remove it from the new element and restore the position

    other option: is user refresh it's page, server will restore the natural order
2026-07-20 22:11:00 +02:00
Edouard Vanbelle ae6e3a8eb3 feat(items): re-enable lazy loading with cursor
example: if a folder has many resource, client will use lazy loading
    and load next cursor if scroll reached the bottom of the page
    purpose: reduce the amount of call to server
2026-07-20 21:44:50 +02:00
Edouard Vanbelle 5b8fb68b30 feat(items): clarify column names 2026-07-20 21:25:52 +02:00
Edouard Vanbelle 931e27d09c fix(resources): wire missing created_by and updated_by 2026-07-20 21:04:32 +02:00
Edouard Vanbelle 4873a5e837 feat(ui:items): normalize context menu 2026-07-20 19:35:14 +02:00
Edouard Vanbelle 67579932c7 feat(ui): action bar is contextualizez accross sections
- [download] + [remove from recent] for recent section
  - [download] + [remove from favorite] for favorite section
  - [restore] + [permanent delete] for trash section
2026-07-20 19:15:41 +02:00
Edouard Vanbelle c6b856bfc7 feat(ui:files): ensure conext menu is build on click + stick action bar & breadcrumb
ensure conext menu is build on click: API call to determine if user has access to item's parent folder is done only if user request context menu
    (lazy API call)
2026-07-20 19:01:59 +02:00
Edouard Vanbelle 4f3309e087 test(front): correct frontend test + end2end tests 2026-07-20 18:46:45 +02:00
Edouard Vanbelle 569b67caac refactor(front): apply formatter 2026-07-20 12:45:31 +02:00
Edouard Vanbelle 7ffb7bf0ae feat(ui): add 'open parent directory' in recent and favorite section 2026-07-20 12:26:45 +02:00
Edouard Vanbelle 63589e595e refactor(ui): recent: add a quick button to remove item from recent 2026-07-20 12:26:45 +02:00
Edouard Vanbelle e3c3f6fe24 refactor(ui): trash: show action buttons in grid view 2026-07-20 12:26:45 +02:00
Edouard Vanbelle 146a1a32bd refactor(ui): add the rubberband to select multiple items 2026-07-20 12:26:45 +02:00
Edouard Vanbelle 51d39918f7 refactor(ui): ctrl + click or command + click to toggle selection 2026-07-20 12:26:45 +02:00
Edouard Vanbelle 65ac8f76d2 refactor(ui): ctrl + A or command + A to select all items 2026-07-20 12:26:45 +02:00
Edouard Vanbelle a27db0e675 refator(ui): merge of ResourceList part 2 2026-07-20 12:26:45 +02:00
Edouard Vanbelle 75ae837f1a refator(ui): merge of ResourceList part1 2026-07-20 12:26:45 +02:00
Claude 6bc09cb3b3 perf: round 18 — calendar-event in-place iCal edit, ResourceList incremental id-index
Two items from the ROUND17 deferred list, each benchmark-gated with a
BEFORE/AFTER equivalence gate and a rollback-on-regression check (ROUND2–17
discipline). See benches/ROUND18.md.

[C1] backend — CalendarEvent::update_ical_property / remove_ical_property
rewrote the ENTIRE ical_data body with format!("{}{}{}") on every call and
allocated two search needles per call. calendar_storage_adapter::update_event
fans a multi-field edit out into one call per changed field, so a full REST
edit paid one full-body (up to ~11 KB) allocation per property. The body is
now mutated in place (replace_range for an existing property, four insert/
insert_str for a new one, byte-identical spans) and the single "\nNAME:"
needle is built on the stack (the "\r\nNAME:" needle was redundant — the LF
form is its suffix). bench_round18_micro [C1]: 70 -> 2 allocs/op (68 fewer),
2.46x wall, emitted body byte-identical.

[F1] frontend — ResourceList itemIndexById rebuilt a fresh Map over the whole
accumulated list every infinite-scroll page (O(N)/page, O(N^2) drain) and,
being a new instance each page, re-fired the reap-stale effect (another O(N)
id Set/page). New ItemIndexBuilder extends a persistent Map with the fresh
page only and reuses the reference across appends; the reap-stale effect now
tests membership against it. round18.bench.test.ts [F1]: 40x50 drain 74.1 ->
6.4 ms (11.5x), deep-equal to the reference at every page, reference-contract
gate (same-ref append / new-ref rebuild).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoxFtikahM1N4PE5s3ZVH3
2026-07-19 20:53:30 +00:00
Dionisio Pozo a09569b5c2 Merge pull request #625 from AtalayaLabs/claude/performance-optimization-analysis-raoezl
Round 16: incremental lanes/contextMap builders & alloc cuts
2026-07-19 19:35:26 +02:00
Claude 955f4a7b9f perf: round 16 — shares-lane & contextMap incremental builders, folder/href/disposition/preview alloc cuts
Finishes the route-level half of the O(N²/page) grouped-listing class ROUND15
fixed inside ResourceList, plus a backend CPU/alloc micro-pack. Every change is
benchmark-gated with a hard rollback rule; no PostgreSQL needed for any arm
(benches/ROUND16.md).

Frontend (vitest):
- F1 "My shares" lanes: the `lanes` $derived.by re-bucketed the whole
  accumulated grant list on every page and every grant edit. SharedLanesBuilder
  re-emits only the fresh page (fan-out + first-appearance header), reusing
  untouched lanes' array refs. 25.5x fewer emit calls, 8.8x wall, O(N²/page)→O(N).
- F2 contextMap (trash/recent/favorites/shared-with-me): each rebuilt a fresh
  N-key Map, re-hashing every accumulated id, per page. primeContextPage holds a
  persistent SvelteMap primed per page (the shipped favoriteIds shape).
  25.5x fewer entry calls, 7.2x wall.
- Extracted the shared O(1) append test (isAppendExtension); F1's gate re-covers it.

Backend (counting-allocator):
- M1 folder display constants Arc::from -> intern_display (3 sites): 3 -> 0 allocs/row.
- M2 build_content_disposition (every download + Range seek): 3 -> 1 alloc, 6x, 2.67x wall.
- M3 nc_href (every NC PROPFIND/REPORT href): Vec+join+format -> one pre-sized
  buffer, keeping urlencoding::encode (byte-identical). 38 -> 27 allocs/op.
- M4 NC preview fileId: collect-then-parse -> borrow-slice parse. 4 -> 0 allocs.

Gates: sharedLanes/listContext.bench.test.ts, examples/bench_round16_micro.rs
(GATE PASS all sections). Frontend: vitest 331 pass, svelte-check clean.
Backend: clippy -D warnings clean, 524 lib tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193NjactJVqfU32gxeJDj8m
2026-07-19 17:30:22 +00:00
Edouard Vanbelle ef7143da43 fix(front-test): change the i18n load test
return from Claude:

    The bench measures nothing reliable at this scale. afterMs=13.21 vs beforeMs=12.56 is ~650ns
    per lookup for both paths; at that granularity a single GC pause or scheduler hiccup easily
    swamps the actual gain, and the "after" path happens to run first (cold caches), so it gets
    penalised on unlucky runs. Your local station happens to warm up before the noise lands; CI
    shared-runners often don't.

    Fix: run each path a few times and take the minimum (min is noise-proof — noise only slows
    things down, never speeds them up):
2026-07-19 17:39:35 +02:00
Edouard Vanbelle 920bf5775e fix(ui): quota edition: keep respect of design system 2026-07-19 17:24:42 +02:00
Edouard Vanbelle 6e0e31c694 admin > users colunms 2026-07-19 17:04:29 +02:00
Edouard Vanbelle 35f4984463 i18n: add missing key in admin > users section 2026-07-19 16:26:11 +02:00
Edouard Vanbelle 26b8b530c9 security(users): prevent ext. user to become admin 2026-07-19 16:26:11 +02:00
Edouard Vanbelle 2a08fe83ae feat(user): admin can promote external user + security on deletion
promotion by admin of external user into internal possible
    deletion of a user request admin to enter it's email, this is to prevent any miss click
2026-07-19 16:26:11 +02:00
Edouard Vanbelle e003a8c55b feat(admin): display external users for security reasons 2026-07-19 16:26:11 +02:00
Edouard Vanbelle 03a63d0161 feat(drive): use GroupVignette for group as member 2026-07-19 16:26:11 +02:00
Edouard Vanbelle 5982efd783 i18n(drive): correct locales for drive sections 2026-07-19 16:26:07 +02:00
Edouard Vanbelle 05dfda9aa3 feat(drive): add quota update handler
per today: admin only can update quota
    shared drive can have quota updated (personal drive's quota belong to user's quota)
2026-07-19 16:25:41 +02:00
Edouard Vanbelle 0133450d40 feat(drive): show user in drive's admin
and fix the shared drive quota display
2026-07-19 16:25:41 +02:00
Edouard Vanbelle 5de65e60ba feat(drive): UX: refresh drive list on change
this fix an issue where the list was cleared but never refreshed
2026-07-19 16:25:41 +02:00
Claude 3be85fa9f0 perf: round 15 — grouped-listing O(N²) rebucket, exif/reseed allocs, tantivy zero-hit snippet skip
Benchmark-gated, same rule as rounds 2–14: every change ships with a
BEFORE/AFTER benchmark and an equivalence/safety gate; an AFTER that doesn't
beat its BEFORE is rolled back. The rule is encoded per harness (GATE FAIL
non-zero exit in the Rust examples, threshold expect() in vitest).

F1 — Grouped listings (trash / recent / favorites / shared-with-me)
re-bucketed the WHOLE accumulated list on every infinite-scroll page.
ResourceSectionsBuilder (new, off the reactive graph) re-buckets only the
fresh page and hands VirtualList the same rows array reference for untouched
buckets. 50×50 (2 500-item) drain: 63 750 → 2 500 bucketOf calls (25.5×),
12.5 → 1.3 ms wall (9.9×); O(N²/page) → O(N). Deep-equal to the full-rebuild
reference at every page for both a contiguous (date) and a non-contiguous
(trash-by-drive) group-by; reference-stability + fallback gated.

B1 — exif Make/Model: the display String was thrown away to allocate the
trimmed copy; display_value_trimmed trims in place (drain + truncate), 2 → 1
alloc per field (8 → 4 allocs/op, 1.26×).

B2 — content-index worker: text_extractor::supports (lowercases MIME +
extension) was called twice per file per drain batch; classify once into a
Vec<bool> and thread it through both uses. 256-file batch: 704 → 353 allocs,
34.5 → 16.7 µs (2.07×).

B3 — tantivy: skip SnippetGenerator::create on a zero-hit content search
(return Ok(vec![]) once top_docs.is_empty()); the per-hit loop was empty.
400-doc index: 1 575.6 → 1 237.2 ns (1.27×), widens with index size.

Harnesses: examples/bench_round15_micro.rs, examples/bench_round15_tantivy.rs,
frontend resourceSections.bench.test.ts; writeup in benches/ROUND15.md. Also
normalizes two round14 bench examples that were committed unformatted
(cargo fmt --all).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o47jSrtL7xuNGTHXmtiYL
2026-07-19 11:36:47 +00:00
Claude c930f865b0 perf: round 14 — faces narrow projection, auth per-request allocs, CalDAV emit buffers, frontend set churn
Benchmark-gated (benches/ROUND14.md); every change ships a BEFORE/AFTER
benchmark with an equivalence gate and is rolled back on regression (the
rule is encoded as a GATE FAIL exit / threshold expect).

Backend
- Q1 faces_for_file → narrow face_boxes_for_file(id, person_id, bbox) with the
  caller filter pushed into SQL: drops the 2 KiB embedding BYTEA + 6 unused
  columns per face. 15-face lightbox open 0.312→0.219 ms, 32 KB→840 B/req.
- A1 cookie auth uses the borrow-only extract_cookie_str (already backs CSRF)
  instead of extract_cookie_value's owned String: -1 alloc/cookie request.
- A2 compute_relevance ASCII case-fold fast path vs name.to_lowercase() per
  result row (Unicode fallback preserved): 1.40x, 12→3 allocs/page.
- A3 sub pre-parsed to Uuid at decode time (TokenClaims.sub_id) vs re-parsing
  the 36-char claim on every request incl. cache hits: 22.7→0.7 ns.
- A4 auth + NextCloud middlewares borrow request.headers() instead of taking
  axum's HeaderMap extractor (a full map clone): 2→0 allocs/authed request.
- A5 CalDAV getlastmodified via the stack rfc2822_utc (byte-identical to
  chrono) vs a per-event to_rfc2822() heap String: 5→0 allocs.
- A6 CalDAV per-event href + quoted etag written into reused page buffers vs a
  fresh format! pair per event: 3.48x, 240→6 allocs/40-event page.

Frontend
- F1 t() shares one frozen EMPTY_PARAMS for the no-interpolation call forms vs
  a throwaway {} per call: -1 alloc/call.
- F2 favorites favoriteIds is a persistent SvelteSet with per-page add (clear
  on reset) vs a brand-new set over the whole accumulated list each page:
  22.3x over a 40-page drain (O(N^2)→O(N)).

Verified: cargo check --all-targets, cargo clippy -D warnings, both bench
packs (GATE PASS), frontend npm run check + vitest (4/4). ROUND14.md also
records the investigated-but-deferred backlog (music N+1, contact vcard
over-fetch, CachedBlobBackend syscalls, ResourceList.sections builder, etc.).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PymgCdK78NzUF3oRAQCJfN
2026-07-19 10:22:12 +00:00
Claude f58d72a780 perf: round 13 — grouped-view virtualization, notification/login query narrowing, HTTP dedup, locale precompute
Benchmark-gated (BEFORE/AFTER + equivalence/safety gate per change), same
discipline as rounds 2-12. Full write-up in benches/ROUND13.md.

Shipped:
- V1 Grouped views windowed (files route + ResourceList). The grid arm was
  the last unwindowed path (trash is grouped-by-default in grid): each
  swimlane now feeds its own VirtualList, outer container a flex stack.
  vitest gate: 800-item grouped grid mounts <120 .file-item (was 800).
- Q1 get_users_by_ids drops the <=512 KiB avatar image + ui_preferences
  JSONB (notification path never reads them). 30-member fan-out 8.60 ->
  0.25 ms (34.3x), ~7.7 MB off the wire.
- Q2 Login provisioning is_empty() -> SELECT EXISTS for calendar + address
  book (every login). 0.193 -> 0.170 ms, widens with owned-row count.
- Q3 Recent-access prunes only when the upsert inserted (RETURNING xmax=0)
  — a re-access can't grow the set. 0.567 -> 0.324 ms (1.75x).
- L1 Locale supported-codes precomputed once vs rebuilt per anonymous
  request. 616 -> 17.3 ns (35.7x), 18 -> 1 allocs.
- H1 Duplicate /api TraceLayer removed (global stack already wraps it).
  1.86 -> 1.42 us/request, -6 allocs.
- H2 client_ip span field: borrow-only ClientIpDisplay vs owned String.
  187 -> 173 ns, -1 alloc.

Not shipped (discipline): the "media hooks read the blob 3x" lead was a
correctness bug, not a perf dup — the raw-path metadata/faces readers
resolve only for local+unencrypted+single-chunk blobs and silently produce
nothing otherwise. Flagged for maintainers; routing through read_blob_bytes
is a correctness fix (perf-neutral-to-negative), not a benchmark-gated
perf change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfidAJD5AHw23jtvBUNamB
2026-07-19 08:17:48 +00:00
Claude 50eca0627f perf: round 12 — auth write-path narrowing, fused quota gate, moka blob-cache index, media single-read, sized listing JSON
Benchmark-gated round (benches/ROUND12.md; every change ships with a
BEFORE/AFTER harness + equivalence gates, one candidate rejected by its
own bench):

DB / query shapes (bench_round12_queries):
- NC sharee search: username-only projection instead of the 21-column row
  (incl. the <=512 KiB avatar) per match, + gin_trgm_ops indexes on
  auth.users for the leading-wildcard ILIKE (4.98x; 54.7x with index).
- Password login: delete the redundant full-row update_user — create_session
  already stamps last_login_at in its own txn (4.45x per login).
- Email-verified stamp: narrow conditional UPDATE (8.9x); OIDC repeat login
  now compares profile state in memory and issues ZERO queries when nothing
  changed (was: full 17-column rewrite per login).
- Refresh rotation: revoke+insert+stamp fused into one transaction via new
  rotate_session port method (1.18x).
- WOPI CheckFileInfo / authorize_wopi_access: require(Read) + get_file +
  check(Update) overlapped with tokio::join!, original result precedence
  (cold 1.34x).
- Upload quota gate: user-envelope + drive-cap checks fused into ONE
  round-trip (check_upload_quotas) — the NC chunked PUT pays this per
  chunk (1.81x, 2 -> 1 queries/chunk); shared verdict evaluators keep
  error shapes byte-identical.

CPU / allocs (bench_round12_micro):
- sized_json: pre-sized listing serialization replacing axum Json's 128 B
  seed + doubling-realloc chain on files/folder-resources/photos/search
  responses (1.40x, 13 -> 2 allocs per 500-row page; byte-identical).
- Security headers: 4 SetResponseHeaderLayer folded into the CSP middleware
  pass (5 layers -> 1; 1.43x per request, -26 allocs; header set gated
  byte-identical incl. 304s).
- Media capture-metadata: single-read extraction — nom-exif now parses the
  buffer kamadak already read (zero-copy Bytes) and videos open once with a
  kind() dispatch; per-image opens 2-3 -> 1 (1.44x warm geomean, 1.6-3.2x
  cold cache; extraction outputs gated identical incl. the MIME-mislabel
  track fallback).
- Chunked-upload session ops: owner gate folded into the operation's own
  DashMap lookup + stack-encoded uuid compare (5 -> 3 lookups, -2 allocs,
  1.28x per chunk).

Blob cache (bench_blob_cache_index + round-3 regression guard):
- CachedBlobBackend index: tokio::sync::Mutex<LruCache> -> moka::sync::Cache
  with byte weigher. The mutex serialized every cached chunk read and scaled
  NEGATIVELY (2.08 -> 1.07 Mops/s from 1 -> 2 readers); moka probes are
  lock-free (2.17x at K=2). Byte budget now enforced by moka (manual
  current_size + collect_evictions machinery deleted); eviction listener
  unlinks size-evicted files only (Replaced entries keep their file —
  gated). Single-flight miss gate unchanged (16 concurrent misses -> 1
  fetch re-verified via the round-3 harness).
- put_blob now populates the cache BEFORE the inner backend consumes the
  source file (the old order failed 100% of the time — local renames,
  S3/Azure delete the source — so the first read after a whole-file put
  re-downloaded from the remote); inner-put failure invalidates the entry.

Frontend (vitest gates):
- List-view thumbnails request the 150px icon rendition instead of 400px
  preview into a 40px slot (~7.1x fewer pixels, ~4-5x fewer bytes per
  thumbnail across list views); grid keeps preview.

Rejected by its own bench (kept as evidence in bench_round12_micro §2):
- Single-pass compression predicate: the monomorphized And-chain already
  costs ~4.6 ns / 0 allocs total; the fused node measured within noise.

New migration: 20260719000000_users_search_trgm.sql (trgm indexes).
Deferred with prepared design: grouped file/grid view virtualization
(single-VirtualRows flatten, the photos pattern) — next round's headline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfidAJD5AHw23jtvBUNamB
2026-07-19 01:32:00 +00:00
Claude f621c96c63 Merge origin/main (ResourceList item-based refactor #612, IdP auto-redirect #593) into round 11
Conflict resolution re-applies the round-11 SPA optimizations onto the
refactored item-based ResourceList (which independently converged on the
favoriteIds-prop star — S2 is now upstream's shape):

- ResourceList.selectedItems: upstream reintroduced the full O(N)
  items.filter per selection toggle; replaced with the round-11
  id-index projection — O(k·log k), item order preserved
  (benches/ROUND11.md §S1)
- favorites/recent pages: upstream's rewrite kept the host-side
  selectedItems shadow + unused selectedIds mirror and ignored the
  batchToolbar snippet param; re-applied the param-consuming shape and
  deleted the shadows

Validation on the merged tree: clippy --all-features --all-targets
-D warnings clean; cargo test --workspace 524 passed; frontend npm run
check 0 errors; vitest 310 passed (upstream's new tests + round-11 gates)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABhTEHuGujvwoodh67Kga7
2026-07-18 23:15:10 +00:00