perf(webapp): slim calendar, home, assets-index and bookings loaders - #2738
perf(webapp): slim calendar, home, assets-index and bookings loaders#2738carlosvirreira wants to merge 6 commits into
Conversation
Profiled with a V8 sampling profiler and pg_stat_statements against a seeded local rig (5k assets, 400 bookings). The dominant costs were loader payloads fetching data the routes never render: - calendar: getBookings included the full bookingAssets subtree (~1,775 asset rows plus QR/kit pivots per request) for events that render scalars only. getBookings gains includeAssets (default true); the calendar opts out, skips its unused count, slims custodian/creator selects, and caps the window fetch at 1,000 rows (warn-logged when hit). - home: all four getBookings calls opt out of assets; location distribution is groupBy-first on AssetLocation; the checklist query joins the loader's Promise.all and drops a duplicate custody count. - assets index: the advanced-index CTE paginates before ranking on the default path and materializes only active sort-key columns. Page output verified identical (md5 of page ids, two sorts, two pages). - bookings index: the row-expansion drawer payload (~99% never opened) moves to a new authenticated resource route fetched on open, with an in-drawer error/retry state and shouldRevalidate false so closed drawers skip action revalidation. The /bookings document shrinks from 343KB to 108KB. Measured on the rig at 10 rps sustained per route: calendar p95 1473ms -> 29ms, home 764ms -> 45ms, bookings index 1200ms -> 137ms; every core route now holds p95 under 300ms at 10 and 15 rps sustained load.
🩺 React Doctor — webappFindings on the files changed by this PR:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5188ea5a56
ℹ️ 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".
WalkthroughThe PR adds lazy booking-sidebar loading with authorized aggregation, narrows booking and dashboard queries, caps calendar results, optimizes asset sorting pagination, and adds sidebar, SQL-shape, and performance-seeding changes. ChangesBooking assets sidebar
Booking and dashboard query loading
Advanced asset query pagination
Performance test seeding
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant BookingAssetsSidebar
participant AssetsSidebarLoader
participant Database
User->>BookingAssetsSidebar: open assets sheet
BookingAssetsSidebar->>AssetsSidebarLoader: request booking assets
AssetsSidebarLoader->>Database: authorize booking and load aggregates
Database-->>AssetsSidebarLoader: assets, consumption logs, checkout rows
AssetsSidebarLoader-->>BookingAssetsSidebar: return sidebar payload
BookingAssetsSidebar-->>User: render assets and progress
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/webapp/app/modules/booking/service.server.ts (1)
9042-9060: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse
takeCapfor the home dashboard merge query
page: 1, perPage: 1000is clamped totake: 20, so this ONGOING/OVERDUE custodian merge only receives 20 bookings. Add a boundedtakeCaphere (or otherwise avoid silently capping the query), since the other home calls only fetch 5 rows andtakeAllwould make the query unbounded.🤖 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 `@apps/webapp/app/modules/booking/service.server.ts` around lines 9042 - 9060, Update the home dashboard’s ONGOING/OVERDUE custodian merge query to pass a bounded takeCap so the requested 1000 bookings are not silently limited to the default 20. Keep the query bounded rather than enabling takeAll, and preserve the existing pagination behavior for other callers.
🧹 Nitpick comments (1)
apps/webapp/app/modules/asset/query.server.ts (1)
2736-2753: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a cheap-phase assertion for direct sort-key gating.
The current
query.server.test.tsassertion searches the full SQL, wherea.valueanda.quantityalso appear in the heavy lateral projection. It cannot detect regressions in this slim CTE. Slice beforesorted_asset_queryand assert default sorting omits value/quantity whilevalue:ascorquantity:ascincludes the respective key.🤖 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 `@apps/webapp/app/modules/asset/query.server.ts` around lines 2736 - 2753, The test assertion for direct sort-key gating must inspect only the slim CTE, not the full generated SQL. In query.server.test.ts, slice the SQL before sorted_asset_query and assert default sorting excludes a.value and a.quantity, while value:asc and quantity:asc include their corresponding direct sort keys; update the existing assertions around DIRECT_SORT_KEY_SELECTS behavior.
🤖 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 `@apps/webapp/app/routes/_layout`+/home.tsx:
- Around line 405-411: Exclude soft-deleted members from onboarding completion:
update the directCustodians source query in
apps/webapp/app/routes/_layout+/home.tsx lines 405-411 to filter active team
members with deletedAt: null before deriving hasCustodies; also update the
team-member count in apps/webapp/app/utils/dashboard.server.ts lines 275-277 to
include deletedAt: null.
---
Outside diff comments:
In `@apps/webapp/app/modules/booking/service.server.ts`:
- Around line 9042-9060: Update the home dashboard’s ONGOING/OVERDUE custodian
merge query to pass a bounded takeCap so the requested 1000 bookings are not
silently limited to the default 20. Keep the query bounded rather than enabling
takeAll, and preserve the existing pagination behavior for other callers.
---
Nitpick comments:
In `@apps/webapp/app/modules/asset/query.server.ts`:
- Around line 2736-2753: The test assertion for direct sort-key gating must
inspect only the slim CTE, not the full generated SQL. In query.server.test.ts,
slice the SQL before sorted_asset_query and assert default sorting excludes
a.value and a.quantity, while value:asc and quantity:asc include their
corresponding direct sort keys; update the existing assertions around
DIRECT_SORT_KEY_SELECTS behavior.
🪄 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: CHILL
Plan: Pro Plus
Run ID: b641f99a-70e0-4fb6-a15a-abfc17311163
📒 Files selected for processing (9)
apps/webapp/app/components/booking/booking-assets-sidebar.tsxapps/webapp/app/modules/asset/query.server.tsapps/webapp/app/modules/booking/constants.tsapps/webapp/app/modules/booking/service.server.tsapps/webapp/app/routes/_layout+/bookings._index.tsxapps/webapp/app/routes/_layout+/home.tsxapps/webapp/app/routes/api+/bookings.$bookingId.assets-sidebar.tsapps/webapp/app/utils/dashboard.server.tsapps/webapp/test/components/booking/booking-assets-sidebar.test.tsx
- co-locate the sidebar dual-mode test with its component (repo test convention; flagged by Codex review) - home custodian merge: pass takeCap 1000 so the ONGOING/OVERDUE merge sees every booking instead of the perPage-clamped first 20 (flagged by CodeRabbit; uses the takeCap primitive this PR introduces) - assets-index query test: assert direct sort-key gating against the sliced cheap phase instead of the full SQL, where the heavy lateral also matches (CodeRabbit nitpick); default sort now provably omits value/quantity from the slim CTE
|
Replies to the two CodeRabbit comments that could not be posted inline: Outside-diff: use Nitpick: cheap-phase assertion for direct sort-key gating (query.server.ts 2736-2753). Agreed and fixed in cf8d051: the test now slices the SQL before |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@apps/webapp/app/modules/asset/query.server.test.ts`:
- Around line 1129-1135: Strengthen the slim SELECT assertions around cheap so
each active-sort case also verifies that the other sort key is absent: the
valuation sort must exclude assetQuantity, and the quantity sort must exclude
assetValue. Keep the existing inclusion checks for the requested sort column.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 8f706659-f7bc-4e07-bb14-f840297d85d8
📒 Files selected for processing (3)
apps/webapp/app/components/booking/booking-assets-sidebar.test.tsxapps/webapp/app/modules/asset/query.server.test.tsapps/webapp/app/routes/_layout+/home.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/webapp/app/routes/_layout+/home.tsx
Follow-up to CodeRabbit's re-review: inclusion-only assertions would pass even if gating regressed to all-or-nothing. Valuation sort legitimately selects BOTH value and quantity (it orders by total value = assetValue * assetQuantity), so the exclusion is asserted against an unrelated key there, and against value on the single-key quantity sort.
Integrates the quantity-tracked availability wave with the lazy assets drawer. - ListBookingsContent (new shared row component on main) goes dual-mode: bookingAssets is optional and deliberately NOT normalised to [] — an empty array would read as 'eager with zero assets' and suppress the drawer fetch. The 'Includes unavailable assets' badge derives from the pivots when they are eager (child bookings tabs) and from the index loader's bounded bookingsWithUnavailableAssets query when they are not. - BookingAssetsSidebar keeps main's qty-progress + stock-badge props and falls back to the lazily fetched payload when a caller omits them. - getBookings keeps main's include shape, hoisted to a module const so the includeAssets gate can omit it without changing the type for other callers. - The two independently-added test suites for the sidebar are split by concern: QT stock badges stay in booking-assets-sidebar.test.tsx (plus a useFetcher stub the dual-mode component now requires), and the eager/lazy contract moves to booking-assets-sidebar-dual-mode.test.tsx.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/webapp/app/components/booking/booking-assets-sidebar.tsx (1)
725-749: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAdd
availableUnitsByAssetto the lazy sidebar payload.The bookings-index renderer passes
bookingwithoutavailableUnitsByAsset, andassets-sidebaronly returnsbookingAssets,dispositionedByAsset,dispositionBreakdownByAsset, andcheckedOutByAsset. The component then readsundefinedforavailability, soresolveQtyStockBadgeVariantreturnsnulland the bookings-index asset drawer cannot renderInsufficientStockBadgeorPendingReturnBadge. Either compute the workspace-availability map inassets-sidebarand merge it like the other maps, or remove the stock-badge support for the index drawer if this is intentional.🤖 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 `@apps/webapp/app/components/booking/booking-assets-sidebar.tsx` around lines 725 - 749, Update the lazy assets-sidebar loader and its consumer to provide and merge an availableUnitsByAsset map alongside the other quantity maps, using the existing availability calculation used by eager callers. Ensure the resolved availability reaches resolveQtyStockBadgeVariant so the bookings-index drawer can render InsufficientStockBadge and PendingReturnBadge.
🧹 Nitpick comments (3)
apps/webapp/scripts/seed-perf.ts (1)
51-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDerive
TARGETS.customFieldsfrom the plan list.
TARGETS.customFieldsis8andbuildCustomFieldPlans()returns 8 hardcoded entries. The summary log at line 533 reports the constant, not the inserted row count. If a plan entry is added or removed, the log becomes wrong. LogcustomFieldPlans.lengthinstead, or derive the target from the plan.Also applies to: 274-285
🤖 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 `@apps/webapp/scripts/seed-perf.ts` around lines 51 - 63, Update the custom-field target reporting to derive its value from buildCustomFieldPlans() rather than the hardcoded TARGETS.customFields constant. Ensure the summary log uses customFieldPlans.length so it always reflects the number of plans inserted, including when plan entries change.apps/webapp/app/routes/_layout+/bookings._index.tsx (1)
212-260: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRun the stock-conflict decoration and unavailable-assets lookup concurrently.
decoratedBookings(lines 218-221) andbookingsWithUnavailableAssets(lines 234-260) both depend only onbookings, not on each other's result. They currently run as two sequential awaits, adding one extra round trip of latency to every/bookingsrequest. Since this PR's goal is reducing loader latency, run them withPromise.allinstead.⚡ Proposed fix to parallelize the two independent queries
- const decoratedBookings = await decorateBookingsWithStockConflicts({ - bookings, - organizationId, - }); - - const bookingIdsOnPage = bookings.map((b) => b.id); - const bookingsWithUnavailableAssets = - bookingIdsOnPage.length > 0 - ? ( - await db.booking.findMany({ - where: { - id: { in: bookingIdsOnPage }, - organizationId, - bookingAssets: { - some: { - asset: { - OR: [ - { availableToBook: false }, - { custody: { some: {} } }, - ], - }, - }, - }, - }, - select: { id: true }, - }) - ).map((b) => b.id) - : []; + const bookingIdsOnPage = bookings.map((b) => b.id); + const [decoratedBookings, bookingsWithUnavailableAssets] = await Promise.all([ + decorateBookingsWithStockConflicts({ bookings, organizationId }), + bookingIdsOnPage.length > 0 + ? db.booking + .findMany({ + where: { + id: { in: bookingIdsOnPage }, + organizationId, + bookingAssets: { + some: { + asset: { + OR: [ + { availableToBook: false }, + { custody: { some: {} } }, + ], + }, + }, + }, + }, + select: { id: true }, + }) + .then((rows) => rows.map((b) => b.id)) + : Promise.resolve<string[]>([]), + ]);🤖 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 `@apps/webapp/app/routes/_layout`+/bookings._index.tsx around lines 212 - 260, Run decorateBookingsWithStockConflicts and the unavailable-assets db.booking.findMany lookup concurrently with Promise.all, since both depend only on bookings and organizationId. Preserve the existing result values and empty-page behavior, then destructure the two resolved results for subsequent loader use.apps/webapp/app/components/booking/booking-assets-sidebar.tsx (1)
68-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the duplicated booking-asset Prisma select shared by both files. Both files independently declare the same nested
assetselect for the booking-asset payload. This duplication has already caused drift:booking-assets-sidebar.tsx's select omitsconsumptionTypewhilelist-bookings-content.tsx's select still includes it.
apps/webapp/app/components/booking/booking-assets-sidebar.tsx#L68-L133: extract the nestedassetselect fromBookingWithAssetsinto a shared, exported select/type (for example alongside the shared include already referenced inapps/webapp/app/modules/booking/constants.ts) and import it here.apps/webapp/app/components/booking/list-bookings-content.tsx#L102-L208: replace the inline duplicateassetselect inListBookingsContentPropswith the same shared select/type used bybooking-assets-sidebar.tsx.🤖 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 `@apps/webapp/app/components/booking/booking-assets-sidebar.tsx` around lines 68 - 133, Extract the duplicated nested asset select from BookingWithAssets in apps/webapp/app/components/booking/booking-assets-sidebar.tsx#L68-L133 into a shared exported select/type, colocated with the existing booking include in apps/webapp/app/modules/booking/constants.ts, and preserve all fields including consumptionType. Replace the inline asset select in ListBookingsContentProps at apps/webapp/app/components/booking/list-bookings-content.tsx#L102-L208 with the shared select/type, importing it from the shared module so both payloads remain synchronized.
🤖 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 `@apps/webapp/scripts/seed-perf.ts`:
- Around line 42-45: Remove the unused ExtendedPrismaClient type import from the
seed-perf module, leaving the Prisma and createDatabaseClient imports unchanged.
---
Outside diff comments:
In `@apps/webapp/app/components/booking/booking-assets-sidebar.tsx`:
- Around line 725-749: Update the lazy assets-sidebar loader and its consumer to
provide and merge an availableUnitsByAsset map alongside the other quantity
maps, using the existing availability calculation used by eager callers. Ensure
the resolved availability reaches resolveQtyStockBadgeVariant so the
bookings-index drawer can render InsufficientStockBadge and PendingReturnBadge.
---
Nitpick comments:
In `@apps/webapp/app/components/booking/booking-assets-sidebar.tsx`:
- Around line 68-133: Extract the duplicated nested asset select from
BookingWithAssets in
apps/webapp/app/components/booking/booking-assets-sidebar.tsx#L68-L133 into a
shared exported select/type, colocated with the existing booking include in
apps/webapp/app/modules/booking/constants.ts, and preserve all fields including
consumptionType. Replace the inline asset select in ListBookingsContentProps at
apps/webapp/app/components/booking/list-bookings-content.tsx#L102-L208 with the
shared select/type, importing it from the shared module so both payloads remain
synchronized.
In `@apps/webapp/app/routes/_layout`+/bookings._index.tsx:
- Around line 212-260: Run decorateBookingsWithStockConflicts and the
unavailable-assets db.booking.findMany lookup concurrently with Promise.all,
since both depend only on bookings and organizationId. Preserve the existing
result values and empty-page behavior, then destructure the two resolved results
for subsequent loader use.
In `@apps/webapp/scripts/seed-perf.ts`:
- Around line 51-63: Update the custom-field target reporting to derive its
value from buildCustomFieldPlans() rather than the hardcoded
TARGETS.customFields constant. Ensure the summary log uses
customFieldPlans.length so it always reflects the number of plans inserted,
including when plan entries change.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 342425e0-141f-409c-9b6c-156d3ff41030
📒 Files selected for processing (9)
apps/webapp/app/components/booking/booking-assets-sidebar-dual-mode.test.tsxapps/webapp/app/components/booking/booking-assets-sidebar.test.tsxapps/webapp/app/components/booking/booking-assets-sidebar.tsxapps/webapp/app/components/booking/list-bookings-content.tsxapps/webapp/app/modules/asset/query.server.test.tsapps/webapp/app/modules/asset/query.server.tsapps/webapp/app/modules/booking/service.server.tsapps/webapp/app/routes/_layout+/bookings._index.tsxapps/webapp/scripts/seed-perf.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/webapp/app/modules/booking/service.server.ts
seed-perf.ts is an operator-only script for the local perf rig, not product code — it was swept in by a blanket stage on the merge commit. Removing it also resolves the unused-import lint finding CodeRabbit raised against it.
|
Reply to the outside-diff Major ( The observation is accurate but it is pre-existing on Verified on What this PR does change is the other three maps, and those are preserved: the index used to compute them page-wide in the loader, and the resource route now returns all three ( Adding availability to the index drawer would be a new feature (it needs the per-asset workspace-availability computation run for the whole page or per drawer open), and it would land on the perf-sensitive surface this PR is slimming. Happy to do it as a follow-up if you want the badges there, @DonKoko — it is a product call, not a merge blocker. |
Makes four hot loaders fetch only what their routes actually render. No schema changes, no new dependencies, no caching layer.
Headline:
/calendarserver p95 drops 136ms → 28ms and/home60ms → 33ms, measured back-to-back against this branch andmainon the same seeded database./bookingsships a 343KB → 109KB document.Why these routes were slow
Profiled with a V8 sampling profiler inside the production server plus
pg_stat_statements, against a locally seeded org (5,000 assets, 400 bookings, 120 kits, 15,000 notes). Every hotspot was the same shape: a loader fetching a subtree the page never reads./calendarbookingAssetssubtree: ~1,775 asset rows + QR/barcode/kit pivots per request (~1.5MB through Prisma)/homegetBookingscalls/bookings/assetsWhat changed
1.
getBookingsgainsincludeAssets(defaulttrue) andtakeCap. WhenincludeAssets: false, thebookingAssetskey is omitted from the Prisma include entirely. Every existing caller keeps today's behaviour by default.2.
/calendaropts out of assets, skips acountquery it never read, slims the custodian/creator selects to the five fields the event mapping uses, and replaces an unboundedtakeAllwithtakeCap: 1000(warn-logged if a workspace ever hits it — previously this query had no ceiling at all).3.
/homeopts all four calls out of assets. Two correctness fixes came out of the same pass:perPage: 1000, which silently clamps to 20 — so "top custodians" was computed from the first 20 active bookings. It now usestakeCapand sees them all.groupBy-first (under 1ms).Promise.alland a duplicate custody count was dropped.4.
/assetsadvanced-index CTE paginates before ranking on the default path, so Postgres does a top-N sort on the existing(organizationId, createdAt, id)index instead of sorting every matching row to feedROW_NUMBER. It also materialises only the sort-key columns actually referenced by the activeORDER BY. Search and custom-field sorts route through the previous SQL byte-identically.5.
/bookingsstops shipping the assets-drawer payload. A new authenticated resource route (GET /api/bookings/:bookingId/assets-sidebar) returns the identical shape on open.BookingAssetsSidebaris dual-mode: eager callers (booking overview, the asset/kit/user/me bookings tabs) are untouched and never fetch; the index fetches on open with a spinner, an error state with retry, andshouldRevalidate: falseso closed drawers don't refetch after page actions.Measurements
Open-loop 10 rps sustained per route, 25s, production build, same seeded DB,
mainand this branch measured minutes apart. Two samples each; the numbers below are the second (the first had one machine-noise outlier on an untouched route, which is why control routes are included).mainp50 / p95/calendar/home/assets/bookings/kits(control, untouched)/locations(control, untouched)Document sizes, same session:
main/bookings/home/calendarOn
/bookingsbeing flat here: it trades the pivot payload for one bounded query that computes the "Includes unavailable assets" badge, so wall-clock is a wash at this data size while the document shrinks by 3x. The payload reduction is the durable win; it scales with assets-per-booking, which this seed keeps modest.Rig caveats, stated plainly: this is a local single-process rig, not production. Absolute numbers are optimistic (no network RTT, warm cache, one Node process); the relative change and the payload reduction are the meaningful signals. The measurement harness is open-loop (arrivals independent of completions) because a closed loop measures queue collapse rather than service time.
Behaviour notes for review
/bookings, drawer content now loads when you open a row (spinner on first open; reopening renders the cached payload immediately and refreshes in the background). Open-drawer rendering is byte-identical because the service and the resource route share one include constant.requirePermission(booking/read), org scoping, DRAFT-creator visibility, and the restricted-role custody scope — a SELF_SERVICE/BASE user cannot fetch a drawer for a booking the list wouldn't show them.InsufficientStockBadge/PendingReturnBadge, becauseavailableUnitsByAssetis produced only by the booking-overview loader onmaintoo — neither bookings-list surface has ever supplied it. Adding it to the index would be a new feature on a perf-sensitive surface; happy to do it separately if you want those badges there.Merge history
This branch was merged with
maintwice, most recently to integrate the quantity-tracked availability wave. The interesting part of that resolution:ListBookingsContent(main's new shared row component) hadbookingAssets: rawItem.bookingAssets ?? [], and an empty array would read as "eager with zero assets" and silently suppress the drawer fetch. The prop is now optional withundefineddeliberately preserved, and the code says so, so the normalisation doesn't come back. Main's stock-conflict pill, disposition maps, and QT badge props are all preserved.Testing
🤖 Generated with Claude Code