Skip to content

Commit 449a1ae

Browse files
committed
feat(ui): dashboard URL search deep links and docs
Add query-parameter deep links for SIP filters (from_user, call_id, etc.), legacy homer-app JSON parsing, copy-link in Protocol Search, and SEARCH_URL.md with per-protocol examples and extension guide.
1 parent 9047b3d commit 449a1ae

8 files changed

Lines changed: 773 additions & 1 deletion

File tree

docs/SEARCH.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Example request body:
3838

3939
For full setup and configuration details, see [MCP UI Guide](MCP_UI_GUIDE.md).
4040

41+
## Dashboard URL search
42+
43+
Bookmarkable dashboard links (`?from_user=…#dashboard`), per-protocol examples, and how to add new protocols: **[SEARCH_URL.md](SEARCH_URL.md)**.
44+
4145
## Authentication
4246

4347
On first login, the JWT token is cached in `~/.homer_token` for subsequent requests:

docs/SEARCH_URL.md

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# Dashboard URL search
2+
3+
Homer 11 can open the dashboard with **prefilled search filters** and run a query automatically. This replaces the homer-app Share URL pattern ([homer-app#607](https://github.com/sipcapture/homer-app/issues/607)) with simple query parameters.
4+
5+
Related docs:
6+
7+
- [Search CLI](SEARCH.md) — terminal search and `--proto` names
8+
- [Mapping examples](../examples/mappings/README.md)`fields_mapping` JSON per protocol
9+
- API: `POST /api/v4/transactions/search` — same `filter` object the UI sends
10+
11+
---
12+
13+
## URL shape
14+
15+
Use the dashboard hash and put filters in the query string.
16+
17+
**Recommended** — query **before** the hash:
18+
19+
```text
20+
https://coordinator.example/?from_user=123123&minutes=10#dashboard
21+
```
22+
23+
**Also supported** — query after the hash:
24+
25+
```text
26+
https://coordinator.example/#dashboard?from_user=123123&minutes=10
27+
```
28+
29+
On load the UI:
30+
31+
1. Switches to the dashboard (you must already be logged in, or complete OAuth/login first).
32+
2. Applies the time window and filter fields to the Protocol Search widget (if present).
33+
3. Runs search on the first **Results** or **Time Chart** widget.
34+
4. Strips search parameters from the address bar (filters stay in the widget).
35+
36+
Copy a link from the UI: Protocol Search widget → link icon next to **Clear**.
37+
38+
Implementation: `src/ui/src/dashboard/searchDeepLink.ts`, `SearchDeepLinkBootstrap.tsx`.
39+
40+
---
41+
42+
## Query parameters
43+
44+
### Time range
45+
46+
| Parameter | Description |
47+
|-----------|-------------|
48+
| `minutes` or `m` | Relative window ending **now** (default **60** when only filters are set) |
49+
| `from`, `to` | Absolute range, Unix epoch **milliseconds** |
50+
51+
Example — last 15 minutes:
52+
53+
```text
54+
/?from_user=alice&m=15#dashboard
55+
```
56+
57+
Example — fixed window (same as homer-app issue):
58+
59+
```text
60+
/?from_user=123123&from=1743857605187&to=1743858205187#dashboard
61+
```
62+
63+
### Protocol selection
64+
65+
| Parameter | Alias | Description |
66+
|-----------|-------|-------------|
67+
| `proto_type` | `proto` | HEP / virtual protocol id (see table below) |
68+
| `event_type` | `event` | Mapping **profile** name (`call`, `registration`, `default`, …) |
69+
70+
Defaults if omitted: `proto_type=1`, `event_type=call` (SIP calls).
71+
72+
### SIP-oriented filters (built into URL parser)
73+
74+
At least one of these must be present or the deep link is ignored.
75+
76+
| Parameter | Aliases | Maps to API `filter` |
77+
|-----------|---------|----------------------|
78+
| `from_user` | `user_from`, `caller` | `from_user` |
79+
| `to_user` | `user_to`, `callee` | `to_user` |
80+
| `call_id` | `callid` | `call_id` |
81+
| `method` || `method` |
82+
| `src_ip` || `src_ip` |
83+
| `dst_ip` || `dst_ip` |
84+
| `limit` || `param.limit` (default 50) |
85+
86+
Example — SIP From user, registration profile:
87+
88+
```text
89+
/?proto_type=1&event_type=registration&from_user=1000&minutes=30#dashboard
90+
```
91+
92+
---
93+
94+
## Built-in protocols (`proto_type` + `event_type`)
95+
96+
These match seeded `mapping_schema` rows and the Search CLI `--proto` table in [SEARCH.md](SEARCH.md).
97+
98+
| Protocol | `proto_type` | Typical `event_type` | URL example (extra params) |
99+
|----------|--------------|----------------------|----------------------------|
100+
| SIP calls | `1` | `call` | `from_user`, `to_user`, `call_id`, `method` |
101+
| SIP (generic) | `1` | `default` | same |
102+
| SIP registration | `1` | `registration` | `from_user` (AOR/contact in UI) |
103+
| RTCP | `5` | `default` | `src_ip`, `dst_ip`, `call_id` (payload search) |
104+
| DNS | `53` | `default` | `src_ip`, `dst_ip`, `call_id` |
105+
| LOG | `100` | `default` | `call_id` (message text search) |
106+
| OTLP traces | `200` | `default` | set `proto_type=200`; use UI or API for `trace_id` (see below) |
107+
| OTLP metrics | `201` | `default` | `proto_type=201` |
108+
| OTLP logs | `202` | `default` | `proto_type=202` |
109+
| RTP agent | `34` | `default` | CLI/API; URL uses shared IP / `call_id` fields |
110+
| Line Protocol | `300` | `schema__table` | e.g. `event_type=main__cpu` (see LP section) |
111+
112+
Examples:
113+
114+
```text
115+
# RTCP on a subnet
116+
/?proto_type=5&event_type=default&src_ip=10.0.1.10&minutes=60#dashboard
117+
118+
# DNS
119+
/?proto_type=53&dst_ip=8.8.8.8&m=120#dashboard
120+
121+
# OTLP traces (protocol only; refine in widget or API)
122+
/?proto_type=200&event_type=default&minutes=15#dashboard
123+
124+
# SIP registration
125+
/?proto_type=1&event_type=registration&from_user=sip:user@domain.com#dashboard
126+
```
127+
128+
### Line Protocol (`proto_type=300`)
129+
130+
LP rows are created when measurements are ingested (`lp_mapping_sync`). The profile name is **`schema__table`**, for example `main__cpu`.
131+
132+
```text
133+
/?proto_type=300&event_type=main__cpu&minutes=10#dashboard
134+
```
135+
136+
Field filters for LP in the URL are not wired yet; use the Protocol Search widget after the link opens, or the CLI with `--proto lp --event-type main__cpu`.
137+
138+
---
139+
140+
## Legacy homer-app JSON URLs
141+
142+
homer-app used a single JSON blob as the query string:
143+
144+
```text
145+
/search/result?{"timestamp":{"from":...,"to":...},"param":{"search":{"1_call":{"user_from":"123123"}}}}
146+
```
147+
148+
Homer 11 accepts the same JSON when it starts with `{` (before or as `?q=...`). Known mappings:
149+
150+
- `param.search.<profile>.user_from``from_user`
151+
- `param.search.<profile>.user_to` / `callee``to_user`
152+
- `timestamp.from` / `timestamp.to` → time range
153+
154+
Prefer the flat query parameters above for new integrations.
155+
156+
---
157+
158+
## API payload (what the URL becomes)
159+
160+
Deep links end up as `POST /api/v4/transactions/search`:
161+
162+
```json
163+
{
164+
"filter": {
165+
"proto_type": 1,
166+
"event_type": "call",
167+
"from_user": "123123"
168+
},
169+
"param": { "limit": 50 },
170+
"timestamp": { "from": 1743857605187, "to": 1743858205187 }
171+
}
172+
```
173+
174+
Field names in `filter` must match the mapping field **`id`** values (see `examples/mappings/fields_*.json`) and what `buildSearchSQLV4` understands in `transactions_v4.go`.
175+
176+
---
177+
178+
## Adding a new protocol (data + UI)
179+
180+
### 1. Ingest and storage
181+
182+
Ensure the node writes the protocol into DuckLake (HEP type or custom table). For HEP types, table suffixes follow `hep_proto_<id>_<profile>` (see [Storage layout](STORAGE_LAYOUT.md)).
183+
184+
### 2. `mapping_schema` row
185+
186+
Add a row so the Protocol Search widget can list fields and the coordinator can build SQL:
187+
188+
1. Copy a seed from [`examples/mappings/`](../examples/mappings/) or create `fields_<hepid>_<profile>.json`.
189+
2. Each searchable column needs an `"id"` (filter key), `"name"`, `"form_type"`, etc.
190+
3. Insert via Settings → Mappings UI, or SQL/API into settings DB `mapping_schema`:
191+
- `hepid` — same integer as `proto_type` in search
192+
- `profile` — same string as `event_type` in search
193+
- `hep_alias` — display label
194+
- `fields_mapping` — JSON array from your file
195+
196+
See [examples/mappings/README.md](../examples/mappings/README.md).
197+
198+
### 3. Backend filter support
199+
200+
In `src/coordinator/handlers/transactions_v4.go`, `buildSearchSQLV4` must translate your field ids into SQL for that table. SIP/RTCP/DNS/LOG/OTLP paths already exist; a **new** HEP type may need a new branch or generic column mapping.
201+
202+
### 4. Dashboard widget
203+
204+
Operators add a **Protocol Search** widget, pick the mapping (hepid + profile) in widget settings, and a **Results** widget. Presets can be registered in `src/ui/src/dashboard/widgets/registry.ts` (`search` extras) and `SearchPanel.tsx` (`SIP_PRESETS`, `OTLP_PRESETS`, …).
205+
206+
---
207+
208+
## Extending URL parameters for more fields
209+
210+
Today the URL parser only exposes a **fixed set** of query keys (SIP-friendly names + `proto_type` / `event_type`). Protocol-specific ids such as `trace_id`, `service_name`, or `aor` are **not** read from the URL unless you extend the code.
211+
212+
### Option A — Add named parameters (simple)
213+
214+
Edit `src/ui/src/dashboard/searchDeepLink.ts`:
215+
216+
1. Add the field to `SearchDeepLinkSpec` and `TRIGGER_KEYS` if it should alone trigger a search.
217+
2. Parse it in `parseSearchDeepLink()` from `URLSearchParams`.
218+
3. Copy it in `buildSearchPayload()` into `filter.<id>`.
219+
4. Mirror in `buildSearchDeepLinkURL()` and `deepLinkSpecToFormFields()` for the copy-link button.
220+
5. Add tests in `searchDeepLink.test.ts`.
221+
222+
Example — OTLP `trace_id`:
223+
224+
```typescript
225+
// parse
226+
spec.trace_id = p.get('trace_id')?.trim() || undefined
227+
// payload
228+
if (spec.trace_id) filter.trace_id = spec.trace_id
229+
```
230+
231+
### Option B — Generic `filter.<field>=<value>` (scalable)
232+
233+
Support repeated or prefixed params, e.g. `filter.trace_id=abc` or `f.trace_id=abc`, and merge any `filter.*` key into the API `filter` object after `proto_type` / `event_type` are set. This avoids editing the parser for every mapping field but requires careful validation (allowlist or mapping-driven ids only).
234+
235+
### Option C — Encoded JSON filter (power users)
236+
237+
A single param `filter={"proto_type":200,"trace_id":"..."}` URL-encoded. Same flexibility as homer-app but harder to hand-edit; good for tools generating links.
238+
239+
### Option D — No URL change
240+
241+
Generate links only through the UI copy button after configuring the widget (dynamic mode already serializes all visible fields into the stored form; extending copy-link to emit every configured field id is the smallest UX win).
242+
243+
---
244+
245+
## Requirements and limitations
246+
247+
| Topic | Detail |
248+
|-------|--------|
249+
| Authentication | User must have a valid session/JWT; URL does not embed credentials. |
250+
| Dashboard layout | At least one **results** or **chart** widget must exist on the active dashboard. |
251+
| Search widget | Optional; without it, filters still run but the form may not show values. |
252+
| Custom mappings | URL uses **field ids** from `fields_mapping`; aliases like `user_from` are SIP legacy only. |
253+
| Dynamic widget fields | Extra mapping fields only appear in the URL after Option A/B/D above. |
254+
| SQL / AI tabs | Deep links only drive structured **Form** search, not SQL or MCP tabs. |
255+
256+
---
257+
258+
## Troubleshooting
259+
260+
| Symptom | Check |
261+
|---------|--------|
262+
| Dashboard opens, no search | No `from_user` / `call_id` / … trigger param; add at least one filter key. |
263+
| All rows returned | Filter empty in API body — wrong param name or legacy JSON profile key (`1_call` vs `call`). |
264+
| Wrong protocol | Set `proto_type` and `event_type` to match the mapping row. |
265+
| No results widget | Add Results Table from the empty-state buttons in Protocol Search. |
266+
| homer-app link | Path `/search/result` is not used in Homer 11; use `/#dashboard` and coordinator host. |
267+
268+
---
269+
270+
## Quick reference
271+
272+
```text
273+
# SIP by From user, last hour
274+
/?from_user=123123&minutes=60#dashboard
275+
276+
# By Call-ID
277+
/?call_id=abc-def-ghi@host&from=1700000000000&to=1700003600000#dashboard
278+
279+
# RTCP
280+
/?proto_type=5&src_ip=10.0.0.1&m=30#dashboard
281+
282+
# OTLP traces (open correct protocol; add trace_id in UI or extend URL parser)
283+
/?proto_type=200&minutes=15#dashboard
284+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ nav:
7373
- Config wizard: WIZARD.md
7474
- Environment variables: ENVIRONMENT_VARIABLES.md
7575
- Search CLI: SEARCH.md
76+
- Dashboard URL search: SEARCH_URL.md
7677
- Certificates: CERTIFICATES_README.md
7778
- Architecture:
7879
- Overview: STORAGE_ARCHITECTURE.md

src/ui/src/dashboard/DashboardLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Input } from '@/components/ui/input'
1818
import { Separator } from '@/components/ui/separator'
1919
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
2020
import { cn } from '@/lib/utils'
21+
import SearchDeepLinkBootstrap from './SearchDeepLinkBootstrap'
2122

2223
function DashboardGrid() {
2324
const confirm = useConfirm()
@@ -510,6 +511,7 @@ export default function DashboardLayout({
510511
timeZone={timeZone}
511512
requestTimeRange={handleTimeChange}
512513
>
514+
<SearchDeepLinkBootstrap />
513515
<DashboardGrid />
514516
</DashboardProvider>
515517
</main>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useEffect, useRef } from 'react'
2+
import { useDashboard } from './context/DashboardContext'
3+
import { useSearchStore } from './stores/search-store'
4+
import {
5+
buildSearchPayload,
6+
deepLinkSpecToFormFields,
7+
getDeepLinkSearchParams,
8+
parseSearchDeepLink,
9+
resolveDeepLinkTimestamp,
10+
stripDeepLinkParamsFromURL,
11+
} from './searchDeepLink'
12+
13+
/**
14+
* On dashboard load, apply ?from_user=… (or homer-app legacy JSON) and run search
15+
* against the first results/chart widget.
16+
*/
17+
export default function SearchDeepLinkBootstrap() {
18+
const {
19+
widgets,
20+
loading,
21+
activeDashboardId,
22+
publishSearch,
23+
requestTimeRange,
24+
} = useDashboard()
25+
const specRef = useRef(parseSearchDeepLink(getDeepLinkSearchParams()))
26+
const appliedRef = useRef(false)
27+
28+
useEffect(() => {
29+
if (appliedRef.current || loading) return
30+
const spec = specRef.current
31+
if (!spec) return
32+
33+
const resultWidget = widgets.find((w) => w.type === 'results' || w.type === 'chart')
34+
if (!resultWidget) return
35+
36+
appliedRef.current = true
37+
38+
const searchWidget = widgets.find((w) => w.type === 'search')
39+
const storeKey = searchWidget
40+
? `${activeDashboardId || '_'}:${searchWidget.id}`
41+
: null
42+
43+
const ts = resolveDeepLinkTimestamp(spec)
44+
requestTimeRange(null, ts.from, ts.to)
45+
46+
const formFields = deepLinkSpecToFormFields(spec)
47+
if (storeKey) {
48+
const { setField, setLimit } = useSearchStore.getState()
49+
for (const [k, v] of Object.entries(formFields)) {
50+
setField(storeKey, k, v)
51+
}
52+
if (spec.limit != null) {
53+
setLimit(storeKey, spec.limit)
54+
}
55+
}
56+
57+
const payload = buildSearchPayload(spec, { from: ts.from, to: ts.to }, spec.limit ?? 50)
58+
publishSearch(resultWidget.id, payload)
59+
stripDeepLinkParamsFromURL()
60+
}, [widgets, loading, activeDashboardId, publishSearch, requestTimeRange])
61+
62+
return null
63+
}

0 commit comments

Comments
 (0)