# 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=` 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 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 to sort by "; 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 to sort by Duration", with 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`