# /orders Page Performance — Findings & Implementation Log

**Investigation started:** 2026-05-02
**Last updated:** 2026-05-03 (post-implementation)
**Status:** Optimizations shipped. Test pages removed. Live page noticeably faster.

---

## TL;DR

The DB was **not** the bottleneck. PHP/Blade rendering, body-appended tooltip leaks, and a 100ms-interval JS scrubber were. Across two days of investigation + iterative shipping we eliminated:
- ~200 wasted DB queries per render (N+1 in `order-assignments.blade.php`)
- ~150 body-appended tooltip nodes per render (replaced with shared singletons)
- ~600+ inline event handlers per render (event delegation)
- ~500+ KB of inline-style HTML per render (CSS classes)
- A `setInterval(disableFilamentTooltips, 100)` + `MutationObserver(document.body)` combo burning 5–10% CPU
- 327+ console.log calls per render
- A 2-second polling loop building per-element body tooltips for assignment chips

Plus added: hover-to-open dropdowns, real-time chip pop-out animations, lazy-build OOS supplier badges via `<template>`, faster real-time polling.

---

## Original baseline (2026-05-02, before any changes, 100 rows)

| Metric                           | Value     |
|----------------------------------|-----------|
| TTFB                             | 372 ms    |
| DOM Content Loaded               | 893 ms    |
| Full load                        | 991 ms    |
| HTML transferred (gzip)          | 313 KB    |
| HTML decoded                     | **3.4 MB**|
| Total DOM nodes                  | **8,967** |
| Buttons                          | 399       |
| `onclick=` handlers              | **651**   |
| Hidden `dropdown-menu` elements  | **672**   |
| Tooltips appended to `<body>`    | 217       |
| Slowest JS file (table.js)       | 1,045 ms  |

After every Livewire interaction, the page leaked ~209 tooltips and a custom `[CLEANUP]` script removed them.

### Pagination Scaling (live `/orders`, original)

| Page size | DOM nodes | Livewire roundtrip | Server (PHP) | DB queries |
|-----------|-----------|--------------------|--------------|------------|
| 10        | 3,421     | **782 ms**         | 154 ms       | 35         |
| 25        | 6,617     | **1,028 ms**       | 196 ms       | 65         |
| 50        | 12,133    | **2,705 ms**       | 308 ms       | 115        |
| 100       | 19,947    | **6,481 ms**       | 511 ms       | 215        |

25 → 100 rows = ~6× slower. Cliff is at 25 → 50 (DOM nodes, layout cost).

### Where the original backend time went (50-row render)

Total request: **308 ms server**.

| Bucket                              | Time    | Notes |
|-------------------------------------|---------|-------|
| `count(*) from orders`              | 59 ms   | Used by paginator. **Single biggest query.** Full-index scan over 738K rows (uses `is_full_delivered` index). |
| 50 × `order_assignments WHERE id = ?`        | ~5 ms   | **N+1, redundant — already eager-loaded** |
| 50 × `order_department_assignments WHERE id = ?` | ~5 ms   | **N+1, redundant** |
| All other DB                        | ~5 ms   | orders fetch, items, notes, duplicates, statuses |
| **PHP/Blade rendering**             | **~230 ms** | The dominant cost. |

---

## Implementation log — what shipped

### Round 1 — Backend N+1 + first tooltip leak

**1.1 Killed the N+1 in `order-assignments.blade.php`**
- Removed two redundant `$record->load(...)` calls (lines 4–14 of the original).
- Added a graceful lazy-load fallback so other pages reusing this column (e.g. `/old-fulfillment`) continue to work even without eager loading their own queries.
- File: `resources/views/filament/tables/columns/order-assignments.blade.php`
- Result: ~200 fewer DB queries per render at 100 rows. Saved ~80–200 ms server-side.

**1.2 Killed the customer + address column tooltip leak**
- Server now bakes `data-no-enhance="1"` + `onclick="...window.copyText(...)"` + native `title=` directly into customer (company / name / email / phone) and shipping address cells.
- The JS enhancer in `enhanceCustomerInfoCells()` early-returns on `[data-no-enhance]` cells — no per-element body-appended tooltip created.
- CSS added for blue hover highlight (replaces JS-driven hover that set inline color).
- Files: `app/Filament/Resources/OrderResource.php`, `app/Providers/AppServiceProvider.php`, `public/css/custom.css`
- Result: ~400+ fewer tooltip nodes appended to `<body>` per render.

