openvidu/openvidu-dashboard/openspec/specs/ingress-ui/spec.md

344 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# ingress-ui Specification
## Purpose
The Ingress page: the list of ingress definitions with summary tiles, filters, search and live refresh; the
Ingress detail page is locked in CE.
## Requirements
### Requirement: Ingress page URL state
Edition: CE. `/ingress` SHALL read its whole state from the query parameters `status`, `inputType`, `q`, `from`, `to`,
`tz`, `sort`, `order`, `cursor` and `limit` as the shell's URL-state rules describe; `sort` accepts `createdAt`,
`totalDuration`, `sessions` and `lastSession`; invalid values fall back silently, and so does a metric sort with a range
wider than the ranked-sort limit, which falls back to `createdAt`.
#### Scenario: Deep link reproduces the view
- **GIVEN** `/ingress?status=publishing&inputType=rtmp&q=cam&from=now-7d&to=now&tz=UTC&order=asc&limit=50`
- **WHEN** it is opened in a new browser session
- **THEN** the search box reads `cam`, the input select shows RTMP, the status select shows Publishing, the range
picker shows the 7-day preset in UTC, the table is sorted by Created ascending and shows 50 rows
#### Scenario: Metric sort deep link
- **GIVEN** `/ingress?sort=lastSession&from=now-24h&to=now`
- **WHEN** it is opened
- **THEN** the table is sorted by Last session descending and the Last session header shows the sort
#### Scenario: Metric sort beyond the limit
- **GIVEN** `/ingress?sort=sessions&from=now-30d&to=now` and the default 7-day limit
- **WHEN** it is opened
- **THEN** the table is sorted by Created descending, the Sessions header is disabled with its tooltip and no list
request carries `sort=sessions`
#### Scenario: Filter change drops the cursor
- **GIVEN** the list on its third page
- **WHEN** the user selects the WHIP input type
- **THEN** the URL has `inputType=whip`, no `cursor`, and Back returns to the third page
#### Scenario: Defaults
- **WHEN** `/ingress` opens without query parameters
- **THEN** it uses `status=all`, every input type, `sort=createdAt`, `order=desc`, the global range and the stored page
size
#### Scenario: Any state change drops the cursor
- **GIVEN** `/ingress` with a `cursor` in the URL
- **WHEN** a filter, the search, the range, the sort or the page size changes
- **THEN** `cursor` is dropped and a history entry is created
### Requirement: Ingress summary tiles
Edition: CE. The Ingress page SHALL show four KPI tiles from the summary endpoint above the filters: Publishing now,
Ingress in range (with the input type breakdown as hint), Sessions in range and Total duration; approximate values
carry a tooltip saying the numbers are hour-aligned; tiles keep their value while the list reloads or fails.
#### Scenario: Tiles of a range
- **GIVEN** the summary answers `{ publishingNow: 2, ingressesInRange: 5, byInputType: { rtmp: 3, whip: 2, url: 0 },
sessionsInRange: 12, totalDurationMs: 21600000, approximate: true }`
- **WHEN** the tiles render
- **THEN** they read "Publishing now 2", "Ingress in range 5" with hint "RTMP 3 · WHIP 2 · URL 0", "Sessions in
range 12" and "Total duration 6 h 00 m", the last two with the approximate tooltip, and no tile reads "Ingresses"
#### Scenario: Failed list keeps the tiles
- **GIVEN** tiles already rendered
- **WHEN** the list request fails
- **THEN** the tiles keep their values and the error banner appears over the table only
### Requirement: Status and input type filters
Edition: CE. The Ingress page SHALL offer, after the search box and in this order, an input type select (All, RTMP,
WHIP, URL) bound to `inputType` and a status select (All, Publishing, Buffering, Idle, Error, Complete, Deleted)
bound to `status`; both write the URL on change and show their current value from the URL.
#### Scenario: Filter order
- **WHEN** the Ingress page renders
- **THEN** the filter bar reads, from the start, the search box labelled "Ingress name or ID", Input and Status
#### Scenario: Status select writes the URL
- **WHEN** the user selects Idle
- **THEN** the URL has `status=inactive` and the table shows idle ingresses only
#### Scenario: Labels map to statuses
- **GIVEN** `?status=inactive`
- **WHEN** the page renders
- **THEN** the status select shows Idle
### Requirement: Ingress search
Edition: CE. The Ingress page SHALL offer the shared list search box labelled "Ingress name or ID" for an ingress name
prefix or an `IN_` ID, which writes `q` after a 300 ms debounce, shows the hint "Type at least 2 characters" instead
of searching on one character, shows the hint "Exact ID: range and filters ignored · Enter opens it" on an exact
`IN_` ID, and opens that ingress with the current range when Enter is pressed on it.
#### Scenario: Prefix search
- **WHEN** the user types `ca`
- **THEN** after 300 ms the URL has `q=ca`, no `cursor`, and the table shows the matching ingresses
#### Scenario: Exact id
- **GIVEN** `?status=publishing`
- **WHEN** the user types `IN_ab12cd`
- **THEN** the URL has `q=IN_ab12cd`, the table shows that single ingress whatever its status and the hint reads
"Exact ID: range and filters ignored · Enter opens it"
#### Scenario: Enter on an exact id
- **GIVEN** the search box holds `IN_ab12cd`
- **WHEN** the user presses Enter
- **THEN** `/ingress/IN_ab12cd` opens with the current `from`, `to` and `tz`
#### Scenario: One character
- **WHEN** the user types `c`
- **THEN** no request is sent and the hint reads "Type at least 2 characters"
### Requirement: Ingress table
Edition: CE. The Ingress table SHALL render, in this order, the columns Ingress ID, Name, Room, Input, Status, Created
(sortable, default desc), Total duration (sortable), Sessions (sortable) and Last session (sortable); Ingress ID, Status
and Created cannot be hidden, Created and Last session carry the time-zone header hint, and narrow widths use the card
layout.
#### Scenario: Publishing row
- **GIVEN** a reusable RTMP ingress `cam-1` publishing into `demo-room` with 3 sessions
- **WHEN** the row renders
- **THEN** it shows, in order, `IN_c3d4` once, as a link to `/ingress/IN_c3d4` with its copy button, `cam-1` with its
copy button, `demo-room` linking to `/rooms?q=demo-room`, the "RTMP" badge, the publishing badge without a live
chip, the creation time, the total duration counting up every second, `3` and the last session time
#### Scenario: Deleted row
- **GIVEN** an ingress with `status: "deleted"`
- **WHEN** the row renders
- **THEN** the status cell shows the neutral "deleted" status badge with the deletion time in its tooltip
#### Scenario: Idle ingress with an error
- **GIVEN** an ingress with `status: "inactive"` and `hasError: true`
- **WHEN** the row renders
- **THEN** the status cell shows the "idle" badge in the neutral colour with the error icon and the tooltip "Last
session ended with an error"
#### Scenario: Disabled ingress
- **GIVEN** an idle ingress with `enabled: false`
- **WHEN** the row renders
- **THEN** the status cell shows the "idle" badge followed by a "disabled" badge in the same badge style, not plain
text
#### Scenario: Never used
- **GIVEN** an ingress with `sessions: 0` and no `lastSessionAt`
- **WHEN** the row renders
- **THEN** Sessions reads `0`, Total duration reads `—` and Last session reads `—`
#### Scenario: Columns
- **WHEN** the Ingress table renders a row
- **THEN** Ingress ID links to the detail with a copy button, Name has a copy button ("—" without a name), Room links to
`/rooms?q=<roomName>` carrying the range, Input shows the kind badge, Status shows the status badge, idle in the
neutral colour and deleted with the deletion time in its tooltip, followed by the error icon when `hasError` and a
"disabled" badge when not enabled, and Total duration counts up live while a session runs
### Requirement: Ingress pagination
Edition: CE. The Ingress page SHALL use the cursor paginator with Prev/Next bound to `prevCursor`/`nextCursor`, the page
size select bound to `limit` (10, 25, 50, 100) and "Showing x–y of total" when `total` is present, and hide the
paginator while the first page is empty.
#### Scenario: Next page
- **GIVEN** the first page of 25 with `nextCursor` set
- **WHEN** the user clicks Next
- **THEN** the URL has that `cursor`, the table shows rows 26-50 and the paginator reads "Showing 26–50 of 60"
#### Scenario: Long range without total
- **GIVEN** `?from=now-30d`
- **WHEN** the page renders
- **THEN** the paginator reads "Showing 1–25" without a total
#### Scenario: Empty first page
- **GIVEN** a filter that matches no ingress
- **WHEN** the list answers no items
- **THEN** the empty state is shown and no paginator is rendered under it
### Requirement: Ingress live refresh
Edition: CE. The Ingress page SHALL be a live view following the shell's auto-refresh policy (default 10 s): each tick
reloads the summary and the current page in place; publishing rows are told by their status badge, without a live
chip; refresh pauses while the tab is hidden and skips ticks while a request is in flight.
#### Scenario: In-place refresh
- **GIVEN** the list showing 25 ingresses with the scroll at row 20
- **WHEN** a refresh completes with a session count changed on one row
- **THEN** the rows update without skeletons, the scroll position is kept and no row flashes
#### Scenario: Session ends during refresh
- **GIVEN** a row with the publishing badge
- **WHEN** a refresh returns that ingress as `inactive`
- **THEN** the status badge reads "idle" in the neutral colour and its Total duration stops counting up
### Requirement: Ingress page states
Edition: CE. The Ingress page SHALL show skeleton rows equal to the page size only until the first response, keep the
rows or the empty state on screen during a refresh, show the shared list empty state "No ingress match" when no row
matches, "Ingress <ID> was not found" with "Clear search" when an exact ingress ID finds nothing, and the error banner
with retry and request id while keeping the previous rows when a load fails.
#### Scenario: Empty filtered list
- **GIVEN** `?status=error&q=zzz`
- **WHEN** the list answers no items
- **THEN** the empty state reads "No ingress match" and offers "Clear filters" (removes `status`, `inputType` and
`q`) and "Widen range" (opens the range picker)
#### Scenario: Unknown ingress id
- **GIVEN** the user searches `IN_missing`
- **WHEN** the list answers no items
- **THEN** the page reads "Ingress IN_missing was not found" with "Clear search", which empties the search box and
drops `q`
#### Scenario: Failed refresh
- **GIVEN** rows from the previous load
- **WHEN** the next request fails with `500`
- **THEN** the rows stay visible under the error banner showing the `X-Request-Id` and a Retry action
#### Scenario: No match hint and actions
- **WHEN** no row matches
- **THEN** the shared list empty state "No ingress match" shows the hint "Change the filters or widen the time range."
and the actions "Clear filters" and "Widen range"
### Requirement: Ingress detail route and CE lock
Edition: CE. `/ingress/:ingressId` SHALL be registered in CE with `requiredFeature: "ingress-detail"`, read `tz`,
`order`, `cursor`, `limit` and `types` from the URL, and in CE render the full-page lock over a dimmed silhouette of
three tiles and one block, under the breadcrumbs and a header holding only the title (the name, or the ID without one)
with its copy button and the status badge; the header has no refresh control and no PRO endpoint is called.
#### Scenario: Locked from the list
- **GIVEN** CE and the user follows the `IN_c3d4` link from the list
- **WHEN** the page renders
- **THEN** the breadcrumbs read `Ingress / IN_c3d4`, the title reads `cam-1` with its copy button followed by the
status badge from the list data, no input badge, facts or refresh control are shown, the lock card reads "Ingress
detail is available in OpenVidu PRO" with "Learn about OpenVidu PRO" and "Back to ingress", and no request to
`/api/v1/ingresses/IN_c3d4` is sent
#### Scenario: Locked deep link
- **GIVEN** CE and `/ingress/IN_c3d4` opened directly
- **WHEN** the page renders
- **THEN** the header is filled from `GET /api/v1/ingresses?q=IN_c3d4` and the lock card is shown
#### Scenario: Ingress without name
- **GIVEN** CE and an ingress `IN_c3d4` without name
- **WHEN** the locked page renders
- **THEN** the title reads `IN_c3d4` with its copy button
#### Scenario: Header data source
- **GIVEN** CE and the locked page of `IN_c3d4`
- **WHEN** its header renders
- **THEN** the header is filled from the list cache or, when the cache has no entry, from
`GET /api/v1/ingresses?q=IN_c3d4`
### Requirement: Ingress range by activity
Edition: CE. With a range, the Ingress list SHALL hold the ingress created in it, the ingress whose last session
started in it, and the ingress publishing now that were created before its end, so an ingress created long before
the range that streamed inside it is listed.
#### Scenario: Old ingress used in the range
- **GIVEN** the last 24 h, ingress `cam-1` created 10 days ago whose last session started 2 hours ago and has ended,
ingress `cam-2` created 30 days ago and publishing since 3 days ago, and ingress `cam-3` created 5 days ago and
unused since
- **WHEN** the list loads
- **THEN** it holds `cam-1` and `cam-2`, and not `cam-3`
#### Scenario: Ingress created in the range
- **GIVEN** the last 24 h and an ingress created an hour ago that never published
- **WHEN** the list loads
- **THEN** the ingress is listed with Sessions `0`
### Requirement: Ingress sort keys
Edition: CE. The ingress table SHALL sort by Created (`sort=createdAt`, the default, newest first, for any range),
Total duration (`sort=totalDuration`, the running session included), Sessions (`sort=sessions`) or Last session
(`sort=lastSession`), in the `order` the header toggles; the three metric sorts rank the ingress of the range with
their values at the first page's request time, which the cursor keeps for the following pages.
#### Scenario: Sort by total duration
- **GIVEN** the Ingress page on the default 24 h range with a publishing ingress
- **WHEN** the user activates the Total duration header
- **THEN** the URL has `sort=totalDuration` and `order=desc` without `cursor`, and the publishing ingress is placed by
its stored total plus the time its running session has been publishing
#### Scenario: Next page keeps the ranking
- **GIVEN** the list sorted by Total duration with a publishing ingress on the first page
- **WHEN** the user presses Next a minute later
- **THEN** the second page continues the ranking of the first page and no row of the first page reappears
#### Scenario: Created on a long range
- **GIVEN** `from=now-90d&to=now`
- **WHEN** the user toggles the Created header
- **THEN** the list is sorted by Created ascending, whatever the length of the range
### Requirement: Ingress ranked sort limit
Edition: CE. While the resolved range is wider than the ranked-sort limit of `GET /info`
(`limits.rankedSortMaxRangeMs`, 7 days by default), the Total duration, Sessions and Last session headers, and their
options in the sort select of the card layout, SHALL be disabled with the tooltip "Narrow the range to <n> to
sort by <column>"; Created SHALL stay sortable for any range.
#### Scenario: Thirty-day range
- **GIVEN** `from=now-30d&to=now` and the default limit
- **WHEN** the user hovers or focuses the Sessions header
- **THEN** the header is disabled and its tooltip reads "Narrow the range to <n> to sort by Sessions", with <n>
naming the 7-day limit, while the Created header still toggles the order