openvidu/openvidu-dashboard/openspec/changes/ui-review-fixes/design.md

172 lines
7.8 KiB
Markdown
Raw Normal View History

# Design
## Context
- **Charts:** `charts/chart-options.ts` builds every time-series option.
- The x-axis is an ECharts `time` axis whose labels are `HH:mm`, `MM-dd HH:mm` or `MM-dd` by span, with no minimum
interval, so a 3-minute range ticks every 20–30 s.
- The y-axis has no minimum interval either.
- Step series use `step: 'end'`: each value is drawn from its bucket start to the next point, so the last point has no
segment.
- **Skeletons:** `ovd-skeleton` paints its bars at once and is removed on the first response, which flashes when the
response takes tens of milliseconds.
- **Durations:** `formatDuration()` truncates at every unit. `ovd-duration` formats both running and stored durations
with it.
- **About storage:** `InfoService.storageEntry()` lists each collection's indexes and dates it only when an index leads
with `ts`, `bucket` or `createdAt`. The analytics indexes of the time fields are:
- `rooms.createdAt`, `ingresses.createdAt`;
- `participants { joinedAt: -1, _id: -1 }`;
- `egresses { startedAt: -1, _id: -1 }` and `ingress_sessions { startedAt: -1, _id: -1 }`;
- `events.ts`, `analytics_5min.bucket`, `analytics_hourly.bucket`;
- `analytics_daily { scope, bucket }`;
- the `track_stats` time series, indexed on its metadata before `ts`;
- `nodes`, with no time index and one document per node;
- `tracks` and `track_subscriptions`, with their time field only behind `participantSid`, `roomSid`, `subscriberSid`
or `trackSid`.
- **Global search:** `exactIdTarget()` recognises `TR_` through `entityIdKind()` but drops it, because no route takes
a track ID.
- **Browser and OS labels:** the rollups key browsers by family, and an empty browser falls into `other` until the
writer change adds `none`. Pages that show a client's OS print the reported string (`linux` from the Go SDK, `Linux`
from the JS SDK).
## Goals / Non-Goals
**Goals:**
- One rule per chart property, applied in `timeSeriesOption()` for every chart of every edition.
- No new inputs on the chart, the skeleton or the duration components.
**Non-Goals:**
- Changing the label formats of the axes or the retention notices.
- New indexes for tracks and subscriptions.
## Decisions
### D1. The x-axis interval follows the label unit
`minInterval` is 60 000 ms when the labels show minutes (`HH:mm`, `MM-dd HH:mm`) and 86 400 000 ms when they show days.
It comes from the same span rule as the label format.
Rejected: passing the bucket width from every chart. The labels are the constraint, and the granularity is already in
the chart note.
### D2. Integer data gets integer ticks
`yAxis.minInterval` is 1 when every non-null value of every series is an integer.
Rejected: an `integer` input on the chart, which every count chart of every edition would have to set.
### D3. A step series is extended to `to`
When the last point of a step series is not null and starts before `to`, `timeSeriesOption()` appends `[to, value]`, so
`step: 'end'` draws the last bucket up to the end of the range. A dense series always ends in the bucket that holds
`to`.
Rejected: `step: 'start'`, which would draw every value one bucket early.
### D4. The skeleton fades in after 300 ms
Every loading placeholder keeps its height from the start and carries the global class `ovd-skeleton-reveal`, whose CSS
animation keeps its opacity at 0 for 300 ms and then shows it: `ovd-skeleton`, the skeleton rows of `ovd-data-table`
and the KPI tile skeleton. Pages keep their loading logic. Under `prefers-reduced-motion` the global rule shortens the
fade and the delay stays.
Rejected:
- delaying the loading flag in every loader, which touches every page and both loaders;
- keeping the previous content, which does not exist on a first load.
### D5. Stored durations are rounded, running ones are not
`formatDuration(ms, { mode })` rounds to the smallest unit it will show by default:
- it picks the unit from the magnitude, rounds, and formats again when rounding crosses a unit (59 600 ms → 60 s →
"1 m 00 s");
- `mode: 'floor'` keeps the current truncation;
- `ovd-duration` passes `floor` while `running` is true, both for `liveDurationMs` and for `liveTotalMs`.
Rejected: rounding live durations, which would make a counter jump to the next second halfway through.
### D6. A fixed time field per collection
`InfoService` holds one map from collection to field, and optional filter, and reads `find(filter).sort({ field: 1 })
.limit(1)` with a projection of the field. This replaces the index discovery and saves a `listIndexes` per collection.
| Collection | Field |
| --- | --- |
| `rooms`, `ingresses` | `createdAt` |
| `participants` | `joinedAt` |
| `egresses`, `ingress_sessions` | `startedAt` |
| `events` | `ts` |
| `analytics_5min`, `analytics_hourly` | `bucket` |
| `analytics_daily` | `bucket` with `{ scope: 'global' }` |
| `nodes` | `firstSeenAt` (one document per node, no index needed) |
`tracks`, `track_subscriptions` and `track_stats` have no entry, and About explains the "—". The `track_stats` query
was explained on a generated dataset: the time series has no index on its time alone, so the plan is a bounded sort
over a scan of the buckets.
Rejected:
- new `{ publishedAt }` and `{ subscribedAt }` indexes, two more indexes on the most written collections for one About
cell;
- a sort without an index, a collection scan every 60 s.
### D7. Plain version text
`sidenav-footer.ts` renders the version in a `span`.
### D8. The room averages say why they are missing
`missingHint()` takes the reason: `roomsEnded === 0` gives "No room ended in this range", anything else "Not provided by
this server version".
### D9. Total duration tile
The Egress page tile is labelled "Total duration", with `INFO_TEXTS.egressTotalDuration`.
### D10. Browser labels and the OS label
`format/labels.ts` gains:
- `BROWSER_LABELS` for `chrome`, `firefox`, `safari`, `edge`, `none` ("No browser") and `other`, used by the browser
donut, which today prints the key;
- `clientOsLabel(os)`, which compares the reported string, case-insensitively and trimmed, with the family names
(`windows`, `macos`, `mac os`, `linux`, `android`, `ios`) and returns the `OS_LABELS` entry, or the string as
reported.
The library exports both for the detail pages of every edition.
Rejected: mapping by substring as the writer does, which would turn "Chrome OS" into "Linux" on a page meant to show
what the client reported.
### D11. A locked track route
`routes.ts` adds `tracks/:trackSid`:
- with no navigation entry and `page.feature: 'participant-detail'`;
- with the breadcrumb param `trackSid`, which titles the lock, as the route has no list above it;
- opening a locked page that reuses the participant lock.
`exactIdTarget()` maps `track` to `/tracks`. An edition with the feature replaces the route by path, as it does for
the other detail routes.
## API
No shape change: `storage.<collection>.oldest` exists and only covers more collections.
## Performance budget
About reads at most eleven point queries every 60 s, with the existing cache. Each is verified in the info integration
test with `explain()`: an `IXSCAN` with one document examined, and a `COLLSCAN` only on `nodes`.
`track_stats` has no entry: its plan scans the buckets of the time series.
## Risks / Trade-offs
- [Rounding changes durations across the UI, by up to half a unit] → Unit tests of the formatter for both modes, and
updated fixtures where a duration reads differently.
- [A running duration that ends shows one second more than it last counted] → Accepted: the stored value is the
measured one.
- [The `none` browser key only appears in rollups written after the writer change] → Older rollups keep those sessions
under `other`, which still reads "Other".
## Migration Plan
Ships with the 3.10 dashboard. The browser label of `none` relies on the typings of `writer-review-fixes`, applied
first.