**1.3 Per-row PHP allocation**
- Moved `$validUSStates`, `$stateMap`, `US_COUNTRY_NAMES` from inline arrays inside the `shipping_street_address` closure to `private const` on `OrderResource`. Allocated once per process instead of 100× per render.
- File: `app/Filament/Resources/OrderResource.php`

### Round 2 — Inline-style + handler purge

**2.1 Replaced inline styles with CSS classes**
- Per-row inline `style="..."` strings were repeated thousands of times. Converted to reusable classes in `custom.css`:
  - `.cust-cell`, `.total-cell`, `.total-row`, `.total-row-final`, `.action-btn`, `.action-dropdown`, `.dropdown-item`, `.dropdown-divider`, `.caret-down`, `.supplier-price-badge`, `.oos-toggle-btn`, `.item-action-btn`, `.log-costs-btn`.
- Files: `app/Filament/Resources/OrderResource.php`, `public/css/custom.css`
- Result: ~300–400 KB lighter HTML payload per render.

**2.2 Replaced `onmouseover`/`onmouseout` color-swap closures with CSS `:hover`**
- The Actions column had ~250-char inline closures on every button × 4 buttons × 100 rows = 1,600 closures per render.
- Single CSS rule (`.fi-ta-table .action-btn:hover { ... }`) does the same job.
- Saved ~100 KB HTML, eliminated 1,600 attached closures.

**2.3 Replaced 4-node SVG carets with single CSS triangle**
- `<svg width="10" height="10"><path .../></svg>` × 3 per row × 100 rows replaced with `<span class="caret-down">` (CSS triangle).
- Saved ~250 DOM nodes per render.

**2.4 Lazy-build dropdown items**
- Ship / Follow Up / Resend dropdown menus used to ship pre-built items (~9 hidden `<a>` per row × 100 rows = 900 hidden DOM nodes).
- Now ship as **empty** `<div class="dropdown-menu">`. The `toggleDropdown()` function lazy-builds items from registered templates on first open.
- File: `resources/views/filament/resources/order-resource/pages/list-orders.blade.php`

### Round 3 — Hover-to-open dropdowns

**3.1 Hover triggers Ship chevron / Follow Up / Resend / Record Costs dropdowns**
- Wrapped chevron+menu in `.dropdown-wrapper` (Ship's main button intentionally outside, so hovering Ship doesn't open the dropdown).
- Single delegated `mouseover`/`mouseout` listener handles every dropdown across the page (no per-element handlers).
- Per-wrapper close timers (`WeakMap`) so multiple wrappers don't stomp on each other.
- 200 ms hide delay so user can move from button into menu without it closing.
- Click-outside listener closes any open hover-opened dropdown.
- For per-item Record Costs: synthesizes a click on the existing handler to reuse its build/append/position logic.

### Round 4 — Shared tooltip singletons

**4.1 Created `#shared-copy-tooltip` (single body element + 2 delegated listeners)**
- Replaces N body-appended tooltips for "Click to copy" hints.
- Triggered by elements with `data-copy-tip="..."` attribute.
- Multi-line via `" · "` separator.
- Uses custom attribute (not native `title=`) because `disableFilamentTooltips()` strips title attributes.

**4.2 Created `#shared-rich-tooltip` for HTML content**
- Multi-line, max-width, smart positioning (above element; flips to below if too close to top).
- Triggered by `data-rich-tip="..."` attribute (HTML-encoded).
- Auto-hides on scroll/resize.

**4.3 Migrated SKU + Tracking + Customer + Address tooltips to `#shared-copy-tooltip`**
- SKU: `<span class="partslink-number" data-copy-tip="Click to copy">`
- Tracking: `<button class="tracking-number" data-copy-tip="Left-Click to Copy · Right-Click to Delete">` (multi-line)
- Customer cells: `data-copy-tip="Click to copy company / name / email / phone"`
- Address: `data-copy-tip="Click to copy address"`
- Removed all setTimeout-based body-tooltip creation, all per-element `onmouseover`/`onmouseout` handlers.
- `initializeTooltips()` made into a no-op (was running per Livewire patch, walking every `.partslink-number` + `.tracking-number` to attach handlers).

