2026-07-14 12:21:33 +04:00
# Marketplace Admin Dashboard - Sprint 19
## Scope
Sprint 19 adds the production Admin Dashboard and makes it the default landing
page for the admin area. It also wires the previously-unrouted `admin/products`
feature and adds route placeholders for backoffice sections that don't have a
feature built yet.
## Routing
All admin routes live under `/:lang/backoffice/**` (`app.routes.ts` ), guarded
by the existing `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts` ):
```text
/:lang/backoffice -> redirects to dashboard
/:lang/backoffice/dashboard -> AdminDashboardPageComponent
/:lang/backoffice/products -> AdminProductsListPageComponent
/:lang/backoffice/products/create -> AdminProductEditorPageComponent
/:lang/backoffice/products/:id/edit -> AdminProductEditorPageComponent
/:lang/backoffice/products/:id/duplicate -> AdminProductEditorPageComponent
feat(admin): complete category management
Sprint 20. Adds features/admin/categories/ (model, gateway interface +
local gateway, facade, list/editor pages), mirroring the admin/products
container/facade/service split.
- indented hierarchy view + native HTML5 drag-and-drop reorder
- visibility toggle, item counter, empty state, include-deleted filter
- editor: slug uniqueness validation, translations, SEO fields, breadcrumb
preview, image via existing MediaPickerComponent
- soft delete/restore, blocked when a category has children or items
- draft/publish status + localStorage draft recovery (mirrors Project
Editor autosave) + CanDeactivate unsaved-changes guard
- wired into app.routes.ts (replaces the categories coming-soon placeholder)
- docs/ADMIN.md + docs/BACKEND.md updated with the new gap detail
Not yet done: admin/products' category dropdown still reads from its own
AdminProductsGateway.loadCategories() rather than this gateway (Sprint 21).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:34:13 +04:00
/:lang/backoffice/categories -> AdminCategoriesListPageComponent
/:lang/backoffice/categories/create -> AdminCategoryEditorPageComponent
/:lang/backoffice/categories/:id/edit -> AdminCategoryEditorPageComponent
2026-07-14 12:21:33 +04:00
/:lang/backoffice/static-pages -> BackofficeComingSoonPageComponent
/:lang/backoffice/transactions -> BackofficeComingSoonPageComponent
/:lang/backoffice/orders -> BackofficeComingSoonPageComponent
/:lang/backoffice/media -> BackofficeComingSoonPageComponent
```
`admin/products` (`features/admin/products/` ) was already fully implemented
in an earlier sprint but was never wired into `app.routes.ts` and its internal
navigation hardcoded the `ru` locale segment. Both are fixed in this sprint:
routes are wired, and `admin-products-list-page.component.ts` /
`admin-product-editor-page.component.ts` now build the locale segment from
`LanguageService.currentLanguage()` .
**Dashboard as default admin page:** on successful admin Telegram QR login,
`TelegramLoginComponent` (`mode="admin"` ) navigates to
`/:lang/backoffice/dashboard` (`components/telegram-login/telegram-login.component.ts` ).
The `backoffice` route's empty path also redirects to `dashboard` , so any bare
`/:lang/backoffice` link lands there too.
## Architecture
```text
src/app/features/admin/dashboard/
models/ admin-dashboard.model.ts
services/ admin-dashboard-metrics.gateway.interface.ts
admin-dashboard-metrics.local.gateway.ts
admin-dashboard-metrics-gateway.token.ts
admin-dashboard-history.service.ts
facade/ admin-dashboard.facade.ts
components/ admin-dashboard-card.component.*
admin-dashboard-quick-actions.component.*
admin-dashboard-activity.component.*
admin-dashboard-health.component.*
pages/ admin-dashboard-page.component.*
src/app/features/backoffice/shared/
backoffice-coming-soon-page.component.*
```
Follows the existing container/facade/service split (ADR-006, ADR-007):
`AdminDashboardPageComponent` is the container, `AdminDashboardFacade` owns
orchestration, presentational card/quick-actions/activity/health components
take only `@Input()` s and have no HttpClient/localStorage/route access.
### Data sources (future-ready)
Cards never read `ConfigService` , `localStorage` , or an HTTP client directly -
everything routes through `AdminDashboardFacade` , which composes:
- **`ProjectEditorFacade` ** (already existed) - `bootstrap` , `status` ,
`lastSavedAt` , `lastPublishedAt` (new, see below), `validationIssues` ,
`homepageWidgets` . Backs Marketplace Status, Project Name, Current Theme,
Languages, Last Publish, Last Draft Save, Bootstrap Version, Active Layout,
Enabled Widgets, and the System Health checks.
- **`ADMIN_DASHBOARD_METRICS_GATEWAY` ** (new `InjectionToken` , same swap
pattern as `BACKOFFICE_DATA_PROVIDER` ) - defaults to
`AdminDashboardMetricsLocalGateway` , which composes
`BackofficeDataService.loadCategories()/loadProducts()` (already used by
`AdminProductsLocalGateway` ) into counts. Backs Categories Count and
Products Count. Swapping to a real dashboard-metrics endpoint later means
implementing `AdminDashboardMetricsGateway` and rebinding the token - the
facade and cards don't change.
- **`AdminDashboardHistoryService` ** (new) - localStorage-backed activity log,
scoped per tenant, same pattern as `ProjectEditorDraftStorageService` . The
facade appends an entry whenever `lastSavedAt` /`lastPublishedAt` change
(detected via an `effect()` , primed on first read so the initial bootstrap
load doesn't get logged as an activity event). Backs Recent Activity.
### Orders / Revenue
No backend or local data model exists for orders or revenue anywhere in the
codebase (`features/backoffice/orders` is an empty placeholder folder). These
two cards render an honest * * `pending-backend` ** card state ("Awaiting backend
integration") rather than fabricated numbers - not a "no data" empty state,
since the gap is structural, not a temporarily-empty dataset.
### Card states
`AdminDashboardCardComponent` (`components/admin-dashboard-card.component.ts` )
renders one of: `loading` (skeleton), `empty` , `error` , `pending-backend` , or
the ready value + optional subtitle. The container computes each card's status
per data source (bootstrap not yet loaded -> `loading` ; metrics gateway error
-> `error` ; no supported locales -> `empty` ; Orders/Revenue -> always
`pending-backend` ).
### System Health
`ProjectValidator` (`features/project-editor/services/project-validator.service.ts` )
already covered 5 of the 6 required checks. This sprint added two more:
- `translationIssues()` - flags a supported non-default locale missing a
header nav label translation or a static-page `translations` entry.
- `layoutIssues()` - flags `bootstrap.layout.type` or any section's
`layout.strategy` that isn't one of the known enum values
(`PlatformLayoutType` / `SectionLayoutStrategy` ). Runtime validation matters
here because bootstrap JSON isn't type-checked at load time.
Dashboard mapping (`AdminDashboardFacade.healthChecks` ):
| Dashboard label | Validator code |
|---|---|
| Bootstrap valid | structural: `bootstrap !== null && schemaVersion` set |
| Configuration valid | no validation issues at all |
| Missing translations | `missing-translations` (new) |
| Invalid colors | `invalid-colors` (existing) |
| Invalid widget references | `missing-widget` (existing - a homepage widget with no `type` ) |
| Invalid layouts | `invalid-layouts` (new) |
### Quick Actions
Static list in `AdminDashboardFacade` (`route` arrays relative to the lang
root); the page component prefixes the current locale
(`LanguageService.currentLanguage()` ) before binding `routerLink` . Categories,
Static Pages, Transactions, Orders, and Media Library currently land on
`BackofficeComingSoonPageComponent` since those features aren't built yet -
this is a routing placeholder, not a dashboard card placeholder.
### `lastPublishedAt` (ProjectEditorFacade change)
Before this sprint, `publish()` only updated `lastSavedAt` , so "last draft
save" and "last publish" were indistinguishable after a publish. Added
`lastPublishedAt: number | null` to `ProjectEditorState` /
`ProjectEditorFacade` , set only inside `publish()` . `lastSavedAt` behavior is
unchanged (still updated by both `save()` and `publish()` ).
feat(admin): complete category management
Sprint 20. Adds features/admin/categories/ (model, gateway interface +
local gateway, facade, list/editor pages), mirroring the admin/products
container/facade/service split.
- indented hierarchy view + native HTML5 drag-and-drop reorder
- visibility toggle, item counter, empty state, include-deleted filter
- editor: slug uniqueness validation, translations, SEO fields, breadcrumb
preview, image via existing MediaPickerComponent
- soft delete/restore, blocked when a category has children or items
- draft/publish status + localStorage draft recovery (mirrors Project
Editor autosave) + CanDeactivate unsaved-changes guard
- wired into app.routes.ts (replaces the categories coming-soon placeholder)
- docs/ADMIN.md + docs/BACKEND.md updated with the new gap detail
Not yet done: admin/products' category dropdown still reads from its own
AdminProductsGateway.loadCategories() rather than this gateway (Sprint 21).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:34:13 +04:00
## Sprint 20 - Category Management
`features/admin/categories/` (model/gateway/facade/pages/components), same
container/facade/service split as `admin/products` and `admin/dashboard` :
```text
src/app/features/admin/categories/
models/ admin-category.model.ts
services/ admin-categories-gateway.interface.ts
admin-categories-local.gateway.ts
admin-categories-form.factory.ts
facade/ admin-categories.facade.ts
guards/ admin-category-dirty.guard.ts
components/ admin-categories-list.component.*
admin-category-form.component.*
pages/ admin-categories-list-page.component.ts
admin-category-editor-page.component.ts
```
- **Hierarchy**: `AdminCategory.parentId` (nullable). List page renders a
flattened, indented tree (`AdminCategoriesFacade.rootCategories()` /
`childrenOf(id)` ); the editor's parent `<select>` excludes the category
itself and its descendants to prevent cycles.
- **Reordering**: native HTML5 drag-and-drop in
`admin-categories-list.component.ts` (`draggable` , `dragstart` /`drop` ),
persists via `AdminCategoriesFacade.reorder()` which just rewrites `order` .
- **Delete/restore**: soft delete (`deletedAt` timestamp). Blocked
client-side (`facade.canDelete()` ) if the category has children or
`itemsCount > 0` ; list has an "include deleted" filter with a Restore
action for soft-deleted rows.
- **Draft/publish**: `status: 'draft' | 'published'` , set by the editor's
"Save Draft" vs "Publish" buttons (`AdminCategoriesFacade.saveDraft(publish)` ).
- **Local draft recovery + unsaved-changes guard**: every `updateDraft()`
call persists the in-progress category to `localStorage` under
`admin-category-draft:<id>` (via the existing `LocalStorageService` ,
same pattern as Project Editor autosave); the editor reloads that draft
ahead of the saved value if present, and is cleared on save.
`adminCategoryDirtyGuard` (mirrors `projectEditorDirtyGuard` ) blocks
navigation away from an unsaved edit with `window.confirm` .
- **Image**: reuses the existing `MediaPickerComponent` (same one used by
Media Manager) rather than a free-text URL field.
- **Seed data**: `AdminCategoriesLocalGateway` seeds its in-memory cache from
`BackofficeDataService.loadCategories()` (`CategoryCardConfig` , currently
flat/no hierarchy) - same swappable-provider pattern as
`AdminProductsLocalGateway` .
- **Not yet wired**: `admin/products` ' category `<select>` still uses
`AdminProductsGateway.loadCategories()` (its own `AdminProductCategoryOption`
seed), not `AdminCategoriesGateway` - unifying them is Sprint 21 scope
(`docs/SPRINT-PLAN.md` ).
2026-07-15 10:31:00 +04:00
## Sprint 21 - Product Management completion
- **Categories now real**: `AdminProductsLocalGateway` seeds its category dropdown from `AdminCategoriesLocalGateway.loadCategories()` (Sprint 20) instead of raw `BackofficeDataService.loadCategories()` - product `categoryId` now points at real admin-managed categories.
- **Archive/restore**: `AdminProduct.archived` (soft, distinct from `visible` ). List has an "include archived" filter + per-row Archive/Restore action; archived products excluded by default (mirrors categories' `deletedAt` /restore pattern).
- **Barcode**: added alongside `sku` .
- **Variants**: lightweight `AdminProductVariant[]` (`name` /`price` /`quantity` ), edited as `name|price|quantity` lines (same textarea-parse convention as `specifications` /`attributes` ). Not a full options-matrix variant system - scoped to what the model/backend contract actually needs today.
- **Related products**: `relatedProductIds: string[]` , checkbox picker in the editor sourced from `AdminProductFormComponent` 's `allProducts` input - which is `AdminProductsFacade.products()` , i.e. whatever page is currently loaded in the facade (usually primed by navigating from the list). Not a full catalog search; fine for the current mock-data scale, worth revisiting if `AdminProductsLocalGateway` is ever swapped for a real API with more than a page of products.
- **Gallery**: `media.gallery` now built via the shared `MediaPickerComponent` (add/remove thumbnails) instead of a raw URL textarea; `media.images` /`media.videos` unchanged (still textarea, out of this ticket's scope).
- **Preview**: simple read-only line in the editor showing computed discounted price.
- **Infinite scroll**: `AdminProductsFacade.infiniteScroll` toggle - when on, `loadMore()` appends the next page to `products()` instead of replacing it; pagination UI swaps for a "Load more" button. Off by default (existing paginated behavior unchanged).
2026-07-15 10:42:23 +04:00
## Sprint 22 - Media System hardening
`core/media/` (`MediaRepository` abstraction, `MockMediaRepository` IndexedDB
implementation) + `features/backoffice/media/` + the shared
`shared/media/media-picker/` :
- **Folders**: flat `folder?: string` tag on `MediaAsset` (no nesting) -
"New folder" just sets the active filter to a name typed via
`window.prompt` (mirrors the `window.confirm` pattern already used for
destructive actions elsewhere); the folder is created implicitly the next
time something uploads into it. `MediaRepository.listFolders()` derives
the folder list from existing records rather than a separate folder
entity - intentionally light, matches the flat-storage reality of an
IndexedDB mock.
- **Tags**: already existed on `MediaAsset` ; added an edit affordance
(`window.prompt` , comma-separated) and `MediaLibraryFacade.updateTags()` .
- **Validation**: `MockMediaRepository.validateFile()` rejects anything over
10MB or outside the allow-list (`jpeg/png/webp/gif/svg+xml/pdf` ); errors
now propagate as real messages through `MediaLibraryFacade.error` (both
`media-library-page` and `media-picker` display it - previously upload
failures were swallowed into a generic string).
- **SVG sanitization**: `sanitizeSvg()` strips `<script>` tags and
`on*="..."` attributes from uploaded SVG markup before storing it, since
SVG is the one accepted format that can carry inline script.
- **Compression/resize**: raster images (not SVG/GIF) are downscaled to a
2000px max dimension and re-encoded (JPEG/PNG, quality 0.85) via
`<canvas>` before being stored - client-side only, no crop UI. A full
interactive cropper was out of scope for this ticket; revisit if a real
design need for manual cropping shows up.
- **Reuse confirmed**: `MediaPickerComponent` is now wired into Category
images (Sprint 20), Product gallery (Sprint 21), and Project Editor
branding (logo / compact logo / favicon, this sprint) - one media library
for the whole platform, per the sprint goal. Static Pages editor has no
image fields to wire (confirmed, not a gap). Hero image: no dedicated
hero-image field exists in `BootstrapConfig` today - nothing to wire.
- **Storage abstraction**: already existed via `MediaRepository` (abstract
class + DI token `providedIn: 'root'` on `MockMediaRepository` ) - swapping
to a real CDN/backend means implementing `MediaRepository` against a real
API and rebinding the provider; no consumer (`MediaLibraryFacade` ,
`MediaPickerComponent` , or any of the pickers above) changes.
## Sprint 22 - Media System hardening
`core/media/` (`MediaRepository` abstraction, `MockMediaRepository` IndexedDB
implementation) + `features/backoffice/media/` + the shared
`shared/media/media-picker/` :
- **Folders**: flat `folder?: string` tag on `MediaAsset` (no nesting) -
"New folder" just sets the active filter to a name typed via
`window.prompt` (mirrors the `window.confirm` pattern already used for
destructive actions elsewhere); the folder is created implicitly the next
time something uploads into it. `MediaRepository.listFolders()` derives
the folder list from existing records rather than a separate folder
entity - intentionally light, matches the flat-storage reality of an
IndexedDB mock.
- **Tags**: already existed on `MediaAsset` ; added an edit affordance
(`window.prompt` , comma-separated) and `MediaLibraryFacade.updateTags()` .
- **Validation**: `MockMediaRepository.validateFile()` rejects anything over
10MB or outside the allow-list (`jpeg/png/webp/gif/svg+xml/pdf` ); errors
now propagate as real messages through `MediaLibraryFacade.error` (both
`media-library-page` and `media-picker` display it - previously upload
failures were swallowed into a generic string).
- **SVG sanitization**: `sanitizeSvg()` strips `<script>` tags and
`on*="..."` attributes from uploaded SVG markup before storing it, since
SVG is the one accepted format that can carry inline script.
- **Compression/resize**: raster images (not SVG/GIF) are downscaled to a
2000px max dimension and re-encoded (JPEG/PNG, quality 0.85) via
`<canvas>` before being stored - client-side only, no crop UI. A full
interactive cropper was out of scope for this ticket; revisit if a real
design need for manual cropping shows up.
- **Reuse confirmed**: `MediaPickerComponent` is now wired into Category
images (Sprint 20), Product gallery (Sprint 21), and Project Editor
branding (logo / compact logo / favicon, this sprint) - one media library
for the whole platform, per the sprint goal. Static Pages editor has no
image fields to wire (confirmed, not a gap). Hero image: no dedicated
hero-image field exists in `BootstrapConfig` today ('hero' only appears
as a `SectionLayoutStrategy` enum value) - nothing to wire.
- **Storage abstraction**: already existed via `MediaRepository` (abstract
class + DI token `providedIn: 'root'` on `MockMediaRepository` ) - swapping
to a real CDN/backend means implementing `MediaRepository` against a real
API and rebinding the provider; no consumer (`MediaLibraryFacade` ,
`MediaPickerComponent` , or any of the pickers above) changes.
feat(admin): order management
Sprint 23.
New features/admin/orders/ module, same container/facade/service split as
admin/products and admin/categories.
- AdminOrder model + AdminOrdersLocalGateway seeding 24 deterministic
synthetic orders (no real order data source exists anywhere in this
repo - explicitly a placeholder, not a mock of production volume)
- list: search, status filter, pagination, CSV export (client-side Blob
download)
- detail: customer/payment/shipping, itemized total, status timeline,
change-status dropdown, refund request + cancel (window.confirm-gated),
separate customer-facing vs internal notes, print invoice via
window.print() with @media print hiding non-invoice chrome
- wired into /:lang/backoffice/orders(/:id), replacing the coming-soon
placeholder
docs/ADMIN.md + docs/BACKEND.md updated; dashboard's Orders/Revenue cards
(Sprint 19) remain intentionally un-wired to this mock and still render
pending-backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 10:50:59 +04:00
## Sprint 23 - Orders (mock/local)
`features/admin/orders/` (model/gateway/facade/pages), same
container/facade/service split as the rest of `admin/*` :
- **No real data source exists for orders anywhere in this repo** (already
called out in Sprint 19's dashboard gap and `docs/BACKEND.md` item 7) -
`AdminOrdersLocalGateway` seeds 24 deterministic synthetic orders in
memory (cycling through all statuses/customers) rather than reading from
`BackofficeDataService` , since there is nothing there to read. This is
explicitly a placeholder to unblock the admin UI, not a real mock of
production order volume.
- List: search (order number/customer/email), status filter, pagination,
CSV export (client-side `Blob` download, no server round-trip).
- Detail: customer/payment/shipping info, itemized line items + total,
status timeline, change-status dropdown, refund request and cancel
(both `window.confirm` -gated), customer-visible notes vs internal-only
notes (two separate free-text logs), print invoice via `window.print()`
with a `@media print` rule hiding all non-invoice chrome (`.no-print` ) -
no PDF generation library, deliberately minimal.
- Wired into `/ :lang/backoffice/orders` and `/ :lang/backoffice/orders/:id` ,
replacing the coming-soon placeholder.
2026-07-15 10:57:29 +04:00
## Sprint 24 - Transactions (mock/local)
`features/admin/transactions/` . `AdminTransactionsLocalGateway` derives its
mock data from `AdminOrdersLocalGateway` 's 24 seeded orders (one
transaction per order, deterministic type/status/method assignment) rather
than a separate synthetic dataset - keeps order numbers/totals consistent
between the two mock feature areas.
- List: search, status filter, type filter (payment/refund/qr_payment),
pagination, CSV export.
- Retry failed transactions (`status: 'failed' -> 'retried'` , appends an
audit entry).
- Fraud flag toggle per transaction.
- Audit log: each transaction carries its own `audit: AdminTransactionAuditEntry[]`
(creation, retries, fraud-flag changes), viewed via a dialog - this is a
per-transaction audit trail, not the system-wide audit/security log
planned for Sprint 26 (Monitoring); the two are intentionally separate
scopes.
- Wired into `/ :lang/backoffice/transactions` , replacing the coming-soon
placeholder.
feat(admin): order management
Sprint 23.
New features/admin/orders/ module, same container/facade/service split as
admin/products and admin/categories.
- AdminOrder model + AdminOrdersLocalGateway seeding 24 deterministic
synthetic orders (no real order data source exists anywhere in this
repo - explicitly a placeholder, not a mock of production volume)
- list: search, status filter, pagination, CSV export (client-side Blob
download)
- detail: customer/payment/shipping, itemized total, status timeline,
change-status dropdown, refund request + cancel (window.confirm-gated),
separate customer-facing vs internal notes, print invoice via
window.print() with @media print hiding non-invoice chrome
- wired into /:lang/backoffice/orders(/:id), replacing the coming-soon
placeholder
docs/ADMIN.md + docs/BACKEND.md updated; dashboard's Orders/Revenue cards
(Sprint 19) remain intentionally un-wired to this mock and still render
pending-backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 10:50:59 +04:00
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
## Sprint 25 - Users & Roles (mock/local)
`features/admin/users/` . Single consolidated page (`admin-users-page` ) at
`/ :lang/backoffice/users` - not previously in the Quick Actions list or
routes at all, this is a net-new admin section.
- **Users**: name, Telegram username, `scope` (`marketplace` vs `office`
admin - distinguishes tenant-level owners/admins from internal staff),
role, status (`active` /`invited` /`suspended` ), last login. Role change is
an inline `<select>` ; suspend/reactivate is confirm-gated for suspend
only.
- **Roles/permissions**: 4 built-in roles (`owner` /`admin` /`editor` /`viewer` )
with a flat permission-string list (`products.manage` , `*` for owner,
etc.) - a real permission catalog and custom-role creation don't exist,
intentionally scoped down to what's needed to demonstrate the model.
- **Invitations**: email + role + scope form, pending list with revoke.
No email actually sends - `AdminUsersLocalGateway.inviteUser()` only
creates the local record.
- **Passwordless login**: already existed before this sprint -
`AdminAuthService` 's Telegram QR flow (`docs/ADMIN.md` 's existing admin
login section, `docs/BACKEND.md` item 1). This sprint's Users page links
to it via a hint, doesn't reimplement it.
- **Session manager / device manager**: per-user session list (device, IP,
last active, current-session badge) with per-session revoke, mocked
(`AdminUsersLocalGateway.loadSessions()` fabricates 2 sessions per user
on first view) - the real `AdminAuthService` /session-cookie flow only
ever tracks the * current * browser's session, so multi-device session
listing has no real backend counterpart yet (see `docs/BACKEND.md` item 1).
- **Audit**: per-user audit log (role/status changes), same dialog pattern
as Sprint 24's per-transaction audit - not the system-wide security/audit
log planned for Sprint 26.
- Wired into `AdminDashboardFacade` 's Quick Actions list (`dashboard.actionUsers`
-> `/ :lang/backoffice/users` ).
2026-07-15 11:12:47 +04:00
## Sprint 26 - Monitoring (mock/local, health reuses real data)
`features/admin/monitoring/` , single page at `/ :lang/backoffice/monitoring`
(new Dashboard Quick Action).
- **Health**: reuses `AdminDashboardFacade.healthChecks` directly (the same
real, non-mocked bootstrap-validation checks from Sprint 19's dashboard)
instead of duplicating the logic - this is the one section on this page
backed by real data.
- **Audit / security / login / failed-login / API / error / warning
events**: one unified `AdminMonitoringEvent` feed (`category` + `level`
discriminators) with category filter + search, seeded with 40
deterministic synthetic entries by `AdminMonitoringLocalGateway` - no
logging backend exists anywhere in this system, so there is nothing real
to read from.
- **Queue monitoring**: 3 mock named queues with depth + status.
- **Webhook monitoring**: mock delivery log (endpoint/event/status/time).
- This is deliberately a separate, system-wide log from the two
narrower-scoped audit trails added earlier: Sprint 24's per-transaction
audit and Sprint 25's per-user audit. No consolidation attempted - they
track different things.
2026-07-14 12:21:33 +04:00
## Known gaps / backend needs
- **Dashboard metrics endpoint.** Categories/Products counts are computed
client-side from `BackofficeDataService` (itself mock/API-switchable via
`BACKOFFICE_DATA_PROVIDER` ). A dedicated `/builder/dashboard/summary` -style
endpoint would let `AdminDashboardMetricsGateway` return richer data
(real-time counts, trend deltas) without touching the facade or cards.
- **Orders/Revenue have no backend at all** (see above) - needs an order
domain and revenue aggregation before these cards can show real data.
- **Recent Activity is local-only**, scoped to the browser/tenant via
localStorage (`adminDashboard.activityHistory.v1` ), same limitation as the
existing draft-save local storage. It will not show another editor's
activity until a real audit-log endpoint exists.
- **Admin authorization is still not enforced server-side** (see
`Project-Editor.md` - "Admin Authentication" section); this sprint does not
change that. Nothing new here beyond routing/dashboard.