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

335 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.

# egress-ui Specification
## Purpose
The Egress page: how egress data is laid out, filtered, paged, refreshed and linked, built from the shared UI
building blocks and driven by the URL state rules of the shell; the Egress detail page is locked in CE.
## Requirements
### Requirement: Egress page URL state
Edition: CE. `/egress` SHALL read its whole state from the query parameters `status`, `type`, `room`, `q`, `from`, `to`,
`tz`, `sort`, `order`, `cursor` and `limit` as the shell's URL-state rules describe; `sort` accepts `startedAt`,
`endedAt` and `duration`; invalid values fall back silently, and so does `endedAt` or `duration` with a range wider than
the ranked-sort limit, which falls back to `startedAt`.
#### Scenario: Deep link reproduces the view
- **GIVEN** `/egress?status=failed,aborted&type=room_composite&q=demo&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 `demo`, the type select shows Room composite, the status select shows Failed and
Aborted, the range picker shows the 7-day preset in UTC, the table is sorted by Started ascending and shows 50 rows
#### Scenario: Ranked sort deep link
- **GIVEN** `/egress?sort=duration&order=asc&from=now-24h&to=now`
- **WHEN** it is opened
- **THEN** the table is sorted by Duration ascending and the Duration header shows the ascending sort
#### Scenario: Ranked sort beyond the limit
- **GIVEN** `/egress?sort=duration&from=now-30d&to=now` and the default 7-day limit
- **WHEN** it is opened
- **THEN** the table is sorted by Started descending, the Duration header is disabled with its tooltip and no list
request carries `sort=duration`
#### Scenario: Filter change drops the cursor
- **GIVEN** the list on its third page
- **WHEN** the user selects the Active status
- **THEN** the URL has `status=starting,active,ending`, no `cursor`, and Back returns to the third page
#### Scenario: Defaults
- **WHEN** `/egress` opens without query parameters
- **THEN** it uses `status=all`, `sort=startedAt`, `order=desc`, the global range and the stored page size
#### Scenario: Any state change drops the cursor
- **GIVEN** `/egress` 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: Egress summary tiles
Edition: CE. The Egress page SHALL show four KPI tiles from the summary endpoint above the filters: Active, Started
in range (hint: count by type, in the fixed type order room composite, web, participant, track composite, track, with
type labels and without zero counts), Failed in range (hint: aborted and limit reached) and Recorded time (total
duration); approximate values carry a tooltip saying the range exceeds the exact-count limit, and the tiles keep their
last value while refreshing.
#### Scenario: Tiles after load
- **GIVEN** the summary answers `{ active: 3, startedInRange: 120, byType: { track: 20, room_composite: 100 },
byEndStatus: { complete: 110, failed: 7, aborted: 2 }, totalDurationMs: 36000000, approximate: false }`
- **WHEN** the page renders
- **THEN** the tiles read 3, 120 with hint "Room composite 100 · Track 20", 7 with hint "aborted 2", and "10 h 00 m"
#### Scenario: Stable hint order
- **GIVEN** a refresh that answers `byType` with its keys in another order
- **WHEN** the tile updates
- **THEN** the hint keeps the order "Room composite · Track"
### Requirement: Status, type, room and search filters
Edition: CE. The Egress page SHALL offer, in this order, the shared list search box labelled "Room name or egress ID"
(a room-name prefix or an exact egress ID, with the shared hints, clear button and debounce), a type select (All plus
the five types with their labels), a status select (All, Active, Complete, Failed, Aborted, Limit reached; Active
writes `starting,active,ending`) and, when `room` is set, the shared room chip; filters are written to the URL on
change.
#### Scenario: Filter order
- **GIVEN** `/egress?room=RM_ab12cd`
- **WHEN** the page renders
- **THEN** the filter bar reads, from the start, the search box labelled "Room name or egress ID", Type, Status and
the room chip
#### Scenario: Exact egress id
- **GIVEN** the user types `EG_ab12cd` in the search box
- **WHEN** the list answers one item
- **THEN** the table shows that single row, the hint reads "Exact ID: range and filters ignored · Enter opens it",
and the row's detail link leads to `/egress/EG_ab12cd`
#### Scenario: Enter on an exact egress id
- **GIVEN** the search box holds `EG_ab12cd`
- **WHEN** the user presses Enter
- **THEN** `/egress/EG_ab12cd` opens with the current `from`, `to` and `tz`
#### Scenario: Room chip
- **GIVEN** `/egress?room=RM_ab12cd` opened from the page of room `demo-room`
- **WHEN** the page renders
- **THEN** a chip "Room: demo-room" (reading "Room: RM_ab12cd" until the name is known) with a link to the room and a
remove button is shown, and removing it drops `room` and `cursor` from the URL
#### Scenario: Short search text
- **GIVEN** the user types `d`
- **WHEN** the debounce elapses
- **THEN** no request is sent and the hint reads "Type at least 2 characters"
### Requirement: Egress table
Edition: CE. The egress table SHALL show the columns Egress ID, Room, Type, Status, Started (sortable), Ended
(sortable), Duration (sortable) and Outputs, plus an error marker with tooltip on rows with `hasError` whose status does
not already say so (not failed, aborted or limit reached); Started and Ended carry the time-zone header hint.
#### Scenario: Row of an active recording
- **GIVEN** a `room_composite` egress active for 40 minutes in `demo-room` with one file output
- **WHEN** the row renders
- **THEN** it reads `EG_a1b2` once, linking to `/egress/EG_a1b2` with its copy button, `demo-room` linking to
`/rooms/RM_ab12cd`, the "Room composite" badge, the green "active" badge, the start time, a dash for Ended,
"40 m 00 s" counting up every second and the file icon without count badge
#### Scenario: Failed row
- **GIVEN** a `track` egress with `status: "failed"`, `hasError: true` and `errorCode: 403`
- **WHEN** the row renders
- **THEN** the red "failed" badge is shown with no error marker beside it
#### Scenario: Error behind a complete status
- **GIVEN** a `room_composite` egress with `status: "complete"`, `hasError: true` and `errorCode: 403`
- **WHEN** the row renders
- **THEN** an error marker whose tooltip reads "Egress failed (code 403) — open the detail for the error" follows the
"complete" badge, without the code part when the egress has no `errorCode`
#### Scenario: Reconciler-closed egress
- **GIVEN** an egress with `closedBy: "reconciler"`
- **WHEN** the row renders
- **THEN** the status badge has the dotted outline and the tooltip "Closed by the reconciler: the end time is
estimated"
#### Scenario: Phone layout
- **GIVEN** a 375 px viewport
- **WHEN** the table renders
- **THEN** each row is a card with the egress id link and its copy button, room, status, start time and output icons,
and a sort select offering Started, Ended and Duration is shown above
#### Scenario: Columns
- **WHEN** the egress table renders a row
- **THEN** Egress ID is a detail link with a copy button, Room a link to the room page ("—" for a web egress without
room), Type the kind badge with the type label (such as "Room composite"), Status the status badge with, when the
reconciler closed the egress, the estimated cue and the tooltip "Closed by the reconciler: the end time is estimated",
Duration counts up live for an in-flight egress and Outputs shows one icon per kind with a count badge above one
### Requirement: Egress pagination
Edition: CE. The Egress page SHALL use the cursor paginator with the stored page size (10, 25, 50 or 100, never
above the API's 100), send back the response's opaque `nextCursor` or `prevCursor` unchanged, show "Showing x–y of
total" only when the response carries `total`, and hide the paginator while the first page is empty.
#### Scenario: Next page
- **GIVEN** a response with `nextCursor`
- **WHEN** the user presses Next
- **THEN** the URL gets `cursor=<nextCursor>` and the paginator reads "Showing 26–50 of 212"
#### Scenario: Empty first page
- **GIVEN** a filter that matches no egress
- **WHEN** the list answers no items
- **THEN** the empty state is shown and no paginator is rendered under it
### Requirement: Egress live refresh
Edition: CE. The Egress page SHALL follow the shell's auto-refresh policy (default 10 s), refresh the list and the
tiles in place, show no live chip (the status badges say which egresses are in flight), and offer no "stale" state
because egresses report no periodic heartbeat.
#### Scenario: In-place refresh
- **GIVEN** the list showing an active egress
- **WHEN** a tick returns it as `complete`
- **THEN** the row's badge changes without a skeleton, the Active tile decrements and no LIVE chip is shown at any
point
### Requirement: Egress page states
Edition: CE. The Egress page SHALL render skeleton rows only until the first response, keep the rows or the empty
state on screen during a refresh, render the shared list empty state "No egress match" with the hint "Change the
filters or widen the time range." and the actions "Clear filters" and "Widen range", render "Egress <ID> was not
found" with "Clear search" when an exact egress ID finds nothing, and show the error banner keeping the previous rows
with retry and the request id.
#### Scenario: Empty filtered list
- **GIVEN** `?status=failed&type=web` returns no rows
- **WHEN** the empty state renders
- **THEN** it reads "No egress match" with the hint "Change the filters or widen the time range.", "Clear filters"
(dropping `status`, `type`, `room` and `q`) and "Widen range"
#### Scenario: Unknown egress id
- **GIVEN** the user searches `EG_missing`
- **WHEN** the list answers no items
- **THEN** the page reads "Egress EG_missing was not found" with "Clear search", which empties the search box and
drops `q`
#### Scenario: Refresh keeps the rows
- **GIVEN** rows on screen
- **WHEN** a refresh runs
- **THEN** the rows stay and no skeleton replaces them
#### Scenario: Failed refresh
- **GIVEN** rows from the previous load
- **WHEN** the refresh request fails
- **THEN** the rows stay visible under the inline error banner with retry and the request id
### Requirement: Egress detail route and CE lock
Edition: CE. `/egress/:egressId` SHALL be registered in CE with `requiredFeature: "egress-detail"`, read `tz`, `order`,
`cursor` and `limit` 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 egress ID) with its copy button and the status
badge with its estimated cue; the header shows no refresh control and no PRO endpoint is called.
#### Scenario: Locked from the list
- **GIVEN** CE and the user follows the `EG_a1b2` link from the list
- **WHEN** the page renders
- **THEN** the breadcrumbs read `Egress / EG_a1b2`, the title reads `EG_a1b2` with its copy button followed by the
status badge from the list data, no type badge, facts or refresh control are shown, the lock card reads "Egress
detail is available in OpenVidu PRO" with "Learn about OpenVidu PRO" and "Back to egress", and no request to
`/api/v1/egresses/EG_a1b2` is sent
#### Scenario: Locked deep link
- **GIVEN** CE and `/egress/EG_a1b2` opened directly
- **WHEN** the page renders
- **THEN** the header is filled from `GET /api/v1/egresses?q=EG_a1b2` and the lock card is shown
#### Scenario: Silhouette
- **GIVEN** CE and the locked egress page
- **WHEN** the lock renders
- **THEN** the dimmed silhouette behind it shows three tiles and one block, and no bar above them
#### Scenario: Header data source
- **GIVEN** CE and the locked page of `EG_a1b2`
- **WHEN** its header renders
- **THEN** the header is filled from the list cache or, when the cache has no entry, from
`GET /api/v1/egresses?q=EG_a1b2`
### Requirement: Egress sort keys
Edition: CE. The egress table SHALL sort by Started (`sort=startedAt`, the default, newest first), Ended
(`sort=endedAt`) or Duration (`sort=duration`), in the `order` the header toggles. The range always selects on the
start time. Ended and Duration rank the egresses of the range: an in-flight egress ranks as ending now and by its
elapsed time, both taken at the first page's request time, which the cursor keeps for the following pages.
#### Scenario: Sort by duration
- **GIVEN** the Egress page on the default 24 h range
- **WHEN** the user activates the Duration header
- **THEN** the URL has `sort=duration` and `order=desc` without `cursor`, and the longest egresses started in the
range come first, an in-flight one placed by the time it has been running
#### Scenario: In-flight egress sorted by Ended
- **GIVEN** two active egresses and ten ended ones started in the range
- **WHEN** the table is sorted by Ended descending
- **THEN** the two active egresses come first, as if ending now, followed by the ended ones from the latest end
#### Scenario: Next page keeps the ranking
- **GIVEN** the list sorted by Duration with an active egress on the first page
- **WHEN** the user presses Next a minute later
- **THEN** the second page continues the ranking of the first page, the active egress does not move to it and no
row of the first page reappears
#### Scenario: Range on the start time
- **GIVEN** `sort=endedAt` and the last 24 h
- **WHEN** the list loads
- **THEN** it holds only egresses started in the last 24 h, whatever their end time
### Requirement: Egress 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 Ended and Duration 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>";
Started 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 Duration header
- **THEN** the header is disabled and its tooltip reads "Narrow the range to <n> to sort by Duration", with <n>
naming the 7-day limit, while the Started header still toggles the order
#### Scenario: Range narrowed back
- **GIVEN** the 30-day range with the Duration header disabled
- **WHEN** the user picks the 7-day preset
- **THEN** the Ended and Duration headers become sortable
### Requirement: Egress room-name search
Edition: CE. A search text of at least two characters that is not an egress ID SHALL list, through `q`, the egresses
of the range whose room name starts with that text, case-insensitively, with no limit on the number of matching rooms
and no hint about one.
#### Scenario: Prefix matching many rooms
- **GIVEN** 150 rooms whose name starts with `load-`, each with an egress started in the range
- **WHEN** the user types `LOAD-`
- **THEN** after the debounce the URL has `q=LOAD-`, the paginator walks through the egresses of all 150 rooms, and no
hint about the newest matching rooms is shown
#### Scenario: Prefix only
- **GIVEN** egresses in rooms `Demo-Room`, `demo-2` and `my-demo` started in the range
- **WHEN** the user types `de`
- **THEN** the table lists the egresses of `Demo-Room` and `demo-2`, and none of `my-demo`