mirror of https://github.com/OpenVidu/openvidu.git
642 lines
32 KiB
Markdown
642 lines
32 KiB
Markdown
# ui-shell Specification
|
||
|
||
## Purpose
|
||
The application frame shared by every page of the dashboard: routes and navigation, header controls, URL-driven
|
||
state, viewer preferences, session handling in the browser, locked PRO presentation, visual system and the
|
||
keyboard, responsive and accessibility rules that every page inherits.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Route configs with navigation metadata
|
||
Edition: CE. The UI library SHALL export its routes as route configs whose navigation metadata (label, icon, path,
|
||
order, feature key, live-capable, range-scoped, timed, retention) drives the sidenav, the header controls and the
|
||
breadcrumbs. A shell composes routes from one or more config arrays; a later array replaces an earlier route with the
|
||
same path and keeps the metadata keys it does not set. The shells SHALL contain no page logic.
|
||
|
||
#### Scenario: CE shell composes the library routes
|
||
- **GIVEN** the CE shell provides the library route configs
|
||
- **WHEN** the application starts
|
||
- **THEN** the sidenav lists Overview, Analytics, Rooms, Participants, Agents, Egress, Ingress and Settings in this order, each with its icon and route
|
||
|
||
#### Scenario: Later config overrides a route by path
|
||
- **GIVEN** a shell composes the CE configs followed by a config that declares `/rooms/:roomSid`
|
||
- **WHEN** the user opens `/rooms/RM_ab12cd`
|
||
- **THEN** the component of the later config renders and the navigation metadata of the entry is preserved
|
||
|
||
### Requirement: Sidenav modes by breakpoint
|
||
Edition: CE. The sidenav SHALL follow the Material CDK breakpoints: Large and XLarge use side mode expanded to 240 px
|
||
with a stored option to pin it as a 72 px rail; Medium uses a 72 px rail with tooltips that the burger expands as an
|
||
overlay; Small and XSmall use an over-mode drawer closed by default. The entry of the current section is marked visually
|
||
and with `aria-current="page"`, on its list and on every detail page under it, whatever the query parameters.
|
||
|
||
#### Scenario: Rail pinned on desktop
|
||
- **GIVEN** a Large viewport
|
||
- **WHEN** the user pins the sidenav as a rail
|
||
- **THEN** entries show icons with tooltips, the choice persists across reloads and the sidenav is in side mode
|
||
|
||
#### Scenario: Drawer on a phone
|
||
- **GIVEN** an XSmall viewport
|
||
- **WHEN** the user taps the burger
|
||
- **THEN** the drawer opens over the content, shows the entries and the sidenav footer, and closes on Escape or on selecting an entry
|
||
|
||
#### Scenario: Detail page keeps its section
|
||
- **GIVEN** the Rooms entry links to `/rooms?from=now-24h&to=now&tz=UTC`
|
||
- **WHEN** the user opens `/rooms/RM_ab12cd`, whose URL carries no range
|
||
- **THEN** Rooms is marked as the current entry
|
||
|
||
### Requirement: Header controls
|
||
|
||
Edition: CE. The header SHALL show, left to right: burger, logo and product name linking to Overview, global search
|
||
(≤ 400 px wide), the time range picker on range-scoped routes or the zone chip on other unlocked timed pages, refresh
|
||
control (only on live-capable routes whose page registered a refresh scheduler, none on a locked page), theme toggle and
|
||
user menu (username, Settings, Sign out), the last three vertically aligned, equally spaced.
|
||
|
||
#### Scenario: Header on a detail route without range
|
||
- **GIVEN** the route metadata is neither range-scoped nor live-capable
|
||
- **WHEN** the page renders
|
||
- **THEN** the picker and the refresh control are hidden and the other controls are shown
|
||
|
||
#### Scenario: Header collapses on narrow viewports
|
||
- **GIVEN** an XSmall viewport
|
||
- **WHEN** the header renders
|
||
- **THEN** it shows a brand row (burger, logo, search icon, overflow menu) and a second row with the range preset, or
|
||
the zone chip on a timed page without the picker, and the compact refresh control, with theme and user actions in the
|
||
overflow menu
|
||
|
||
#### Scenario: Wide header keeps the search compact
|
||
- **GIVEN** an XLarge viewport
|
||
- **WHEN** the header renders
|
||
- **THEN** the search field is at most 400 px wide and the remaining width stays free between it and the controls
|
||
|
||
#### Scenario: Locked page
|
||
- **GIVEN** the CE edition
|
||
- **WHEN** `/rooms/RM_ab12cd` renders its full-page lock
|
||
- **THEN** the header shows neither the range picker, nor the zone chip, nor a refresh control
|
||
|
||
#### Scenario: Analytics
|
||
- **WHEN** Analytics renders
|
||
- **THEN** the header shows the range picker and a refresh control with only the refresh button and the update time
|
||
|
||
#### Scenario: Aligned controls
|
||
- **GIVEN** a Large viewport on the Rooms page
|
||
- **WHEN** the header renders
|
||
- **THEN** the refresh button, the interval button, the theme toggle and the user menu button are centred on one line with the same spacing between them
|
||
|
||
#### Scenario: Overview
|
||
- **WHEN** Overview renders
|
||
- **THEN** the header shows the zone chip with the session zone and the refresh control, and no range picker
|
||
|
||
### Requirement: Retention of range-scoped pages
|
||
Edition: CE. Every range-scoped route SHALL name in its metadata the retention of its data among those of
|
||
`GET /api/v1/info`, and the header SHALL give it to the range picker on every viewport: the entity retention on Rooms,
|
||
Participants, Agents, Egress and Ingress, the daily rollup retention on Analytics.
|
||
|
||
#### Scenario: Lists
|
||
- **GIVEN** the default 90-day entity retention and a range starting 120 days ago
|
||
- **WHEN** the range picker opens on Rooms, Participants, Agents, Egress or Ingress
|
||
- **THEN** it reads "Data older than 90 days is not retained."
|
||
|
||
#### Scenario: Analytics with daily rollups kept forever
|
||
- **GIVEN** the default retentions (hourly rollups 400 days, daily rollups without limit) and a range starting 500
|
||
days ago
|
||
- **WHEN** the range picker opens on Analytics
|
||
- **THEN** it shows no retention warning, while the page keeps its banner about the hourly retention
|
||
|
||
#### Scenario: Analytics with a daily rollup retention
|
||
- **GIVEN** `GET /api/v1/info` reporting `retention.rollupsDailyMs` of 730 days and a range starting 3 years ago
|
||
- **WHEN** the range picker opens on Analytics
|
||
- **THEN** it reads "Data older than 730 days is not retained."
|
||
|
||
#### Scenario: Phone
|
||
- **GIVEN** an XSmall viewport on Rooms with a range starting 120 days ago
|
||
- **WHEN** the range picker of the range row opens
|
||
- **THEN** it reads "Data older than 90 days is not retained."
|
||
|
||
### Requirement: Theme toggle
|
||
Edition: CE. The theme toggle SHALL cycle system → light → dark, store the choice as a viewer preference and apply it
|
||
without reload. System mode follows `prefers-color-scheme`; the applied scheme is exposed so charts and icons
|
||
re-theme in place.
|
||
|
||
#### Scenario: Dark theme persists
|
||
- **GIVEN** the user selects dark
|
||
- **WHEN** the page is reloaded
|
||
- **THEN** the dark tokens apply before first paint and the toggle shows the dark state
|
||
|
||
#### Scenario: System mode follows the OS
|
||
- **GIVEN** the preference is system
|
||
- **WHEN** the OS switches to dark
|
||
- **THEN** the application re-themes without reload
|
||
|
||
### Requirement: Breadcrumbs from the route tree
|
||
|
||
Edition: CE. Breadcrumbs SHALL be derived from the activated route tree: an entity page shows two crumbs, the plural
|
||
section name linking to its list with the current range parameters, and the entity identifier (room SID, participant
|
||
SID, egress ID or ingress ID) as plain text with a copy button labelled by the identifier type; names are not part of
|
||
the trail.
|
||
|
||
#### Scenario: Participant inside a room
|
||
- **GIVEN** the route `/participants/PA_x1y2` resolves participant `alice` of room `demo-room`
|
||
- **WHEN** breadcrumbs render
|
||
- **THEN** they read `Participants / PA_x1y2`, the first links to `/participants` with the current `from`, `to` and `tz`, and the second is text followed by a copy button for `PA_x1y2` labelled "Copy participant SID"
|
||
|
||
#### Scenario: Egress of a room
|
||
- **GIVEN** the route `/egress/EG_ab12` of an egress of room `demo-room`
|
||
- **WHEN** breadcrumbs render
|
||
- **THEN** they read `Egress / EG_ab12` with one copy button and the identifier appears once
|
||
|
||
### Requirement: Footer version line
|
||
Edition: CE. The bottom of the sidenav SHALL show only the OpenVidu version ("OpenVidu {version}", the dashboard
|
||
version, as plain text), the edition badge, a Documentation link and, when the server enables it, an OpenAPI link.
|
||
In rail mode the sidenav bottom shows the short edition badge and a documentation icon link with a tooltip.
|
||
|
||
#### Scenario: Version line reflects the info endpoint
|
||
- **GIVEN** `GET /api/v1/info` reports dashboard 3.10.0, edition CE, server 3.10.0, schema 1 and `openapiDocsEnabled: false`
|
||
- **WHEN** the shell renders on a Large viewport
|
||
- **THEN** the sidenav bottom reads "OpenVidu 3.10.0", not a link, with the COMMUNITY badge and a Documentation link, and shows neither "server" nor "schema"
|
||
|
||
#### Scenario: OpenAPI link in development
|
||
- **GIVEN** `openapiDocsEnabled: true`
|
||
- **WHEN** the shell renders
|
||
- **THEN** an OpenAPI link to `api/v1/docs` follows the Documentation link
|
||
|
||
### Requirement: Banner slot
|
||
Edition: CE. A non-blocking banner slot under the header SHALL show, on every page, migration progress, schema-version
|
||
warnings and the silent writer warning reported by `GET /api/v1/status`, and connectivity errors reported by failed
|
||
requests; each banner is dismissible except the schema warnings (mismatch and uninitialized database). The banners
|
||
follow every response of the server status poll.
|
||
|
||
#### Scenario: Migration running
|
||
- **GIVEN** the status reports a migration in progress
|
||
- **WHEN** any page renders
|
||
- **THEN** the banner shows "Migration in progress" with processed/total records (events and stats) and refreshes until it completes
|
||
|
||
#### Scenario: Schema mismatch
|
||
- **GIVEN** the writer schema version differs from the dashboard's
|
||
- **WHEN** any page renders
|
||
- **THEN** a warning banner names both versions and cannot be dismissed
|
||
|
||
#### Scenario: Schema initialized while the page is open
|
||
- **GIVEN** the banner "The OpenVidu server has not initialized the analytics database yet" is shown
|
||
- **WHEN** the server initializes the database
|
||
- **THEN** the banner disappears within 10 s without a reload
|
||
|
||
### Requirement: Login page
|
||
Edition: CE. `/login` SHALL show username and password (with a show/hide toggle), submit on Enter, show the edition and
|
||
version (when the info endpoint exposes them without a session) and a documentation link, and never store
|
||
credentials. Errors are inline text: invalid credentials (focus back to username, password cleared), rate limited
|
||
(`429` with a countdown), server unreachable (banner with retry).
|
||
|
||
#### Scenario: Successful sign-in redirects
|
||
- **GIVEN** `/login?redirect=/rooms?status=active`
|
||
- **WHEN** valid credentials are submitted
|
||
- **THEN** the user lands on `/rooms?status=active`
|
||
|
||
#### Scenario: Rate limited
|
||
- **GIVEN** the login endpoint answers `429` with a retry delay
|
||
- **WHEN** the response arrives
|
||
- **THEN** the form shows "Too many attempts, try again in N s" counting down and re-enables submit at zero
|
||
|
||
#### Scenario: Already authenticated
|
||
- **GIVEN** a valid session
|
||
- **WHEN** the user opens `/login`
|
||
- **THEN** the application redirects to `redirect` or `/overview`
|
||
|
||
### Requirement: Session handling in the browser
|
||
Edition: CE. The shell SHALL implement the "Frontend session flow" of the `auth` capability
|
||
through one HTTP interceptor and the auth service: in-memory token, bearer header, single shared refresh on
|
||
`401` with one replay, silent restore on load, redirect to `/login?redirect=` on failure, nothing in browser
|
||
storage. In addition, Sign out SHALL call the logout endpoint, clear the in-memory session and navigate to
|
||
`/login`.
|
||
|
||
#### Scenario: Sign out
|
||
- **GIVEN** a logged-in session on `/rooms`
|
||
- **WHEN** the user chooses Sign out in the user menu
|
||
- **THEN** the logout endpoint is called, the token is dropped and the application shows `/login` without `redirect`
|
||
|
||
#### Scenario: Protected route without a session
|
||
- **GIVEN** no session and a failed silent restore
|
||
- **WHEN** the user opens `/settings?tab=about`
|
||
- **THEN** the application navigates to `/login?redirect=%2Fsettings%3Ftab%3Dabout`
|
||
|
||
### Requirement: Catch-all route
|
||
Edition: CE. Any unknown path SHALL redirect to `/overview` when authenticated and to `/login?redirect=<path>`
|
||
otherwise.
|
||
|
||
#### Scenario: Unknown path while signed out
|
||
- **GIVEN** no session
|
||
- **WHEN** the user opens `/nowhere`
|
||
- **THEN** the application navigates to `/login?redirect=/nowhere`
|
||
|
||
### Requirement: Locked PRO features in CE
|
||
Edition: CE. PRO entries, links and sections SHALL stay visible in CE and render a lock with the feature title, a short
|
||
explanation and a "Learn about OpenVidu PRO" link. The lock is decided from the feature flags of `GET /api/v1/info` and
|
||
makes no request to the locked endpoint. Full-page locks keep header and breadcrumbs over a dimmed, non-interactive
|
||
silhouette of three tiles and one block, with no refresh control in the header; in-card locks size to their message.
|
||
|
||
#### Scenario: Room detail in CE
|
||
- **GIVEN** the info endpoint does not report the feature `room-detail`
|
||
- **WHEN** the user follows a room SID link of the Rooms list
|
||
- **THEN** `/rooms/RM_ab12cd` shows the room header from the list data and the full-page lock, and no detail request is sent
|
||
|
||
#### Scenario: Track route in CE
|
||
- **GIVEN** the info endpoint does not report the feature `participant-detail`
|
||
- **WHEN** the user opens `/tracks/TR_a2b3`, as the global search does for an exact track ID
|
||
- **THEN** the page shows the full-page lock titled `TR_a2b3`, and no request about the track is sent
|
||
|
||
#### Scenario: Locked sidenav entry
|
||
- **GIVEN** a sidenav entry whose route declares a feature that `GET /api/v1/info` does not report
|
||
- **WHEN** the sidenav renders
|
||
- **THEN** the entry shows a lock icon with a tooltip and opens the full-page lock; no sidenav entry of the CE routes declares a feature, so the CE sidenav shows no lock
|
||
|
||
#### Scenario: Compact in-card lock
|
||
- **GIVEN** the CE Analytics page
|
||
- **WHEN** its PRO cards render
|
||
- **THEN** each locked card is as tall as its lock message, not as the chart it replaces
|
||
|
||
### Requirement: 403 PRO_FEATURE response
|
||
Edition: CE. When a request returns `403` with code `PRO_FEATURE`, the UI SHALL render the lock for that feature instead
|
||
of an error banner and refresh its cached feature flags.
|
||
|
||
#### Scenario: Stale feature flags
|
||
- **GIVEN** the cached flags enable a feature that the server reports as PRO-only
|
||
- **WHEN** the request returns `403 PRO_FEATURE`
|
||
- **THEN** the page shows the lock and the info endpoint is re-read
|
||
|
||
### Requirement: URL query parameters as the single source of truth
|
||
|
||
Edition: CE. List and chart state SHALL live only in the query parameters `from`, `to`, `tz`, `granularity`, `scope`,
|
||
`status`, `kind`, `publisher`, `features`, `nodeId`, `room`, `type`, `inputType`, `q`, `sort`, `order`, `limit`,
|
||
`cursor` and `page`, read by components as signals with no parallel state. Invalid values fall back to defaults
|
||
silently. Every change is a new history entry merged into the current parameters; changing a filter, sort, range or
|
||
`limit` removes `cursor` and `page`.
|
||
|
||
#### Scenario: Link reproduces a view
|
||
- **GIVEN** `/rooms?from=now-7d&to=now&tz=UTC&status=active&sort=uniqueIdentities&order=desc&limit=50`
|
||
- **WHEN** the URL is opened in a new browser
|
||
- **THEN** the page shows the same range, filter, sort and page size
|
||
|
||
#### Scenario: Invalid value falls back
|
||
- **GIVEN** `?limit=7&order=sideways`
|
||
- **WHEN** the page parses the parameters
|
||
- **THEN** it uses the stored page size and `desc`, without an error page
|
||
|
||
#### Scenario: Filter change drops the cursor
|
||
- **GIVEN** a list on its third page
|
||
- **WHEN** the status filter changes
|
||
- **THEN** the new URL has the new status, no `cursor` and no `page`, and Back returns to the third page
|
||
|
||
### Requirement: Relative range serialisation
|
||
Edition: CE. Presets SHALL be serialised as relative expressions and kept relative in the URL: `now`, `now-<n><unit>`
|
||
(`m`, `h`, `d`) and `now/M` (start of the current month in `tz`). Absolute values are ISO 8601. The UI
|
||
resolves relative values to absolute instants at request time so bookmarks stay fresh.
|
||
|
||
#### Scenario: Preset bookmark
|
||
- **GIVEN** `from=now-24h&to=now`
|
||
- **WHEN** the page loads a day later
|
||
- **THEN** the request covers the 24 hours before the current instant and the URL is unchanged
|
||
|
||
#### Scenario: This month
|
||
- **GIVEN** `from=now/M&to=now&tz=Europe/Madrid`
|
||
- **WHEN** the range resolves
|
||
- **THEN** `from` is the first day of the current month at 00:00 in Europe/Madrid
|
||
|
||
### Requirement: Global range carried across pages
|
||
Edition: CE. The last range (`from`, `to`) SHALL be kept in memory for the session; sidenav and breadcrumb links to
|
||
range-scoped routes append it, and a range-scoped route opened without range parameters uses it, or the stored
|
||
default preset (initially last 24 h). Routes not marked range-scoped ignore `from` and `to`. The zone is not part of
|
||
this memory: it follows the session zone of the Timezone requirement.
|
||
|
||
#### Scenario: Range follows navigation
|
||
- **GIVEN** Analytics shows `from=now-7d`
|
||
- **WHEN** the user clicks Rooms in the sidenav
|
||
- **THEN** the rooms list opens with `from=now-7d&to=now` and the same `tz`
|
||
|
||
#### Scenario: Default preset
|
||
- **GIVEN** a fresh session with no stored default
|
||
- **WHEN** `/rooms` opens without parameters
|
||
- **THEN** the URL becomes `/rooms?from=now-24h&to=now&tz=<browser zone>`
|
||
|
||
#### Scenario: Range memory after a reload of a detail
|
||
- **GIVEN** the rooms list on `from=now-7d&to=now&tz=UTC`, then a room detail reloaded in the browser
|
||
- **WHEN** the user clicks Participants in the sidenav
|
||
- **THEN** the participants list opens with the default preset and `tz=UTC`, the zone kept by the detail's URL
|
||
|
||
### Requirement: Back navigation restores list state
|
||
Edition: CE. Navigating from a list to a detail and back SHALL restore the list's URL state, including `cursor`, `page`
|
||
and `limit`, so the same page, rows and "Showing x–y" range are shown; reloading the URL shows the same.
|
||
|
||
#### Scenario: Detail and back
|
||
- **GIVEN** `/egress?cursor=abc&page=3&limit=25`
|
||
- **WHEN** the user opens an egress detail and presses Back
|
||
- **THEN** the list shows the page for `cursor=abc` with 25 rows and reads "Showing 51–75"
|
||
|
||
### Requirement: Page size preference
|
||
Edition: CE. `limit` SHALL accept 10, 25, 50 or 100; the URL value wins, otherwise the stored preference (default 25)
|
||
applies. Changing the page size writes both the URL and the preference and drops the cursor.
|
||
|
||
#### Scenario: Preference applied
|
||
- **GIVEN** the stored page size is 50
|
||
- **WHEN** `/rooms` opens without `limit`
|
||
- **THEN** the list requests 50 rows and the paginator shows 50
|
||
|
||
### Requirement: Timezone
|
||
|
||
Edition: CE. Every timed page (lists, Overview, Analytics; not Settings) SHALL show times in the session zone and
|
||
keep it in its URL as `tz`: a page without a valid `tz` receives the session zone through a URL replacement, and a
|
||
valid `tz` in a URL becomes the session zone, which starts as the stored default zone. The range picker and the zone
|
||
chip change it; it shows in them, in chart notes and in the header tooltip of timestamp columns.
|
||
|
||
#### Scenario: UTC toggle
|
||
- **GIVEN** `tz=Europe/Madrid`
|
||
- **WHEN** the user enables UTC
|
||
- **THEN** the URL has `tz=UTC`, timestamps and charts re-render in UTC with a `UTC` suffix on timestamps and in the
|
||
chart notes, and the cursor is dropped
|
||
|
||
#### Scenario: Invalid zone
|
||
- **GIVEN** a session in UTC
|
||
- **WHEN** `/overview?tz=Mars/Base` is opened
|
||
- **THEN** the URL is replaced with `tz=UTC` and no error is shown
|
||
|
||
#### Scenario: Settings is not a timed page
|
||
- **WHEN** Settings opens from a page in UTC
|
||
- **THEN** its URL carries no `tz`, About shows its timestamps in UTC, and the next timed page opens with `tz=UTC`
|
||
|
||
### Requirement: Auto-refresh policy
|
||
|
||
Edition: CE. Live-capable routes other than Analytics SHALL offer intervals off/10 s/30 s/1 min/5 min, default 10 s,
|
||
stored as one viewer preference for all live views, never in the URL. Analytics has no auto-refresh: it loads when
|
||
opened, on each URL state change and on the manual refresh button. A live view paused with a reason keeps the stored
|
||
preference.
|
||
|
||
#### Scenario: Hidden tab
|
||
- **GIVEN** a list refreshing every 10 s
|
||
- **WHEN** the tab is hidden for a minute and shown again
|
||
- **THEN** no request is made while hidden and one request fires immediately on return
|
||
|
||
#### Scenario: Slow request
|
||
- **GIVEN** a refresh request taking 15 s
|
||
- **WHEN** the next tick arrives
|
||
- **THEN** no second request starts until the first completes
|
||
|
||
#### Scenario: Interval preference
|
||
- **GIVEN** the user sets 30 s on the rooms list
|
||
- **WHEN** the overview opens
|
||
- **THEN** it refreshes every 30 s, like every other live view
|
||
|
||
#### Scenario: Hung request
|
||
- **GIVEN** a 10 s interval and a list request that does not answer
|
||
- **WHEN** 30 s elapse
|
||
- **THEN** the request is cancelled, the page keeps its last data under the refresh error, and a later tick sends a new request
|
||
|
||
#### Scenario: Analytics refreshes on demand
|
||
- **GIVEN** the auto-refresh preference at 10 s and Analytics open on `from=now-24h&to=now`
|
||
- **WHEN** a minute passes
|
||
- **THEN** Analytics sent its requests once, and sends them again only when the user presses refresh or changes its URL state
|
||
|
||
#### Scenario: Polling rules
|
||
- **GIVEN** a live view refreshing automatically
|
||
- **WHEN** it polls
|
||
- **THEN** a tick is skipped while a request is in flight, a request unanswered after 30 s fails, failures back off 10 →
|
||
20 → 40 s up to 5 min and show the banner once, and refresh pauses while the tab is hidden, resumes with an immediate
|
||
fetch, and stops on route leave or while the page is paused with a reason
|
||
|
||
### Requirement: API read timeout
|
||
|
||
Edition: CE. Every API read of the UI except CSV downloads SHALL fail with the error code `TIMEOUT` when the server has
|
||
not answered within 30 s; the page or section that sent it keeps its last data under the error notice "The server took
|
||
too long to answer; the data shown may be out of date." and a later refresh sends a new request.
|
||
|
||
#### Scenario: Slow CSV download
|
||
- **GIVEN** an Analytics CSV export that the server needs 45 s to send
|
||
- **WHEN** the download completes
|
||
- **THEN** the file is saved and no timeout error is shown
|
||
|
||
#### Scenario: Hung list request
|
||
|
||
- **GIVEN** a rooms list whose request does not answer
|
||
- **WHEN** 30 s elapse
|
||
- **THEN** the request is cancelled and the page keeps its last rows under the error notice "The server took too
|
||
long to answer; the data shown may be out of date." with Retry
|
||
|
||
### Requirement: M3 theme tokens
|
||
Edition: CE. The theme SHALL derive from the OpenVidu palette (seed `#0088aa`, accent `#00c8f7`) with the light and dark
|
||
token values: `primary` `#006b86` / `#5fd4fb`, `tertiary` `#00677f` / `#00c8f7`, surfaces
|
||
`#f8fafb` / `#0f1416`, `on-surface` `#171c1e` / `#dfe3e6`, error `#ba1a1a` / `#ffb4ab`. Every text pair meets
|
||
4.5:1 and UI pairs 3:1; `#00c8f7` is never text on light surfaces; the seed is used only for non-text accents.
|
||
|
||
#### Scenario: Primary text contrast
|
||
- **GIVEN** the light theme
|
||
- **WHEN** a link renders on `surface`
|
||
- **THEN** its colour is `#006b86` with contrast ≥ 4.5:1
|
||
|
||
### Requirement: Typography, density and icons
|
||
Edition: CE. Fonts SHALL be Inter (variable, tabular numerals for figures) and JetBrains Mono for SIDs and other
|
||
identifiers, served from the application bundle. Icons are Material Symbols Rounded served as a local SVG subset. No
|
||
asset loads from a CDN. Density is -1 on fine pointers and 0 on coarse pointers; minimum text size 12 px.
|
||
|
||
#### Scenario: Offline deployment
|
||
- **GIVEN** a deployment with no outbound internet access
|
||
- **WHEN** the dashboard loads
|
||
- **THEN** fonts and icons render and no request leaves the origin
|
||
|
||
#### Scenario: Icon-only buttons
|
||
- **GIVEN** any icon-only button
|
||
- **WHEN** it renders
|
||
- **THEN** it has an `aria-label` and a tooltip
|
||
|
||
### Requirement: English interface
|
||
Edition: CE. The UI SHALL be in English only, whatever the browser language, with no language setting; numbers use
|
||
English formatting, and enum values and event types read their labels from one shared label table.
|
||
|
||
#### Scenario: Browser in another language
|
||
- **GIVEN** a browser whose language is Spanish
|
||
- **WHEN** any page renders
|
||
- **THEN** its texts are in English and a count of 1203 reads "1,203"
|
||
|
||
### Requirement: Keyboard navigation and focus order
|
||
Edition: CE. Focus order SHALL be skip link → header → sidenav → breadcrumb → page actions → content. `/` focuses the
|
||
global search; Escape closes drawers and menus and returns focus to the trigger; a route change moves focus to the page
|
||
`h1` and announces its title. Every interactive element shows a 2 px primary focus ring with 2 px offset; chart
|
||
canvases are focusable with a text summary.
|
||
|
||
#### Scenario: Slash focuses search
|
||
- **GIVEN** focus is in the content area
|
||
- **WHEN** the user presses `/`
|
||
- **THEN** the global search field receives focus
|
||
|
||
#### Scenario: Route change announces the title
|
||
- **GIVEN** the user activates a sidenav entry with the keyboard
|
||
- **WHEN** the new page renders
|
||
- **THEN** focus lands on its `h1` and the title is announced
|
||
|
||
### Requirement: Landmarks and reduced motion
|
||
Edition: CE. Each page SHALL expose `banner`, `navigation` and `main` landmarks and exactly one `h1`;
|
||
toasts and refresh updates use polite live regions. With `prefers-reduced-motion`, pulsing live dots, chart
|
||
animations and value highlights are disabled.
|
||
|
||
#### Scenario: Reduced motion
|
||
- **GIVEN** the OS requests reduced motion
|
||
- **WHEN** a live view refreshes
|
||
- **THEN** the live dot does not pulse and tiles update without highlight animation
|
||
|
||
### Requirement: Responsive content rules
|
||
|
||
Edition: CE. Content SHALL be a single column on an 8 px grid with 24 px gutters (16 px below Small), max width 1600 px,
|
||
no horizontal page scroll at 320 px and 200 % zoom. KPI tiles follow the balanced tile rows, each at least 150 px wide,
|
||
so two fit per row on a phone; filters stay inline in the filter bar and wrap onto more lines at any width; tables
|
||
scroll sideways inside their card and become cards on XSmall; touch targets are ≥ 44 × 44 px; the header sticks to the
|
||
top.
|
||
|
||
#### Scenario: Phone width
|
||
- **GIVEN** a 320 px viewport
|
||
- **WHEN** a list page renders
|
||
- **THEN** there is no horizontal page scroll, the filters wrap inside the filter bar and the rows render as cards
|
||
|
||
#### Scenario: Charts below Medium
|
||
- **WHEN** time-series charts render below Medium
|
||
- **THEN** they span the full width at their 280 px height
|
||
|
||
### Requirement: Server status poll
|
||
Edition: CE. While signed in, the shell SHALL request `GET /api/v1/status` at once and then every 30 s, or every 10 s
|
||
while the migration runs or the schema is uninitialized, on every page and whatever the auto-refresh preference. The
|
||
poll pauses while the tab is hidden and fetches at once on return, backs off on failures like the page refresh, and does
|
||
not appear in the refresh control.
|
||
|
||
#### Scenario: Auto-refresh off
|
||
- **GIVEN** the auto-refresh set to Off on the Rooms page
|
||
- **WHEN** two minutes pass with the tab visible
|
||
- **THEN** the Rooms list sends no request and `GET /api/v1/status` requests keep going out 30 s apart
|
||
|
||
#### Scenario: Migration cadence
|
||
- **GIVEN** a status reporting a running migration
|
||
- **WHEN** the next polls run
|
||
- **THEN** they are 10 s apart until a status reports the migration ended, and 30 s apart after it
|
||
|
||
#### Scenario: Hidden tab
|
||
- **GIVEN** the tab hidden for two minutes
|
||
- **WHEN** it is shown again
|
||
- **THEN** no status request was sent while hidden and one is sent at once
|
||
|
||
#### Scenario: Signed out
|
||
- **WHEN** the user signs out
|
||
- **THEN** no further status request is sent
|
||
|
||
### Requirement: Info re-read on server changes
|
||
Edition: CE. The shell SHALL read `GET /api/v1/info` at start and after sign-in, and read it again once when a status
|
||
reports a schema status or a migration state different from the last info document; it SHALL NOT poll the info endpoint.
|
||
|
||
#### Scenario: Migration ends
|
||
- **GIVEN** an info document with a running migration
|
||
- **WHEN** a status reports the migration `completed`
|
||
- **THEN** `GET /api/v1/info` is requested once and About shows the server version and retention of the new document
|
||
|
||
#### Scenario: Steady server
|
||
- **GIVEN** an info document and status responses with the same schema status and migration state
|
||
- **WHEN** ten minutes pass
|
||
- **THEN** `GET /api/v1/info` is not requested again
|
||
|
||
### Requirement: Silent writer banner
|
||
|
||
Edition: CE. When two consecutive statuses found `generatedAt − writer.lastSeenAt` above the silent window of the writer
|
||
freshness windows, or no batch with a schema other than `uninitialized`, and no running migration, the shell SHALL show
|
||
on every page a dismissible warning "No analytics data received for N min" (N from the last status, at least 1) or "No
|
||
analytics data received from the OpenVidu server".
|
||
|
||
#### Scenario: Writer down on another page
|
||
- **GIVEN** the Rooms page open and two consecutive statuses whose writer batch is 3 minutes before their `generatedAt`
|
||
- **WHEN** the banner slot renders
|
||
- **THEN** one banner reads "No analytics data received for 3 min"
|
||
|
||
#### Scenario: Long interval
|
||
- **GIVEN** the auto-refresh at 5 min and a writer batching every 10 s
|
||
- **WHEN** ten minutes pass on the Overview
|
||
- **THEN** no silent writer banner is shown
|
||
|
||
#### Scenario: Migration in progress
|
||
- **GIVEN** a running migration and no writer batch yet
|
||
- **WHEN** any page renders
|
||
- **THEN** only the migration banner is shown
|
||
|
||
#### Scenario: Dismissed and back
|
||
- **GIVEN** the user dismissed the banner
|
||
- **WHEN** a status reports a fresh batch and the next two report the writer silent again
|
||
- **THEN** the banner is shown again
|
||
|
||
#### Scenario: Slow writer
|
||
- **GIVEN** statuses with `writer.batchIntervalMs` 60000 whose writer batch is 100 s before their `generatedAt`
|
||
- **WHEN** several of them arrive in a row
|
||
- **THEN** no silent writer banner is shown, since the silent window for a 60 s batch interval is 135 s
|
||
|
||
#### Scenario: One silent status
|
||
- **GIVEN** a status that finds the writer silent
|
||
- **WHEN** the next status finds a batch within the silent window
|
||
- **THEN** no silent writer banner has been shown
|
||
|
||
#### Scenario: Cleared by a recent batch
|
||
- **GIVEN** the silent writer banner is shown or was dismissed
|
||
- **WHEN** the first status with a batch within the silent window arrives
|
||
- **THEN** the banner is hidden, and once dismissed it returns only after it has cleared
|
||
|
||
### Requirement: Freshness judged at response time
|
||
|
||
Edition: CE. Every "stale", "fresh" or "alive" judgment SHALL be made at the time of the last response for that data
|
||
(`304` included), on the server clock when the response carries `generatedAt`, and SHALL NOT change until the next
|
||
response. An entity is stale when its last response marks it `stale`, as judged on the server clock, never by comparing
|
||
the browser clock with `lastStatsAt`; the writer is fresh or silent by the windows of the last status.
|
||
|
||
#### Scenario: Long interval list
|
||
- **GIVEN** the Rooms auto-refresh at 5 min and an active room whose `lastStatsAt` was 5 s old when the list arrived
|
||
- **WHEN** three minutes pass
|
||
- **THEN** the row shows no stale cue
|
||
|
||
#### Scenario: Browser clock ahead
|
||
- **GIVEN** a browser clock two minutes ahead of the server and an active room whose list item has no `stale` flag
|
||
- **WHEN** the Rooms list renders
|
||
- **THEN** the row shows no stale cue
|
||
|
||
#### Scenario: Between responses
|
||
- **GIVEN** no new response since the last one
|
||
- **WHEN** time passes in the browser
|
||
- **THEN** the browser clock only advances durations, and ages that would grow between responses are shown as clock
|
||
times
|
||
|
||
### Requirement: Writer freshness windows
|
||
Edition: CE. The shell SHALL judge the writer by the `writer.batchIntervalMs` of the last status, I, read as 10 s when
|
||
absent: the writer is fresh while its last batch is at most max(30 s, 1.5 × I + 5 s) old (the fresh window) and
|
||
silent once it is older than max(60 s, 2 × I + 15 s) (the silent window), both measured at the status's `generatedAt`.
|
||
|
||
#### Scenario: Interval absent
|
||
- **GIVEN** a status without `writer.batchIntervalMs`
|
||
- **WHEN** the shell judges the writer
|
||
- **THEN** the fresh window is 30 s and the silent window 60 s
|
||
|
||
#### Scenario: Slow writer
|
||
- **GIVEN** a status with `writer.batchIntervalMs` 60000
|
||
- **WHEN** the shell judges the writer
|
||
- **THEN** the fresh window is 95 s and the silent window 135 s
|
||
|
||
#### Scenario: Fast writer
|
||
- **GIVEN** a status with `writer.batchIntervalMs` 1000
|
||
- **WHEN** the shell judges the writer
|
||
- **THEN** the fresh window is 30 s and the silent window 60 s
|
||
|
||
### Requirement: Banner hysteresis
|
||
Edition: CE. The connectivity banner and the silent writer banner SHALL appear only after two consecutive bad results
|
||
(two API requests that do not reach the server, or two statuses that find the writer silent) and disappear on the
|
||
first good one.
|
||
|
||
#### Scenario: One failed request
|
||
- **GIVEN** a page refreshing every 10 s
|
||
- **WHEN** one request does not reach the server and the next one does
|
||
- **THEN** the connectivity banner is never shown
|
||
|
||
#### Scenario: Server unreachable
|
||
- **GIVEN** a page refreshing every 10 s
|
||
- **WHEN** two consecutive requests do not reach the server
|
||
- **THEN** the banner "The dashboard server is unreachable." is shown
|
||
|
||
#### Scenario: Recovery
|
||
- **GIVEN** the connectivity banner shown
|
||
- **WHEN** the next request reaches the server
|
||
- **THEN** the banner disappears
|