**4.4 Migrated product-notes + problem-part tooltips to `#shared-rich-tooltip`**
- Product name: `data-rich-tip` carries product name + "(Ctrl/Cmd + Click) to copy" hint + (if has notes) the notes block + "SKU: X - Right-click to copy" footer.
- Problem-part icon: `data-rich-tip` carries problem reason + notes + "Click to edit" hint.
- Removed per-item `<div class="product-notes-tooltip">` (was rendered for every item, ~600 chars even when there were no notes).
- `showProductNotes` / `hideProductNotes` stubbed to no-ops.

**4.5 Migrated assignment-chip tooltips to `#shared-rich-tooltip` + killed enhance polling**
- Was the worst offender: per-chip body-appended tooltip + `setInterval(enhanceAssignmentTooltips, 2000)` + `MutationObserver(document.body)` + 4 attached event handlers per chip.
- Now: chips ship with `data-rich-tip="<strong>Assigned by NAME</strong><br>DATE"` directly from `order-assignments.blade.php`.
- For real-time chip insertion (when someone else assigns you to an order), the JS-creation paths in `AppServiceProvider.php` (lines 8147, 8202, 8351) also set `data-rich-tip` instead of `data-tooltip`.
- Removed `setInterval(enhanceAssignmentTooltips, 2000)`, removed the MutationObserver on `.fi-ta-table`, removed the initial `setTimeout(enhance, 3000)`.
- Real-time chip insertion still works fully — only the tooltip-creation step is replaced with the shared singleton.

### Round 5 — Supplier badges + per-item buttons

**5.1 OOS supplier badges → `<template>` (lazy instantiation)**
- Was: every item's OOS supplier badges rendered with `display: none` in HTML — 600 chars × N OOS badges of layout/style/paint cost per render.
- Now: wrapped in `<template id="oos-tpl-{itemId}">`. Browser parses but doesn't instantiate; template content is invisible to `querySelectorAll('*')` and triggers no layout.
- `toggleOOSSuppliers()` clones template content on first "Show OOS" click, then runs the existing fade-in animation.
- File: `app/Providers/AppServiceProvider.php`
- Result: ~600–1,500 fewer live DOM nodes per render.

**5.2 Supplier badge inline styles → `.supplier-price-badge` class**
- Per-badge `style="..."` was ~250 chars × N badges. Moved to single CSS class.

**5.3 Supplier hover handlers → delegation**
- Per-badge `onmouseenter="showSupplierTooltip(...)" onmouseleave="hideSupplierTooltip(...)"` (~180 chars × N badges) replaced with single delegated `mouseover`/`mouseout` on document.
- Both badge ID and tooltip ID are derivable from the badge's existing `id` attribute.

**5.4 CSS-based badge dimming**
- Was: on every hover, JS iterated all badges in the same `.supplier-prices-inline` and set `opacity: 0.3` via `requestAnimationFrame` (then restored on mouseout via timer).
- Now: pure CSS `:hover > *:not(:hover) { opacity: 0.3 }`. Zero JS work per hover.

**5.5 Per-item button inline styles → `.item-action-btn` / `.log-costs-btn` classes**
- Update Supplier + Record Costs buttons.

### Round 6 — Console spam + scrubber throttle

**6.1 Silenced per-render console.log spam**
- Commented out 58 `console.log('[CLEANUP] ...')`, `'[Tooltips] ...'`, `'[DEBUG] ...'`, `'[SKIP] ...'`, `'[DISABLED] ...'`, `'[PERIODIC CLEANUP] ...'`, `'[IntervalManager] ...'`, `'[autoFulfillItem] ...'` calls.
- Removed `console.log('[captureStatusBadgeTemplates] Skipping order item badge')` that fired per item per render.
- Saved ~200–400 ms client time per render. Console is quiet now.

**6.2 Throttled `disableFilamentTooltips`**
- Was: `setInterval(disableFilamentTooltips, 100)` + `MutationObserver(document.body, { childList:true, subtree:true })` — burning 5–10% CPU even when idle.
- Now: runs on `livewire:updated` + `livewire:navigated` events + 5-second safety net `setInterval`. Filament tooltips are added at render time, not continuously, so this is sufficient.
- Removed the body MutationObserver entirely.

### Round 7 — Cleanup script audit

**7.1 Trimmed obsolete tooltip selectors from cleanup script**
- The post-Livewire-update cleanup script scanned for `[id^="supplier-tooltip-"]`, `[id^="tracking-tooltip-"]`, `[id^="partslink-tooltip-"]`, `[id^="stock-tooltip-"]`, `.copy-tooltip`, `.tracking-tooltip` — all obsolete since we use shared singletons now.
- Removed those 6 selectors. Kept defensive selectors for `.assignment-tooltip`, `.radioactive-tooltip`, `.problem-part-tooltip` (fire never, but safe).
- Removed the entire `product-notes-tooltip` orphan-cleanup block (we no longer create those at all).

