Description
User Story
As a user of the DIAL chat app
I want to open a Settings page from the user menu and see my current rate-limit/usage status
So that I can monitor my usage without leaving the app — while the feature is still gated
behind a flag so it can be rolled out per environment
Acceptance Criteria
- When
SettingsPageEnabled (env SETTINGS_PAGE_ENABLED) resolves true, a gear icon appears in
the UserMenu dropdown; activating it (click or keyboard) navigates to /settings.
- When the flag resolves
false (its default), the gear icon is omitted entirely from the menu,
and direct navigation to /settings redirects (replacing history) to the root route instead of
rendering the page.
/settings renders a vertical navigation panel (SettingsPanel, from the new
@epam/ai-dial-settings-panel library) with a Usage row, selected by default.
- Opening the Usage tab invokes
useUsageData, which calls GET /api/v1/user/usage and exposes
{ usage, isLoading, usageError }. (Originally also called GET /api/v1/user/limits in
parallel; that call was removed as a follow-up correction — see below — once it was confirmed
usage's top-level cost fields already carry the real global budget.)
- The Usage tab renders three period cards — Today, This week, This month — mapped from
usage
via apps/chat/src/utils/map-usage-data-to-dashboard.ts, using the new
@epam/ai-dial-usage-dashboard library (UsageLimitCardGroup/UsageLimitCard). Each card shows
the used amount, "used of $total", a progress bar, "$remaining left", and a used-percent caption,
plus a Default ("Within limits") / RunningLow / LimitReached status badge — colors and
layout matched against the Figma spec (DIAL 2.0 Concept, node 1106-189).
Per-model/per-function token and request metering (the "Model limits" / "Function limits"
sections of the Figma design) is intentionally out of scope for this ticket and remains a
follow-up — delivered as a follow-up, see "Model limits" section below. Per-model metering
is done; per-function (toolset) metering remains a further follow-up.
- While
GET /api/v1/user/limits / GET /api/v1/user/usage return 502 on the backend, the tab
falls back to a temporary in-memory mock dataset (deliberately spanning all three statuses) so
the UI is reviewable; the fallback and its flag are isolated in one file
(usage-tab-temp-mock.ts) to delete once the backend is fixed.
- On the backend, DIAL Core requests for
/v1/user/limits and /v1/user/usage emit debug-level
logs (request start, response status/body, error body) without changing response behavior, to
aid support/troubleshooting.
Model limits (per-model table) — follow-up, delivered
Adds a "Model limits" table below the three aggregate cards, showing per-model Cost/Tokens/Requests
usage and status, with a period selector.
- New
ModelLimitsSection component (@epam/ai-dial-usage-dashboard): heading with model count, a
controlled period selector (Last minute / Last hour / Last 24 hours / Last 7 days / Last 30 days),
and a table with Item, Cost, Tokens, Requests, and Status columns — one row per model present in
the fetched usage data. Fully presentational: normalized ModelLimitRow/ModelLimitMetricCell
types, a ModelLimitStatus enum, and a five-value ModelLimitsPeriod enum, all free of Core DTO
field names and the 2**53 unlimited sentinel.
- New app-level adapter
apps/chat/src/utils/map-user-usage-to-model-limits.ts: reads
usage.deployments (already fetched by useUsageData — no new API call), joins deployment IDs
with model/catalog metadata (useDeployments().items), maps the selected period to the correct
*Stats fields, detects finite/unlimited/unavailable per metric, computes per-metric and
per-row status (RunningLow at ≥75% used, LimitReached at ≥100%), and formats all display and
accessible-label strings.
- Row set and order are exactly
Object.keys(usage.deployments) — the table shows only models
present in the fetched usage data (never more, never fewer), independent of the deployments
catalog's load state or order, so there's no visible reflow once the catalog finishes loading.
- Period selector exposes all granularities the upstream data actually has:
minute*Stats/
hourRequestStats in addition to day/week/month. Cost/Tokens have no hour-level field
(Unavailable for "Last hour"); Requests have no minute/week/month-level field (Unavailable
for those periods) — never a silent fallback to a different period's field.
- Per-model cost is always shown as attributed spend + "No limit" (never a progress bar or a
finite cost status), since the upstream contract's per-deployment cost total is always the
unlimited sentinel — the real budget is the global one already shown in the aggregate cards.
- Correction — dropped the
GET /api/v1/user/limits fetch entirely: confirmed against real
production payloads that GET /api/v1/user/usage's top-level dayCostStats/weekCostStats/
monthCostStats already carry the same real global cost budget GET /api/v1/user/limits would
report, so the second endpoint call was redundant. useUsageData now calls only getUserUsage();
UseUsageDataResult is { usage, isLoading, usageError } (dropped limits/limitsError); the
previous partial-vs-full-failure notification distinction collapsed to one failure mode.
getUserLimits() itself is left in place, unused, for a future feature that might need it.
- Integrated below
UsageLimitCardGroup in UsageTab, reusing the existing loading/error/
notification behavior without emitting a duplicate notification; shows a localized empty state
when usage.deployments is absent/empty.
Definition of Done
- Unit tests pass:
apps/chat (211 test files / 3016+ tests), libs/settings-panel (12/12),
libs/usage-dashboard (32/32, including 17 new ModelLimitsSection tests), apps/chat-api
(feature-flag registration + logging). New adapter test file map-user-usage-to-model-limits.spec.ts
(29 tests).
nx lint / nx typecheck / nx build clean for chat, chat-api, usage-dashboard, and
@epam/ai-dial-settings-panel. nx affected lint/test/build against origin/development clean.
- i18n keys added for all new user-visible strings (including the Model limits column headers,
period labels, and unavailable/no-limit text); no missing translations. The now-unreachable
PartialLoadError key was removed.
SettingsPanel, UsageLimitCard/UsageLimitCardGroup, and ModelLimitsSection use only CSS
logical properties (RTL-safe layout).
- Manual visual QA against the Figma design (
DIAL 2.0 Concept, node 1106-189) done for the three
aggregate cost cards and the Model limits table.
npm run validate:docs passes; libs/usage-dashboard/README.md documents ModelLimitsSection
and its types.
Related issues
No response
Details
apps/chat-api/src/deployments/details/deployments-details.service.ts — getUserLimits/
getUserUsage now log: a debug line before each DIAL Core call; a debug line with status +
body when DIAL Core returns an error (now also passed into mapDialHttpStatus so the mapped
exception carries DIAL Core's own message); a debug line with the raw response body on success;
and, in the catch block, a debug line that distinguishes an already-mapped HttpException
being re-thrown from a genuine unexpected/network error.
apps/chat-api/src/app-config/feature-flags/feature-key.enum.ts /
config-registry.constants.ts — SettingsPageEnabled feature key (type: 'feature',
visibility: 'client', defaultValue: false), driven by SETTINGS_PAGE_ENABLED, matching the
existing scheduledTasksEnabled shape.
libs/settings-panel (@epam/ai-dial-settings-panel) and libs/usage-dashboard
(@epam/ai-dial-usage-dashboard) — both tagged type:ui, purely presentational, no i18n, no
apps/chat/src/server-api/*, no routing inside the lib; host-specific labels/icons/data are
passed in as props, per the repo's library isolation rules.
apps/chat/vite.config.mts needed a resolve.alias entry for each new lib (pointing at its
src/index.ts) — without it, a lib's CSS Modules/Tailwind-compiled styles never reach the app
bundle; the dev server silently serves the library's stale prebuilt dist/index.js.
- Spans several archived OpenSpec changes for full history:
openspec/changes/archive/2026-08-19-settings-usage-page/,
openspec/changes/archive/2026-08-19-settings-sidebar-panel/,
openspec/changes/archive/2026-08-19-gate-settings-page-feature-flag/,
openspec/changes/archive/2026-08-21-add-usage-dashboard-model-limits/ (the Model limits
follow-up covered above).
Confidential information
Description
User Story
As a user of the DIAL chat app
I want to open a Settings page from the user menu and see my current rate-limit/usage status
So that I can monitor my usage without leaving the app — while the feature is still gated
behind a flag so it can be rolled out per environment
Acceptance Criteria
SettingsPageEnabled(envSETTINGS_PAGE_ENABLED) resolvestrue, a gear icon appears inthe
UserMenudropdown; activating it (click or keyboard) navigates to/settings.false(its default), the gear icon is omitted entirely from the menu,and direct navigation to
/settingsredirects (replacing history) to the root route instead ofrendering the page.
/settingsrenders a vertical navigation panel (SettingsPanel, from the new@epam/ai-dial-settings-panellibrary) with aUsagerow, selected by default.useUsageData, which callsGET /api/v1/user/usageand exposes{ usage, isLoading, usageError }. (Originally also calledGET /api/v1/user/limitsinparallel; that call was removed as a follow-up correction — see below — once it was confirmed
usage's top-level cost fields already carry the real global budget.)usagevia
apps/chat/src/utils/map-usage-data-to-dashboard.ts, using the new@epam/ai-dial-usage-dashboardlibrary (UsageLimitCardGroup/UsageLimitCard). Each card showsthe used amount, "used of $total", a progress bar, "$remaining left", and a used-percent caption,
plus a
Default("Within limits") /RunningLow/LimitReachedstatus badge — colors andlayout matched against the Figma spec (
DIAL 2.0 Concept, node 1106-189).Per-model/per-function token and request metering (the "Model limits" / "Function limits"— delivered as a follow-up, see "Model limits" section below. Per-model meteringsections of the Figma design) is intentionally out of scope for this ticket and remains a
follow-up
is done; per-function (toolset) metering remains a further follow-up.
GET /api/v1/user/limits/GET /api/v1/user/usagereturn502on the backend, the tabfalls back to a temporary in-memory mock dataset (deliberately spanning all three statuses) so
the UI is reviewable; the fallback and its flag are isolated in one file
(
usage-tab-temp-mock.ts) to delete once the backend is fixed./v1/user/limitsand/v1/user/usageemit debug-levellogs (request start, response status/body, error body) without changing response behavior, to
aid support/troubleshooting.
Model limits (per-model table) — follow-up, delivered
Adds a "Model limits" table below the three aggregate cards, showing per-model Cost/Tokens/Requests
usage and status, with a period selector.
ModelLimitsSectioncomponent (@epam/ai-dial-usage-dashboard): heading with model count, acontrolled period selector (Last minute / Last hour / Last 24 hours / Last 7 days / Last 30 days),
and a table with Item, Cost, Tokens, Requests, and Status columns — one row per model present in
the fetched usage data. Fully presentational: normalized
ModelLimitRow/ModelLimitMetricCelltypes, a
ModelLimitStatusenum, and a five-valueModelLimitsPeriodenum, all free of Core DTOfield names and the
2**53unlimited sentinel.apps/chat/src/utils/map-user-usage-to-model-limits.ts: readsusage.deployments(already fetched byuseUsageData— no new API call), joins deployment IDswith model/catalog metadata (
useDeployments().items), maps the selected period to the correct*Statsfields, detects finite/unlimited/unavailable per metric, computes per-metric andper-row status (
RunningLowat ≥75% used,LimitReachedat ≥100%), and formats all display andaccessible-label strings.
Object.keys(usage.deployments)— the table shows only modelspresent in the fetched usage data (never more, never fewer), independent of the deployments
catalog's load state or order, so there's no visible reflow once the catalog finishes loading.
minute*Stats/hourRequestStatsin addition to day/week/month. Cost/Tokens have no hour-level field(
Unavailablefor "Last hour"); Requests have no minute/week/month-level field (Unavailablefor those periods) — never a silent fallback to a different period's field.
finite cost status), since the upstream contract's per-deployment cost total is always the
unlimited sentinel — the real budget is the global one already shown in the aggregate cards.
GET /api/v1/user/limitsfetch entirely: confirmed against realproduction payloads that
GET /api/v1/user/usage's top-leveldayCostStats/weekCostStats/monthCostStatsalready carry the same real global cost budgetGET /api/v1/user/limitswouldreport, so the second endpoint call was redundant.
useUsageDatanow calls onlygetUserUsage();UseUsageDataResultis{ usage, isLoading, usageError }(droppedlimits/limitsError); theprevious partial-vs-full-failure notification distinction collapsed to one failure mode.
getUserLimits()itself is left in place, unused, for a future feature that might need it.UsageLimitCardGroupinUsageTab, reusing the existing loading/error/notification behavior without emitting a duplicate notification; shows a localized empty state
when
usage.deploymentsis absent/empty.Definition of Done
apps/chat(211 test files / 3016+ tests),libs/settings-panel(12/12),libs/usage-dashboard(32/32, including 17 newModelLimitsSectiontests),apps/chat-api(feature-flag registration + logging). New adapter test file
map-user-usage-to-model-limits.spec.ts(29 tests).
nx lint/nx typecheck/nx buildclean forchat,chat-api,usage-dashboard, and@epam/ai-dial-settings-panel.nx affectedlint/test/build againstorigin/developmentclean.period labels, and unavailable/no-limit text); no missing translations. The now-unreachable
PartialLoadErrorkey was removed.SettingsPanel,UsageLimitCard/UsageLimitCardGroup, andModelLimitsSectionuse only CSSlogical properties (RTL-safe layout).
DIAL 2.0 Concept, node 1106-189) done for the threeaggregate cost cards and the Model limits table.
npm run validate:docspasses;libs/usage-dashboard/README.mddocumentsModelLimitsSectionand its types.
Related issues
No response
Details
apps/chat-api/src/deployments/details/deployments-details.service.ts—getUserLimits/getUserUsagenow log: a debug line before each DIAL Core call; a debug line withstatus+bodywhen DIAL Core returns an error (now also passed intomapDialHttpStatusso the mappedexception carries DIAL Core's own message); a debug line with the raw response body on success;
and, in the
catchblock, a debug line that distinguishes an already-mappedHttpExceptionbeing re-thrown from a genuine unexpected/network error.
apps/chat-api/src/app-config/feature-flags/feature-key.enum.ts/config-registry.constants.ts—SettingsPageEnabledfeature key (type: 'feature',visibility: 'client',defaultValue: false), driven bySETTINGS_PAGE_ENABLED, matching theexisting
scheduledTasksEnabledshape.libs/settings-panel(@epam/ai-dial-settings-panel) andlibs/usage-dashboard(
@epam/ai-dial-usage-dashboard) — both taggedtype:ui, purely presentational, no i18n, noapps/chat/src/server-api/*, no routing inside the lib; host-specific labels/icons/data arepassed in as props, per the repo's library isolation rules.
apps/chat/vite.config.mtsneeded aresolve.aliasentry for each new lib (pointing at itssrc/index.ts) — without it, a lib's CSS Modules/Tailwind-compiled styles never reach the appbundle; the dev server silently serves the library's stale prebuilt
dist/index.js.openspec/changes/archive/2026-08-19-settings-usage-page/,openspec/changes/archive/2026-08-19-settings-sidebar-panel/,openspec/changes/archive/2026-08-19-gate-settings-page-feature-flag/,openspec/changes/archive/2026-08-21-add-usage-dashboard-model-limits/(the Model limitsfollow-up covered above).
Confidential information