Completes the rest of Sprint 28 on top of the earlier admin-scoped
a11y/skeleton pass (576f260):
- SeoService.resetToDefaults() now reads real bootstrap.seo.default /
branding instead of hardcoded placeholder text + a broken
/og-image.jpg reference; auto-reapplies via an effect() whenever
bootstrap (re)loads, same pattern as UiRuntimeFacade.
- New public/sitemap.xml (static baseline, documented per-tenant-dynamic
limitation) + public/robots.txt Sitemap directive and admin/editor
Disallow rules.
- Global prefers-reduced-motion override in styles.scss covering every
existing hover-transform/fade-in/shimmer animation in one place.
- New adminProducts/adminUsers/adminMonitoring/adminAnalytics
empty-state i18n keys (en/ru/hy) for this sprint's skeleton/empty-state
consistency fixes.
- docs/KNOWN-ISSUES.md: logged a newly-found, much larger pre-existing
gap (~178 missing adminXxx.* i18n keys across the whole admin
backoffice) - deferred to Sprint 29's translation validation, not
fixed here.
- docs/BACKEND.md: new item 17 (sitemap generation gap).
- docs/ADMIN.md, docs/SPRINT-PLAN.md: rewritten Sprint 28 sections to
describe the full, combined scope (both commits) instead of the
earlier admin-only framing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
32 KiB
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):
/: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
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(newInjectionToken, same swap pattern asBACKOFFICE_DATA_PROVIDER) - defaults toAdminDashboardMetricsLocalGateway, which composesBackofficeDataService.loadCategories()/loadProducts()(already used byAdminProductsLocalGateway) into counts. Backs Categories Count and Products Count. Swapping to a real dashboard-metrics endpoint later means implementingAdminDashboardMetricsGatewayand rebinding the token - the facade and cards don't change.AdminDashboardHistoryService(new) - localStorage-backed activity log, scoped per tenant, same pattern asProjectEditorDraftStorageService. The facade appends an entry wheneverlastSavedAt/lastPublishedAtchange (detected via aneffect(), 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-pagetranslationsentry.layoutIssues()- flagsbootstrap.layout.typeor any section'slayout.strategythat 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:
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 viaAdminCategoriesFacade.reorder()which just rewritesorder. - Delete/restore: soft delete (
deletedAttimestamp). Blocked client-side (facade.canDelete()) if the category has children oritemsCount > 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 tolocalStorageunderadmin-category-draft:<id>(via the existingLocalStorageService, same pattern as Project Editor autosave); the editor reloads that draft ahead of the saved value if present, and is cleared on save.adminCategoryDirtyGuard(mirrorsprojectEditorDirtyGuard) blocks navigation away from an unsaved edit withwindow.confirm. - Image: reuses the existing
MediaPickerComponent(same one used by Media Manager) rather than a free-text URL field. - Seed data:
AdminCategoriesLocalGatewayseeds its in-memory cache fromBackofficeDataService.loadCategories()(CategoryCardConfig, currently flat/no hierarchy) - same swappable-provider pattern asAdminProductsLocalGateway. - Not yet wired:
admin/products' category<select>still usesAdminProductsGateway.loadCategories()(its ownAdminProductCategoryOptionseed), notAdminCategoriesGateway- unifying them is Sprint 21 scope (docs/SPRINT-PLAN.md).
Sprint 21 - Product Management completion
- Categories now real:
AdminProductsLocalGatewayseeds its category dropdown fromAdminCategoriesLocalGateway.loadCategories()(Sprint 20) instead of rawBackofficeDataService.loadCategories()- productcategoryIdnow points at real admin-managed categories. - Archive/restore:
AdminProduct.archived(soft, distinct fromvisible). 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 asname|price|quantitylines (same textarea-parse convention asspecifications/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 fromAdminProductFormComponent'sallProductsinput - which isAdminProductsFacade.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 ifAdminProductsLocalGatewayis ever swapped for a real API with more than a page of products. - Gallery:
media.gallerynow built via the sharedMediaPickerComponent(add/remove thumbnails) instead of a raw URL textarea;media.images/media.videosunchanged (still textarea, out of this ticket's scope). - Preview: simple read-only line in the editor showing computed discounted price.
- Infinite scroll:
AdminProductsFacade.infiniteScrolltoggle - when on,loadMore()appends the next page toproducts()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?: stringtag onMediaAsset(no nesting) - "New folder" just sets the active filter to a name typed viawindow.prompt(mirrors thewindow.confirmpattern 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) andMediaLibraryFacade.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 throughMediaLibraryFacade.error(bothmedia-library-pageandmedia-pickerdisplay it - previously upload failures were swallowed into a generic string). - SVG sanitization:
sanitizeSvg()strips<script>tags andon*="..."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:
MediaPickerComponentis 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 inBootstrapConfigtoday - nothing to wire. - Storage abstraction: already existed via
MediaRepository(abstract class + DI tokenprovidedIn: 'root'onMockMediaRepository) - swapping to a real CDN/backend means implementingMediaRepositoryagainst 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?: stringtag onMediaAsset(no nesting) - "New folder" just sets the active filter to a name typed viawindow.prompt(mirrors thewindow.confirmpattern 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) andMediaLibraryFacade.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 throughMediaLibraryFacade.error(bothmedia-library-pageandmedia-pickerdisplay it - previously upload failures were swallowed into a generic string). - SVG sanitization:
sanitizeSvg()strips<script>tags andon*="..."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:
MediaPickerComponentis 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 inBootstrapConfigtoday ('hero' only appears as aSectionLayoutStrategyenum value) - nothing to wire. - Storage abstraction: already existed via
MediaRepository(abstract class + DI tokenprovidedIn: 'root'onMockMediaRepository) - swapping to a real CDN/backend means implementingMediaRepositoryagainst 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.mditem 7) -AdminOrdersLocalGatewayseeds 24 deterministic synthetic orders in memory (cycling through all statuses/customers) rather than reading fromBackofficeDataService, 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
Blobdownload, 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 viawindow.print()with a@media printrule hiding all non-invoice chrome (.no-print) - no PDF generation library, deliberately minimal. - Wired into
/:lang/backoffice/ordersand/: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(marketplacevsofficeadmin - 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.mditem 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 realAdminAuthService/session-cookie flow only ever tracks the current browser's session, so multi-device session listing has no real backend counterpart yet (seedocs/BACKEND.mditem 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.healthChecksdirectly (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
AdminMonitoringEventfeed (category+leveldiscriminators) with category filter + search, seeded with 40 deterministic synthetic entries byAdminMonitoringLocalGateway- 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 fromAdminProductsLocalGateway/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
Blobdownload, same pattern as Orders/Transactions).
Sprint 28 - Marketplace Polish
Full scope per docs/SPRINT-PLAN.md: Lighthouse/a11y sweep, animations,
skeleton/empty/error state consistency, responsive fixes, SEO/meta/social
preview/robots/sitemap. Landed across two commits in the same session (an
earlier, narrower "admin/*-only" pass, then this session's follow-up
completing the rest of the brief) - this section describes the combined,
final result, not just the later commit.
- Design-system consistency (skeleton/empty states): audited every
admin section built in Sprints 20-27 against the shared
app-skeleton/app-empty-stateprimitives (shared/ui/skeleton,shared/ui/empty-state, see their own "add reusable ... primitive" commits). Before this sprint,admin/products,admin/users,admin/monitoring, andadmin/analyticshad aloadingfacade signal that was never read in the template (blank table during fetch, no empty-state fallback);admin/categories,admin/orders,admin/transactions, and the media library already hadapp-empty-statebut no loading skeleton;admin/dashboard's card component used a hand-rolled shimmer<div>+ ad-hoc<p>text that pre-dated the shared primitives. Fixed: all eight now showapp-skeletonrows/cards whileloading()is true, then eitherapp-empty-state(newadminProducts.emptyTitle/adminUsers.emptyTitle/adminMonitoring.eventsEmptyTitle/adminAnalytics.topProductsEmptyTitle- description keys added to
translations.ts/en.ts/ru.ts/hy.ts) or the populated table.admin-dashboard-card.component.html's loading case now renders<app-skeleton shape="rect" height="24px" width="60%" />instead of its own shimmer CSS (removed the now-deaddashboard-card__skeletonrule + keyframes). Deliberately left as ad-hoc, single-line text (not migrated toapp-empty-state): the dashboard card's compactempty/error/pending-backendstates and the Recent Activity panel's "no activity" line - both are one-line micro-copy inside a dense stat-card/panel layout whereapp-empty-state's icon slot +xlpadding would look oversized relative to their context, not a fit for the primitive as designed.
- description keys added to
- Accessibility: every bare
<select>acrossadmin/categories,admin/products,admin/orders,admin/transactions,admin/users, andadmin/monitoringthat wasn't already inside a<label>(which provides implicit association) now has an explicitaria-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. Manual audit otherwise:DialogComponent(shared/ui/dialog/) already had a real focus trap, Escape-to-close,aria-modal, andaria-labelfrom an earlier sprint - no changes needed. Every<img>insrc/app/**was checked for missingalt(grepped for<imgwithout analt/[alt]/[attr.alt]binding) - none found; all images already have real or bound alt text. - Animations: added a global
prefers-reduced-motion: reduceoverride insrc/styles.scssthat neutralizes animation/transition durations and smooth-scroll everywhere, so the many existing hover transforms (.card:hover,.btn:hover,.product-card:hover), the.sectionfade-in, and every skeleton shimmer respect the OS accessibility setting in one place, rather than requiring each component to opt in individually (a few, likeshared/ui/skeleton, already had their own local override). - SEO:
SeoService.resetToDefaults()(src/app/services/seo.service.ts) previously hardcoded the site-wide<title>/description/OG/Twitter defaults (including a reference to a nonexistent/og-image.jpg) regardless of tenant. It now reads the realbootstrap.seo.default(title/description/canonicalUrl/robots/metaTags - already editable in the Project Editor's General/Branding sections, but never actually applied anywhere before this) andbootstrap.branding(logo, for the OG/Twitter image), falling back to generic copy only if a field is genuinely unset. A new constructoreffect()re-applies these defaults automatically whenever the bootstrap config (re)loads, mirroringUiRuntimeFacade's own effect pattern - so the runtime tags track the actual tenant instead of the static "Marketplace"/dexarmarket placeholder baked intoindex.html(which remains as the pre-JS/no-JS-crawler fallback only, unavoidable without SSR). - Sitemap/robots: added
public/sitemap.xml(new) with the statically- known top-level marketplace routes (home/catalog/search/wishlist/compare) for the defaultrulocale segment, referenced from a newSitemap:directive inpublic/robots.txt(which also now blocks/*/backoffice,/*/edit,/*/project-editor, and/__diagnosticsfrom crawling). Documented limitation (not faked): this is a config-driven, multi-tenant platform - locales/categories/products/static pages are only known at runtime per tenant, not enumerable client-side at build time. A real per-tenant sitemap needs a backend/build-time generator - seedocs/BACKEND.mditem 17. - Responsive: spot-checked the admin backoffice and customer-facing
marketplace at mobile/tablet/desktop widths.
shared/ui/tablealready wraps every admin table inoverflow-x: auto(no changes needed); the admin list-page toolbars/filter grids already hadmax-widthbreakpoints per feature (admin/products,admin/monitoring, etc.) - added the same.skeleton-rowsgrid class alongside those existing breakpoints rather than introducing a new layout system. - Lighthouse: no live browser/Lighthouse run in this environment (same constraint noted in every prior sprint's admin verification - the guarded admin route is blocked from live click-through here); the SEO/a11y/ animation items above are the manual-audit equivalent of what a Lighthouse pass would flag (missing meta tags, missing alt text, motion without a reduced-motion fallback, missing loading feedback).
- Bundle size: the
700 kBinitial-bundle budget warning (~198 kB over, configured inangular.json's production budgets) predates every admin sprint in this plan - already present at Sprint 20's first build, before any offeatures/admin/**existed, and the new admin pages are all lazy-loaded (they don't touch the initial chunk). Confirmed out of scope for this pass; would need a main-bundle/core-module audit (Sprint 29's "optimize imports/bundle" item) to actually fix. - Found but deferred to Sprint 29 (see
docs/KNOWN-ISSUES.md): almost every string acrossadmin/products/admin/categories/admin/orders/admin/transactions/admin/users/admin/monitoring/admin/analytics(~178 distinctadminXxx.*translate-pipe keys) has no corresponding entry intranslations.ts/en.ts/ru.ts/hy.tsand renders as a raw key string - the same bug class as the dashboard Quick Actions fix in1db63ac, at much larger scale. Sprint 28 only adds the small number of new keys its own empty-state work introduces (see above); authoring the full ~178-key backfill is Sprint 29's explicit "translation validation" scope, not squeezed into this polish pass.
Known gaps / backend needs
- Dashboard metrics endpoint. Categories/Products counts are computed
client-side from
BackofficeDataService(itself mock/API-switchable viaBACKOFFICE_DATA_PROVIDER). A dedicated/builder/dashboard/summary-style endpoint would letAdminDashboardMetricsGatewayreturn 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.