mirror of https://github.com/OpenVidu/openvidu.git
openvidu-dashboard: info no longer reports a license
GET /api/v1/info loses the license key, the feature catalogue the license-info identifier, and the backend the LicenseInfo extension token. OpenSpec change remove-info-license.chore/update-openvidu-actions-1.0.25
parent
322781dd73
commit
1890b6c61f
|
|
@ -25,9 +25,9 @@ Behaviour is specified in `openspec/specs/*-api`, `api-conventions`, `auth`, `st
|
||||||
authentication and stays out of the OpenAPI document. An edition that extends CE registers the full definition in the
|
authentication and stays out of the OpenAPI document. An edition that extends CE registers the full definition in the
|
||||||
lock's place (mount order kept) and swaps a CE route for its extension with `registry.replace()`; a `pro` startup
|
lock's place (mount order kept) and swaps a CE route for its extension with `registry.replace()`; a `pro` startup
|
||||||
that leaves a lock without a definition exits with code 1, naming it.
|
that leaves a lock without a definition exits with code 1, naming it.
|
||||||
- Extension tokens that `configure` can rebind: `Features`, `LicenseInfo`, `AnalyticsCatalogue` (metrics and
|
- Extension tokens that `configure` can rebind: `Features`, `AnalyticsCatalogue` (metrics and dimensions the
|
||||||
dimensions the analytics endpoints compute) and `OverviewExtension` (extra overview fields, merged before the
|
analytics endpoints compute) and `OverviewExtension` (extra overview fields, merged before the `ETag`). A
|
||||||
`ETag`). A `configure` that throws stops the startup with code 1.
|
`configure` that throws stops the startup with code 1.
|
||||||
- A resource slice is `src/<resource>/`: `*-schemas.ts` (Zod), `*-query.ts` (pure filter builders), `*-repository.ts`
|
- A resource slice is `src/<resource>/`: `*-schemas.ts` (Zod), `*-query.ts` (pure filter builders), `*-repository.ts`
|
||||||
(MongoDB), `*-service.ts` (paging, mapping), `*-responses.ts` (OpenAPI response schemas, checked against the mappers
|
(MongoDB), `*-service.ts` (paging, mapping), `*-responses.ts` (OpenAPI response schemas, checked against the mappers
|
||||||
in unit tests) and `*-routes.ts` (`register<Resource>Routes`: the CE routes and the locks of the PRO ones).
|
in unit tests) and `*-routes.ts` (`register<Resource>Routes`: the CE routes and the locks of the PRO ones).
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Container } from 'inversify';
|
import { Container } from 'inversify';
|
||||||
import { type Db, type MongoClient } from 'mongodb';
|
import { type Db, type MongoClient } from 'mongodb';
|
||||||
|
|
||||||
import { type Edition, type FeatureKey, type InfoLicense } from '@openvidu-dashboard/typings';
|
import { type Edition, type FeatureKey } from '@openvidu-dashboard/typings';
|
||||||
|
|
||||||
import { AnalyticsCache } from '../analytics/analytics-cache.js';
|
import { AnalyticsCache } from '../analytics/analytics-cache.js';
|
||||||
import { type AnalyticsCatalogue, CE_ANALYTICS_CATALOGUE } from '../analytics/analytics-catalogue.js';
|
import { type AnalyticsCatalogue, CE_ANALYTICS_CATALOGUE } from '../analytics/analytics-catalogue.js';
|
||||||
|
|
@ -63,7 +63,6 @@ export function buildContainer(inputs: ContainerInputs): Container {
|
||||||
container.bind(TOKENS.Shutdown).toConstantValue(shutdown);
|
container.bind(TOKENS.Shutdown).toConstantValue(shutdown);
|
||||||
container.bind<() => Date>(TOKENS.Clock).toConstantValue(() => new Date());
|
container.bind<() => Date>(TOKENS.Clock).toConstantValue(() => new Date());
|
||||||
container.bind<FeatureKey[]>(TOKENS.Features).toConstantValue([]);
|
container.bind<FeatureKey[]>(TOKENS.Features).toConstantValue([]);
|
||||||
container.bind<() => InfoLicense | undefined>(TOKENS.LicenseInfo).toConstantValue(() => undefined);
|
|
||||||
container.bind(TOKENS.RouteRegistry).toConstantValue(new RouteRegistry(edition));
|
container.bind(TOKENS.RouteRegistry).toConstantValue(new RouteRegistry(edition));
|
||||||
container.bind<AnalyticsCatalogue>(TOKENS.AnalyticsCatalogue).toConstantValue(CE_ANALYTICS_CATALOGUE);
|
container.bind<AnalyticsCatalogue>(TOKENS.AnalyticsCatalogue).toConstantValue(CE_ANALYTICS_CATALOGUE);
|
||||||
container
|
container
|
||||||
|
|
@ -86,7 +85,6 @@ export function buildContainer(inputs: ContainerInputs): Container {
|
||||||
version: version,
|
version: version,
|
||||||
features: () => container.get<FeatureKey[]>(TOKENS.Features),
|
features: () => container.get<FeatureKey[]>(TOKENS.Features),
|
||||||
limits: rankedSortLimits(config.lists.totalMaxRangeMs),
|
limits: rankedSortLimits(config.lists.totalMaxRangeMs),
|
||||||
license: () => container.get<() => InfoLicense | undefined>(TOKENS.LicenseInfo)(),
|
|
||||||
openapiDocsEnabled: inputs.docsEnabled,
|
openapiDocsEnabled: inputs.docsEnabled,
|
||||||
statsMode
|
statsMode
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ export const TOKENS = {
|
||||||
RouteRegistry: Symbol.for('openvidu-dashboard.RouteRegistry'),
|
RouteRegistry: Symbol.for('openvidu-dashboard.RouteRegistry'),
|
||||||
/** `FeatureKey[]` reported by info; empty on CE. */
|
/** `FeatureKey[]` reported by info; empty on CE. */
|
||||||
Features: Symbol.for('openvidu-dashboard.Features'),
|
Features: Symbol.for('openvidu-dashboard.Features'),
|
||||||
/** `() => InfoLicense | undefined` reported by info; nothing on CE. */
|
|
||||||
LicenseInfo: Symbol.for('openvidu-dashboard.LicenseInfo'),
|
|
||||||
InfoService: Symbol.for('openvidu-dashboard.InfoService'),
|
InfoService: Symbol.for('openvidu-dashboard.InfoService'),
|
||||||
SessionRepository: Symbol.for('openvidu-dashboard.SessionRepository'),
|
SessionRepository: Symbol.for('openvidu-dashboard.SessionRepository'),
|
||||||
AuthService: Symbol.for('openvidu-dashboard.AuthService'),
|
AuthService: Symbol.for('openvidu-dashboard.AuthService'),
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ const infoResponse = z
|
||||||
.int()
|
.int()
|
||||||
.meta({ description: 'Participants and agents without a room filter' })
|
.meta({ description: 'Participants and agents without a room filter' })
|
||||||
}),
|
}),
|
||||||
license: z.object({ validUntil: z.iso.date().optional(), maskedKey: z.string() }).optional(),
|
|
||||||
openapiDocsEnabled: z.boolean()
|
openapiDocsEnabled: z.boolean()
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
DEFAULT_RETENTION_MS,
|
DEFAULT_RETENTION_MS,
|
||||||
type Edition,
|
type Edition,
|
||||||
type FeatureKey,
|
type FeatureKey,
|
||||||
type InfoLicense,
|
|
||||||
type InfoLimits,
|
type InfoLimits,
|
||||||
type InfoMigration,
|
type InfoMigration,
|
||||||
type InfoPublicResponse,
|
type InfoPublicResponse,
|
||||||
|
|
@ -38,8 +37,6 @@ export interface InfoServiceOptions {
|
||||||
version: string;
|
version: string;
|
||||||
features: () => FeatureKey[];
|
features: () => FeatureKey[];
|
||||||
limits: InfoLimits;
|
limits: InfoLimits;
|
||||||
/** The license of a PRO deployment. */
|
|
||||||
license?: () => InfoLicense | undefined;
|
|
||||||
openapiDocsEnabled: boolean;
|
openapiDocsEnabled: boolean;
|
||||||
/** Refreshed with the `config` document of every full read. */
|
/** Refreshed with the `config` document of every full read. */
|
||||||
statsMode?: StatsModeReader;
|
statsMode?: StatsModeReader;
|
||||||
|
|
@ -67,7 +64,6 @@ export class InfoService {
|
||||||
const schema = byId.get('schema') as SchemaMetaSchema | undefined;
|
const schema = byId.get('schema') as SchemaMetaSchema | undefined;
|
||||||
const capabilities = byId.get('capabilities') as SchemaMetaCapabilities | undefined;
|
const capabilities = byId.get('capabilities') as SchemaMetaCapabilities | undefined;
|
||||||
const build = await this.db.command({ buildInfo: 1 });
|
const build = await this.db.command({ buildInfo: 1 });
|
||||||
const license = this.options.license?.();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
edition: this.options.edition,
|
edition: this.options.edition,
|
||||||
|
|
@ -80,7 +76,6 @@ export class InfoService {
|
||||||
storage: await this.storageInfo(),
|
storage: await this.storageInfo(),
|
||||||
features: this.options.features(),
|
features: this.options.features(),
|
||||||
limits: this.options.limits,
|
limits: this.options.limits,
|
||||||
...(license === undefined ? {} : { license }),
|
|
||||||
openapiDocsEnabled: this.options.openapiDocsEnabled
|
openapiDocsEnabled: this.options.openapiDocsEnabled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ test('About shows the version, the retention and the storage, and no finished mi
|
||||||
await expect(page.getByRole('row', { name: 'Daily rollups Kept forever' })).toBeVisible();
|
await expect(page.getByRole('row', { name: 'Daily rollups Kept forever' })).toBeVisible();
|
||||||
await expect(page.getByRole('row', { name: /rooms 1,203/ })).toBeVisible();
|
await expect(page.getByRole('row', { name: /rooms 1,203/ })).toBeVisible();
|
||||||
await expect(page.getByRole('link', { name: /OpenAPI/ })).toHaveCount(0);
|
await expect(page.getByRole('link', { name: /OpenAPI/ })).toHaveCount(0);
|
||||||
await expect(about).not.toContainText('License');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('incompatible writer', () => {
|
test.describe('incompatible writer', () => {
|
||||||
|
|
@ -112,14 +111,11 @@ test.describe('info unavailable', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('PRO', () => {
|
test.describe('PRO', () => {
|
||||||
test.use({
|
test.use({ apiOptions: { edition: 'pro' } });
|
||||||
apiOptions: { edition: 'pro', info: { license: { validUntil: '2027-01-01', maskedKey: '****-ABCD' } } }
|
|
||||||
});
|
|
||||||
|
|
||||||
test('shows the PRO badge and no license section', async ({ page }) => {
|
test('shows the PRO badge', async ({ page }) => {
|
||||||
await page.goto('/settings?tab=about');
|
await page.goto('/settings?tab=about');
|
||||||
|
|
||||||
await expect(page.locator('ovd-about-tab ovd-edition-badge')).toHaveText('PRO');
|
await expect(page.locator('ovd-about-tab ovd-edition-badge')).toHaveText('PRO');
|
||||||
await expect(page.getByText('****-ABCD')).toHaveCount(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,5 @@ export const FEATURE_COPY: Readonly<Record<FeatureKey, FeatureCopy>> = {
|
||||||
'per-node-breakdown': {
|
'per-node-breakdown': {
|
||||||
title: 'Per-node breakdown',
|
title: 'Per-node breakdown',
|
||||||
description: 'Rooms, participants and egress per OpenVidu node.'
|
description: 'Rooms, participants and egress per OpenVidu node.'
|
||||||
},
|
|
||||||
'license-info': {
|
|
||||||
title: 'License',
|
|
||||||
description: 'Validity and key of the OpenVidu PRO license.'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ describe('InfoService', () => {
|
||||||
|
|
||||||
expect(features.enabled('room-detail')).toBe(true);
|
expect(features.enabled('room-detail')).toBe(true);
|
||||||
expect(features.enabledSignal('participant-detail')()).toBe(true);
|
expect(features.enabledSignal('participant-detail')()).toBe(true);
|
||||||
expect(features.enabled('license-info')).toBe(false);
|
expect(features.enabled('per-node-breakdown')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('re-reads the info document when a request answers 403 PRO_FEATURE', async () => {
|
it('re-reads the info document when a request answers 403 PRO_FEATURE', async () => {
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,6 @@ describe('SettingsPage', () => {
|
||||||
expect(tables[0]?.querySelector('tbody tr')?.textContent).toContain('Events');
|
expect(tables[0]?.querySelector('tbody tr')?.textContent).toContain('Events');
|
||||||
expect(tables[1]?.querySelector('tbody tr')?.textContent).toContain('1,203');
|
expect(tables[1]?.querySelector('tbody tr')?.textContent).toContain('1,203');
|
||||||
expect(tables[1]?.querySelector('tbody tr')?.textContent).toContain('12.3 MB');
|
expect(tables[1]?.querySelector('tbody tr')?.textContent).toContain('12.3 MB');
|
||||||
expect(about?.textContent).not.toContain('License');
|
|
||||||
expect(about?.textContent).not.toContain('Links');
|
expect(about?.textContent).not.toContain('Links');
|
||||||
expect(about?.querySelector('ovd-external-link')).toBeNull();
|
expect(about?.querySelector('ovd-external-link')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ describe('DashboardShell', () => {
|
||||||
it('keeps the short edition badge and a documentation icon in the rail', async () => {
|
it('keeps the short edition badge and a documentation icon in the rail', async () => {
|
||||||
size.set('medium');
|
size.set('medium');
|
||||||
|
|
||||||
const harness = await shell('/settings', ['license-info']);
|
const harness = await shell('/settings', ['per-node-breakdown']);
|
||||||
const footer = (harness.fixture.nativeElement as HTMLElement).querySelector(
|
const footer = (harness.fixture.nativeElement as HTMLElement).querySelector(
|
||||||
'ovd-navigation-list ovd-sidenav-footer'
|
'ovd-navigation-list ovd-sidenav-footer'
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ export const FeatureKey = {
|
||||||
ANALYTICS_DIAGNOSTICS: 'analytics-diagnostics',
|
ANALYTICS_DIAGNOSTICS: 'analytics-diagnostics',
|
||||||
ANALYTICS_TOP_ROOMS: 'analytics-top-rooms',
|
ANALYTICS_TOP_ROOMS: 'analytics-top-rooms',
|
||||||
TRACK_DETAIL: 'track-detail',
|
TRACK_DETAIL: 'track-detail',
|
||||||
PER_NODE_BREAKDOWN: 'per-node-breakdown',
|
PER_NODE_BREAKDOWN: 'per-node-breakdown'
|
||||||
LICENSE_INFO: 'license-info'
|
|
||||||
} as const;
|
} as const;
|
||||||
export type FeatureKey = (typeof FeatureKey)[keyof typeof FeatureKey];
|
export type FeatureKey = (typeof FeatureKey)[keyof typeof FeatureKey];
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,6 @@ export interface InfoStorageEntry {
|
||||||
oldest?: string;
|
oldest?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** License of a PRO deployment; CE never reports one. */
|
|
||||||
export interface InfoLicense {
|
|
||||||
/** Last valid day, when the licensing service reports one. */
|
|
||||||
validUntil?: string;
|
|
||||||
/** The license key with every character but the last four masked. */
|
|
||||||
maskedKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Widest resolved ranges on which the lists accept a ranked (metric) sort. */
|
/** Widest resolved ranges on which the lists accept a ranked (metric) sort. */
|
||||||
export interface InfoLimits {
|
export interface InfoLimits {
|
||||||
/** Rooms, egresses and ingresses: `DASHBOARD_LIST_TOTAL_MAX_RANGE`. */
|
/** Rooms, egresses and ingresses: `DASHBOARD_LIST_TOTAL_MAX_RANGE`. */
|
||||||
|
|
@ -96,6 +88,5 @@ export interface InfoResponse {
|
||||||
storage: Record<string, InfoStorageEntry>;
|
storage: Record<string, InfoStorageEntry>;
|
||||||
features: FeatureKey[];
|
features: FeatureKey[];
|
||||||
limits: InfoLimits;
|
limits: InfoLimits;
|
||||||
license?: InfoLicense;
|
|
||||||
openapiDocsEnabled: boolean;
|
openapiDocsEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { describe, expect, expectTypeOf, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_PREFERENCES,
|
DEFAULT_PREFERENCES,
|
||||||
type InfoLicense,
|
|
||||||
type InfoMigration,
|
type InfoMigration,
|
||||||
type InfoResponse,
|
type InfoResponse,
|
||||||
type InfoRetention,
|
type InfoRetention,
|
||||||
|
|
@ -49,7 +48,6 @@ describe('UI contract', () => {
|
||||||
expectTypeOf<InfoResponse['retention']>().toEqualTypeOf<InfoRetention | undefined>();
|
expectTypeOf<InfoResponse['retention']>().toEqualTypeOf<InfoRetention | undefined>();
|
||||||
expectTypeOf<InfoResponse['storage']>().toEqualTypeOf<Record<string, InfoStorageEntry>>();
|
expectTypeOf<InfoResponse['storage']>().toEqualTypeOf<Record<string, InfoStorageEntry>>();
|
||||||
expectTypeOf<InfoResponse['database']['version']>().toBeString();
|
expectTypeOf<InfoResponse['database']['version']>().toBeString();
|
||||||
expectTypeOf<InfoResponse['license']>().toEqualTypeOf<InfoLicense | undefined>();
|
|
||||||
expectTypeOf<InfoResponse['openapiDocsEnabled']>().toBeBoolean();
|
expectTypeOf<InfoResponse['openapiDocsEnabled']>().toBeBoolean();
|
||||||
expectTypeOf<InfoResponse['features']>().not.toBeAny();
|
expectTypeOf<InfoResponse['features']>().not.toBeAny();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-10-27
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Design
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- `InfoService.build()` reads an optional `license` callback, bound in the container to `TOKENS.LicenseInfo` (`() =>
|
||||||
|
undefined` on CE). It spreads the result into the response only when it is defined.
|
||||||
|
- The `GET /api/v1/info` route declares `license` as an optional object of its zod response schema, so the OpenAPI
|
||||||
|
document shows it.
|
||||||
|
- `FeatureKey.LICENSE_INFO` has a copy in `FEATURE_COPY`, but no route or page gates on it.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- No edition can add license data to `GET /api/v1/info`, and the OpenAPI document does not suggest it.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Changing `info.license` of the OpenAPI document or `apiLicense`.
|
||||||
|
- Any other key of the info response.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Remove the seam, not only the field
|
||||||
|
|
||||||
|
The token, the service option, the type and the schema property all go. An extending edition can no longer rebind
|
||||||
|
`LicenseInfo`, because the token no longer exists. The response schema has no `license` property, so the document
|
||||||
|
never declares it.
|
||||||
|
|
||||||
|
Alternative rejected: keeping the token with a CE default and dropping only the schema property. The response
|
||||||
|
would still carry whatever a rebinding returned, without documentation.
|
||||||
|
|
||||||
|
### Drop the feature identifier
|
||||||
|
|
||||||
|
`license-info` gated nothing. Keeping it would advertise a feature in `features` that no page offers.
|
||||||
|
|
||||||
|
### OpenAPI shape
|
||||||
|
|
||||||
|
- `GET /api/v1/info`: the `200` schema loses `license`, and `features` items lose `license-info`.
|
||||||
|
- Paths, parameters and status codes do not change. No query, index or performance budget is involved.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **[Removing a key from `v1`]** → The key was never released, so the versioning policy for released fields does not
|
||||||
|
apply.
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Proposal
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
`GET /api/v1/info` declares an optional `license` block, and the feature catalogue a `license-info` key. No page
|
||||||
|
shows either, and the dashboard is not meant to expose license data, in the UI or through the API.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **BREAKING (unreleased):** `GET /api/v1/info` loses the optional `license` key: no edition reports it, and the
|
||||||
|
OpenAPI document no longer declares it.
|
||||||
|
- The `license-info` identifier leaves the `FeatureKey` enumeration and the lock catalogue of the UI.
|
||||||
|
- The backend loses the `LicenseInfo` extension token, the `InfoLicense` type and the info service option that
|
||||||
|
filled the key.
|
||||||
|
|
||||||
|
Scope excludes the following:
|
||||||
|
- The OpenAPI document's `info.license` (the license of the API itself) and the `apiLicense` option of
|
||||||
|
`startDashboard()`.
|
||||||
|
- CE's lack of license checks, which stays as specified.
|
||||||
|
|
||||||
|
Edition: CE, and the seams an extending edition uses. Repository: dashboard only (`openvidu-dashboard/` in
|
||||||
|
`OpenVidu/openvidu`).
|
||||||
|
|
||||||
|
CHANGELOG: none. The key and the feature identifier never shipped in a release.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `editions-info`: the Info endpoint keys, the migration outcome requirement (replaced by one without the license)
|
||||||
|
and the feature catalogue.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Typings: `api/info.ts` (`InfoLicense`, `InfoResponse.license`) and `api/features.ts` (`LICENSE_INFO`).
|
||||||
|
- Backend: `di/tokens.ts`, `di/container.ts`, `info/info-service.ts` and `info/info-routes.ts`. Rebinding
|
||||||
|
`LicenseInfo` from `DashboardOptions.configure` is no longer possible.
|
||||||
|
- Frontend: `components/lock/features.ts`.
|
||||||
|
- Tests: the typings UI contract, the info, shell and settings specs, and the settings e2e suite.
|
||||||
|
- Docs: `dashboard-ce/backend/AGENTS.md`, which lists the extension tokens.
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Info endpoint
|
||||||
|
|
||||||
|
Edition: CE. With a valid bearer token `GET /api/v1/info` SHALL answer `200` with the keys `edition`, `versions`,
|
||||||
|
`schema`, `migration`, `retention?`, `database`, `storage`, `features`, `limits` and `openapiDocsEnabled`, where
|
||||||
|
`schema.status` is the writer schema gate's `ok`, `newer`, `older` or `uninitialized` and `openapiDocsEnabled` tells
|
||||||
|
whether the docs routes are served.
|
||||||
|
|
||||||
|
#### Scenario: CE with a running writer
|
||||||
|
|
||||||
|
- **GIVEN** the CE dashboard 3.10.0 expects schema 1 and `schema_meta` records writer schema 1, server 3.10.0, a
|
||||||
|
completed migration and the default retention, with the default `DASHBOARD_LIST_TOTAL_MAX_RANGE`
|
||||||
|
- **WHEN** the client requests `GET /api/v1/info` with a valid token
|
||||||
|
- **THEN** the response is `200` with `edition: "ce"`, `versions: { dashboard: "3.10.0", server: "3.10.0" }`,
|
||||||
|
`schema: { dashboard: 1, writer: 1, status: "ok" }`, `migration.state: "completed"`, `migration.progress.processed`
|
||||||
|
and `.total`, `retention: { eventsMs: 604800000, statsMs: 604800000, tracksMs: 604800000,
|
||||||
|
entitiesMs: 7776000000, rollups5MinMs: 604800000, rollupsHourlyMs: 34560000000, rollupsDailyMs: null }`,
|
||||||
|
`features: []`, `limits: { rankedSortMaxRangeMs: 604800000, participantRankedSortMaxRangeMs: 86400000 }` and
|
||||||
|
`openapiDocsEnabled: false`
|
||||||
|
|
||||||
|
#### Scenario: Migration in progress
|
||||||
|
|
||||||
|
- **GIVEN** the writer's migration document has `state: 'running'`, `processed: 400000` and `total: 1200000`
|
||||||
|
- **WHEN** the client requests `GET /api/v1/info` with a valid token
|
||||||
|
- **THEN** `migration` is `{ state: "running", progress: { processed: 400000, total: 1200000, startedAt } }`
|
||||||
|
|
||||||
|
#### Scenario: Nested keys
|
||||||
|
|
||||||
|
- **WHEN** a client with a valid bearer token requests `GET /api/v1/info`
|
||||||
|
- **THEN** `versions` holds `dashboard` and `server?`, `schema` holds `dashboard`, `writer?` and `status`, `migration`
|
||||||
|
holds `state`, `progress?` and `error?`, and `limits` holds `rankedSortMaxRangeMs` and
|
||||||
|
`participantRankedSortMaxRangeMs`
|
||||||
|
|
||||||
|
### Requirement: Feature catalogue
|
||||||
|
|
||||||
|
Edition: CE. `features` SHALL be an array of `FeatureKey` identifiers from a shared enumeration (`room-detail`,
|
||||||
|
`participant-detail`, `egress-detail`, `ingress-detail`, `analytics-quality`, `analytics-diagnostics`,
|
||||||
|
`analytics-top-rooms`, `track-detail`, `per-node-breakdown`), empty on CE; every PRO feature has its identifier in it.
|
||||||
|
|
||||||
|
#### Scenario: CE feature list
|
||||||
|
|
||||||
|
- **WHEN** the CE dashboard answers `GET /api/v1/info`
|
||||||
|
- **THEN** `features` is `[]`
|
||||||
|
|
||||||
|
#### Scenario: Unknown identifier tolerated
|
||||||
|
|
||||||
|
- **GIVEN** a PRO backend newer than the UI reports a feature the UI does not know
|
||||||
|
- **WHEN** the UI reads `features`
|
||||||
|
- **THEN** it ignores the unknown identifier and keeps working
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Migration outcome in info
|
||||||
|
|
||||||
|
Edition: CE. `migration.progress` SHALL carry `processed`, `total`, `startedAt?` and `finishedAt?`, and `migration.error`
|
||||||
|
SHALL carry the reason of a `failed` migration.
|
||||||
|
|
||||||
|
#### Scenario: Failed migration
|
||||||
|
|
||||||
|
- **GIVEN** the writer's migration document has `state: 'failed'`, `finishedAt` and `error: 'legacy event without a room'`
|
||||||
|
- **WHEN** the client requests `GET /api/v1/info` with a valid token
|
||||||
|
- **THEN** `migration` has `state: "failed"`, `progress.finishedAt` and `error: "legacy event without a room"`
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Migration outcome and license in info
|
||||||
|
|
||||||
|
**Reason**: Info no longer reports a license; the migration outcome moves to "Migration outcome in info".
|
||||||
|
|
||||||
|
**Migration**: None. The `license` key never shipped in a release.
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Tasks
|
||||||
|
|
||||||
|
## 1. Typings
|
||||||
|
|
||||||
|
- [x] 1.1 Remove `InfoLicense` and `InfoResponse.license` from `typings/src/api/info.ts`, and `LICENSE_INFO` from
|
||||||
|
`typings/src/api/features.ts`. Drop the `license` assertion of `typings/test/ui-contract.test.ts`. Verify with the
|
||||||
|
typings typecheck and tests.
|
||||||
|
|
||||||
|
## 2. Backend
|
||||||
|
|
||||||
|
- [x] 2.1 Remove `TOKENS.LicenseInfo`, its default binding and the `license` option of `InfoService`, and the
|
||||||
|
`license` property of the info response schema. Verify with the backend typecheck and unit tests, and that the
|
||||||
|
OpenAPI document of the integration suite has no `license` under the info response.
|
||||||
|
|
||||||
|
## 3. Frontend
|
||||||
|
|
||||||
|
- [x] 3.1 Remove the `license-info` entry of `FEATURE_COPY` and the uses of that identifier and of "License" in the
|
||||||
|
info, shell and settings specs. Verify with the library unit tests.
|
||||||
|
|
||||||
|
## 4. Tests
|
||||||
|
|
||||||
|
- [x] 4.1 In `e2e/tests/settings.spec.ts`, drop the `license` of the PRO info and the "License" checks, keeping the
|
||||||
|
PRO badge check. Verify with the e2e suite.
|
||||||
|
|
||||||
|
## 5. Docs and CHANGELOG
|
||||||
|
|
||||||
|
- [x] 5.1 Remove `LicenseInfo` from the extension tokens of `dashboard-ce/backend/AGENTS.md`. No CHANGELOG line: the
|
||||||
|
key never shipped. Verify by reading the file against the change.
|
||||||
|
|
||||||
|
## 6. Integration check
|
||||||
|
|
||||||
|
- [x] 6.1 Run lint, format check, typecheck, spec validation, unit, integration and e2e tests. Verify that they pass.
|
||||||
Loading…
Reference in New Issue