### Round 8 — Real-time polish

**8.1 Pop-out animation on remote-driven chip removal**
- When polling detects another user's assignment change, badges now play `assignmentBadgePopOut 0.25s ease-in` before being removed (was instant `badge.remove()`). Pop-IN animation already existed on insertion.

**8.2 Faster polling for real-time updates**
- `pollOrderItems` interval: `5000ms → 2500ms`. Halved worst-case delay for real-time chip updates (someone else assigning you to an order).

---

## What was NOT changed (intentionally left alone)

- **24 filter widgets above the table** — flagged for last in the optimization order. User decided not worth pursuing given how close we got to the perceived speed of the test page.
- **70 `window.*` modal builder closures (`openInvoiceModal`, `cancelOrder`, `showRefundModal`, etc.)** — ~10 MB heap pressure, but lazy-loading them would be a large refactor for marginal benefit. Skipped per recommendation.
- **`Order::where('order_id', $dupId)->first()` lookup at order-assignments.blade line 98** — per-duplicate lookup. Rare (most orders have no duplicates) and changing it would alter what's displayed (external vs internal ID). Left alone.
- **`captureStatusBadgeTemplates`** function itself — only the console.log spam was removed; the underlying functionality (status badge template caching) still runs.

---

## SQL advisory (no changes made — read-only review)

### `count(*) from orders` — 59 ms per request
Uses smallest index (`is_full_delivered`, 1-byte) and full-scans 738K rows. Options if you want it faster:
- Cache the unfiltered total in Redis with a 60–300 s TTL.
- Use Filament's "simple" pagination (no count) when no filters are active.
- Use approximate `information_schema.tables.TABLE_ROWS` for the unfiltered case (off by single-digit %, takes < 1 ms).

This alone would shave 60 ms off every request.

### `idx_orders_date_placed`
Already used by the default sort. Healthy.

### `orders` table stats
738K rows, 271 MB data, 377 MB indexes. Reasonable.

---

## Files modified during this work

| File | What changed |
|---|---|
| `app/Filament/Resources/OrderResource.php` | Customer / address / order_total / actions columns rewritten with classes + shared tooltips + lazy dropdowns; class constants for US_STATES |
| `app/Providers/AppServiceProvider.php` | Shared tooltip modules; hover-to-open dropdowns; lazy OOS supplier `<template>`; supplier badge delegation; CSS dimming; assignment-chip data-rich-tip; real-time pop-out animations; throttled disableFilamentTooltips; console.log spam silenced; cleanup script audit |
| `app/Filament/Resources/OrderResource/Pages/ListOrders.php` | Original instrumentation removed (was added/removed during measurement) |
| `resources/views/filament/resources/order-resource/pages/list-orders.blade.php` | `toggleDropdown()` now lazy-builds items from `__DROPDOWN_TEMPLATES` |
| `resources/views/filament/tables/columns/order-assignments.blade.php` | Removed `$record->load()` N+1; added graceful lazy-load fallback for non-orders pages; replaced `title=` with `data-rich-tip` for assignment chips + duplicate badges |
| `public/css/custom.css` | All new classes for action buttons, dropdowns, supplier badges, item buttons, caret triangle, hover highlight |

---

## Test page (removed 2026-05-03)

A scratch test page lived at `/orders-test` (and a JS-attribution variant at `/orders-test-2`) during the investigation. They were used to:
- Establish a baseline render with stripped features
- Measure attribution per added feature
- Inject portions of the live page's inline JS to find the slow blocks

Both pages and their support files (Filament page classes, blade views, inject directory, beacon route) were removed once the optimizations were validated against live.

---

## Pre-existing abandoned scaffold (still on disk — flagged for future cleanup)

These files predate this work (Feb 20) and were noted as "abandoned attempts" at the start. Not removed yet:
- `app/Filament/Resources/OrderResource/Pages/ListOrdersTest.php`
- `resources/views/filament/resources/order-resource/pages/list-orders-test.blade.php`
- One line in `OrderResource.php`: `'orders-test' => Pages\ListOrdersTest::route('/orders-test')` (registers a stale URL at `/orders/orders-test` under the OrderResource prefix; harmless but unused).
