Global search palette
A single search surface that sits above every screen and answers "find me anything in the shop" in one keystroke. Opens as an overlay modal (Cmd-K / Ctrl-K), takes a free-text query, returns three groups of typed results (customers, inventory, service tickets).
The palette UI shipped 2026-05-14 backed by plain LIKE queries. The next day (2026-05-15) the backend swapped to the new SQLite FTS5 search engine — same endpoint shape (/api/search), same grouped result shape, dramatically better ranking + synonym handling. The palette UI didn't change.
Focus stewardship rules still apply — the palette input becomes the steward while open; on close, focus returns to the screen's resident steward.
The promise
Operator hits Ctrl+K from any screen → overlay opens → types "jane" or "T-1234" or "chainring" or scans a barcode → up to three groups of results (customers, inventory, service tickets) appear → arrow keys + Enter, or click, to navigate to the result.
It's the universal "I know what I want, I don't know where to find it" surface. Replaces three different per-screen searches with one across-the-board search.
Endpoint
GET /api/search?q=<query>&types=customers,inventory,tickets&limit=10&hint=<type>
| Param | Required | What it does |
|---|---|---|
q | yes | The query string. Trimmed; empty returns {ok: true, query: '', groups: []}. |
types | no | CSV of customers / inventory / tickets. Defaults to all three. Unknown types are dropped. |
limit | no | Per-group cap, clamped to 1..50, default 10. |
hint | no | Optional bias: e.g., the screen the operator is on. Reranks the result groups but doesn't filter. |
Auth: device (Layer 1 OAuth) is required — getActiveDeviceSession() is checked first. Anonymous → 401.
Response shape:
{
"ok": true,
"query": "jane",
"groups": [
{ "type": "customers", "count": 3, "results": [...] },
{ "type": "inventory", "count": 1, "results": [...] },
{ "type": "tickets", "count": 0, "results": [...] }
]
}
The 3 group helpers (searchCustomersForPalette, searchInventoryForPalette, searchTicketsForPalette) run in parallel via Promise.all. Each is a single D1 query with LIKE matching against the screen's pre-existing search fields (e.g., for tickets: ticket number, customer name, bike serial, issue text).
Smart-query heuristics
The palette is permissive about input format:
T-1234or1234for a ticket number routes to tickets with a numeric hint#NNNNfor an account number routes to customers- A barcode-shaped string (when a USB scanner fires into the input) routes to inventory by barcode
- Otherwise it splits the query and matches across name / brand / sku / barcode (inventory), name / phone / email / account (customers), and ticket-number / issue-text (tickets)
The smart-routing lives in each helper, not in apiSearch itself — keeps the helpers single-responsibility.
UI surface
#helm-palette lives at the document level (outside any screen). Hidden by default; toggled by:
- Keyboard:
Ctrl+K(Windows/Linux) orCmd+K(macOS) - Topnav search icon: future affordance
When opened, the input receives focus. While open, the palette is the registered focus steward — Esc / click outside / pressing Enter on a result restores focus to the previous screen's resident steward (per focus stewardship).
Result groups render as labeled sections with type icons. Arrow keys navigate; Enter activates. Each result is a button with a target URL — clicking dispatches the standard screen navigation.
Why this is its own thing
The palette is NOT a Layer-3 setting and NOT a per-screen search. It's a third surface — a global lookup affordance that exists outside the screen layout. Three reasons to keep it separate:
- Different focus contract. A per-screen steward (
#customers-search,#sales-search,#inventory-search) owns the screen's focus. The palette steals focus while open and returns it on close. Conflating them would muddle the focus-stewardship rules. - Different result shape. Per-screen searches return rows scoped to that screen's domain. The palette returns typed groups across domains so the operator doesn't have to know which screen the thing they want lives on.
- Different latency budget. The palette fans out three queries in parallel and renders all groups at once. Per-screen searches are tighter, single-domain, lower-latency.
What it doesn't do
- Doesn't index — every query is a fresh
LIKEagainst D1. Fine at Swicked scale; revisit if any single shop's record count gets so large that a single query takes >200ms. - Doesn't support advanced filters — no
customer:jane AND status:open. The query is plain text. If a power-user surface is wanted later, it gets its own affordance. - Doesn't bridge to AI — that's slice 11's job. The palette is structured search; the AI bubble is conversational. Different tools.
See also
- Focus stewardship — the rules the palette respects on open/close
- helm-editable doctrine — the in-situ edit chassis; the palette is the structured-search complement to in-situ editing
- Slice 4 — Service Tickets — per-screen ticket search this complements
- AI integration — the AI bubble, the conversational search surface that complements this structured one