Files
marketplaces/docs/ADMIN.md
sdarbinyan 576f2600a5 refactor: marketplace release polish
Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).

- a11y: aria-label added to every bare <select> not already inside a
  <label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
  previously rendered blank during the initial fetch (categories, orders,
  transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
  SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
  present at Sprint 20's first build before any admin/* code existed,
  and new admin pages are all lazy-loaded — documented as out of scope
  for this pass rather than chased

docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 00:14:46 +04:00

465 lines
26 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
/:lang/backoffice/categories -> AdminCategoriesListPageComponent
/:lang/backoffice/categories/create -> AdminCategoryEditorPageComponent
/:lang/backoffice/categories/:id/edit -> AdminCategoryEditorPageComponent
/: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()`).
## 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`).
## 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).
## 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.
## 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.
## 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.
## 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`).
## 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.
## Sprint 27 - Analytics
`features/admin/analytics/`, net-new `/:lang/backoffice/analytics` route
+ Dashboard Quick Action.
- **Real, derived data**: revenue/orders/avg-order-value/sales-over-time
chart/top-products are computed by composing the existing
`AdminOrdersLocalGateway` (Sprint 23's seeded mock orders) - not a
separate fabricated dataset. Products/Categories counts come from
`AdminProductsLocalGateway`/`AdminCategoriesLocalGateway`. All of this is
still ultimately backed by mock order/product/category data (per those
sprints), but the *aggregation* is real arithmetic over that data, not
invented numbers.
- **Visitors, funnels, heatmaps**: no analytics/tracking pipeline exists
anywhere in this codebase, so these render an explicit
"Awaiting backend integration" (`pending-backend`) badge, same convention
as the Sprint 19 dashboard's Orders/Revenue cards before Sprint 23 -
not fabricated numbers, not a generic empty state.
- **Chart**: a plain inline `<div>`-bar chart driven by `[style.height.%]`,
no charting library pulled in - reasonable for one sales-over-time series
at this scale; revisit if more chart types are actually needed.
- **Date ranges**: 7/30/90-day toggle filters orders by `createdAt`.
- **Export**: CSV of the sales series (client-side `Blob` download, same
pattern as Orders/Transactions).
## Sprint 28 - Marketplace Polish (scoped to admin/*)
Scoped down (user decision) from a full marketplace-wide audit to the 8
admin features built in Sprints 20-27, since those were the newest and
least-reviewed surface. Not a rewrite - targeted fixes only:
- **Accessibility**: every bare `<select>` across `admin/categories`,
`admin/products`, `admin/orders`, `admin/transactions`, `admin/users`,
and `admin/monitoring` that wasn't already inside a `<label>` (which
provides implicit association) now has an explicit `aria-label`. Selects
already nested in `<label>` (e.g. product form's category/stock-status
selects, category form's parent select) were left as-is - already
correct.
- **Loading states**: list pages that previously rendered a blank table
during the initial fetch (categories, orders, transactions, users,
monitoring's event feed, analytics' summary cards) now show
`app-skeleton` rows/cards while `loading()` is true, falling through to
the existing empty-state or populated table after. `admin/products` and
the media library already had this from earlier sprints.
- **Bundle size**: the `700 kB` initial-bundle budget warning (~198 kB over)
predates every admin sprint in this plan - it was already present at
Sprint 20's first build, before any of `features/admin/**` existed, and
the new admin pages are all lazy-loaded (they don't touch the initial
chunk). Confirmed out of scope for an admin-only polish pass; would need
a main-bundle/core-module audit to actually fix.
- Explicitly **not done** here (would require the "full marketplace audit"
scope the user declined): Lighthouse run, animations, responsive fixes
outside admin/*, SEO/meta/social-preview/robots/sitemap, and a full a11y
audit of the public storefront/Project Editor.
## 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.