Files
marketplaces/docs/AUDIT/PROJECT-STATE.md
sdarbinyan fd5a436220
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
api doc
2026-07-20 01:02:36 +04:00

707 lines
107 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.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROJECT-STATE — Lead Architect Audit
**Audited:** 2026-07-19, branch `B2B`, commit `d853ecb` (HEAD at audit time).
**Method:** Direct source reading (components, facades, gateways, services, models, SCSS, routing) plus parallel research passes over the same source tree, cross-checked against `docs/ARCHITECTURE.md`, `docs/ADMIN.md`, `docs/EDITOR.md`, `docs/StaticPages.md`, `docs/KNOWN-ISSUES.md`, `docs/SPRINT-PLAN.md`, and `docs/backend/BACKEND-INTEGRATION.md`. Every claim below is either grounded in a specific file:line citation or explicitly marked "not verified" / "not exhaustively verified." Prior docs were treated as leads, not truth — every claim reused from them was re-checked against current code during this pass (several turned out to be stale, e.g. the ~178 missing admin i18n keys logged in `docs/KNOWN-ISSUES.md` were fixed by commit `574f038` and no longer reproduce).
No files were modified to produce this document except this one.
---
## 1. Overall architecture
### Stack
Angular 21.1.5, **100% standalone components** (`grep -rl "NgModule" src/app --include=*.ts` → 0 hits). No global state library — no `@ngrx/*`, `@ngxs/*`, or similar in `package.json`. State management is plain injectable Angular services combining RxJS and signals ("service-with-signals"), matching ADR-007 (`docs/architecture/foundation/adr/ADR-007-state-management-and-facade-boundaries.md`).
### Routing (`src/app/app.routes.ts`, 307 lines)
- Every route sits under a `:lang` prefix (`app.routes.ts:297-305`), guarded by `languageGuard` (`src/app/guards/language.guard.ts`), which validates the language code against `LanguageService.languages`, redirects disabled/unknown codes to the current default, and preserves the rest of the path. Bare URLs without a lang prefix redirect to `ru` (`app.routes.ts:307`) — `ru` is the hardcoded fallback default, not configurable from routing alone.
- **100% lazy-loaded**: every route entry uses `loadComponent(() => import(...))`; no eager route components exist anywhere in `coreRoutes` (`app.routes.ts:10-284`) or the `backoffice` children array (`app.routes.ts:56-252`).
- Admin (`/:lang/backoffice/**`) is a single `AdminLayoutComponent` shell (`app.routes.ts:58`, `src/app/features/admin/shell/admin-layout.component.ts`) behind `adminAuthGuard` (`app.routes.ts:57`), with ~20 lazy child routes.
- `canDeactivate` dirty-guards protect unsaved edits: `projectEditorDirtyGuard` (`app.routes.ts:53`), `adminCategoryDirtyGuard` (`app.routes.ts:118,128`). No equivalent guard exists for `admin/products` edit forms — confirmed by absence of a `canDeactivate` entry on the `products/:id/edit` route (`app.routes.ts:88-96`) despite `AdminProductsFacade` having its own dirty-tracking draft logic (per Sprint 21 in `docs/ADMIN.md`). This is an inconsistency: categories protect against navigating away with unsaved changes, products do not.
- `cmsContentRoutes` is a **deliberately empty array** (`app.routes.ts:286-289`) with a `TODO(CMS)` comment: "Disabled hardcoded pages: about, contacts, faq, delivery, guarantee, company-details, payment-terms, return-policy, public-offer, privacy-policy." These pages exist as fully-built components under `src/app/pages/info/**` and `src/app/pages/legal/**` (40 files, per-locale duplicated) but are **entirely unrouted dead code** today — see Section 6.
- Everything unmatched falls through to a catch-all `:staticPath` route (`app.routes.ts:280-283`) resolved dynamically by `StaticPageComponent` against `bootstrap.staticPages`.
- Dev-only `/__diagnostics` route is excluded from production via an `environment.production` ternary at the top of the routes array (`app.routes.ts:293-296`).
- **No dedicated 404/not-found route exists.** Unmatched paths under `:lang` redirect to home (`app.routes.ts:303`, `{ path: '**', redirectTo: '' }`) rather than rendering an error state — a user hitting a genuinely broken link is silently bounced to the homepage with no "page not found" feedback.
- **No maintenance-mode route or component exists anywhere in the codebase** (confirmed: no file matching `*maintenance*` under `src/app`).
### `app.config.ts` (35 lines)
- `provideRouter` with `withInMemoryScrolling({ scrollPositionRestoration: 'top' })`.
- `provideHttpClient` with a fixed, order-significant interceptor chain: `mockDataInterceptor → apiBaseUrlInterceptor → apiHeadersInterceptor → adminAuthHeadersInterceptor → cacheInterceptor` (`app.config.ts:25-27`).
- Two DI overrides hardcoded directly in `app.config.ts`, **not** switched by the runtime-provider-strategy mechanism used elsewhere: `Ed25519VerificationService → NoopEd25519VerificationService` (line 28 — a fail-open/no-op placeholder for a verification step that isn't implemented) and `MediaRepository → MockMediaRepository` (line 29 — the entire media library has no API implementation at all, see Section 5).
### Layered architecture (ADR-002/006/007)
```
Component (container) --> Facade --> Domain Service --> Repository/Provider (DI token) --> Mock | API
```
```mermaid
flowchart LR
A[Container/Page Component] -->|inject facade| B[Facade]
B -->|composes| C[Domain Service]
C -->|DTO to domain mapper| D[Repository / Provider Interface]
D -->|InjectionToken factory, mode from RuntimeProviderStrategyService| E1[Mock Provider]
D -->|InjectionToken factory| E2[Api Provider - HttpClient]
B --> F[Signals / Observables exposed to component]
G[Presentational Component] -->|Input/Output only| A
```
- **Presentational components** (`src/app/shared/ui/**`, `src/app/ui-library/{atoms,molecules,organisms}/**`) are `@Input()`/`@Output()`-only — no `HttpClient`, storage, or facade injection (ADR-006). Verified spot checks (dialog, table, skeleton, empty-state, button) confirm this.
- **Container/page components** (`src/app/pages/**`, `src/app/features/*/containers/**`, `src/app/features/*/pages/**`) own routing and DI of a facade.
- **Facades** live in `src/app/facades/{platform,runtime,website}/` plus feature-local `facade/` folders (e.g. `src/app/features/admin/products/facade/admin-products.facade.ts`). **Inconsistency found**: `ProductFacade` (`src/app/facades/platform/product.facade.ts`) uses constructor DI and is a thin pass-through; `CategoryFacade` (`src/app/facades/platform/category.facade.ts`) uses `inject()` and exposes **both** signals and RxJS observables side by side for the same data (`allCategories`/`allCategories$`, `selectedCategory`/`selectedCategory$`), with the signals populated imperatively inside subscriptions rather than derived via `computed()`/`toSignal()`. Two facades in the same directory follow two different reactive idioms — this is real internal inconsistency, not a style nit, since it means new contributors have no single pattern to copy.
- **Domain services** (`src/app/core/<domain>/*.service.ts`) convert DTOs to domain models via mappers (e.g. `src/app/core/categories/mappers/category.mapper.ts`); DTOs are not supposed to leak past this boundary.
- **Repositories/providers** are swapped via `InjectionToken` factories reading `RuntimeProviderStrategyService` — see Section 5 for the full mechanism and where it is (and is not) actually wired.
### DI style
Mixed: `inject()` appears in ~132 files, constructor DI in ~72 files (grep, file counts not occurrences) — no single convention is enforced project-wide, and both patterns appear within the same architectural layer (see `ProductFacade` vs `CategoryFacade` above).
### Signals usage
`signal(` appears in 53 files, `computed(` in 47 files. Representative: `AuthService` (`src/app/services/auth.service.ts`) exposes private writable signals (`sessionSignal`, `statusSignal`, `showLoginSignal`) via `.asReadonly()` plus `computed()` derivations (`isAuthenticated`, `displayName`) — this is the cleanest, most idiomatic example in the codebase and is mirrored almost exactly by `AdminAuthService` (`src/app/core/admin-auth/admin-auth.service.ts:31-39`, read in full during this audit — same shape, deliberately kept separate per the file's own doc comment explaining admin/customer session isolation).
### Change detection
`ChangeDetectionStrategy.OnPush` is used in 188 of 189 `@Component`-decorated files (grep count) — effectively universal. This is a genuine architectural strength; see Section 10.
### Dynamic page/section/widget rendering (ADR-005)
Pipeline: `page config -> section engine -> section renderer -> widget host -> registered widget component`.
- `SectionEngineService` (`src/app/dynamic-renderer/section-engine/section-engine.service.ts`, 130 lines) builds an ordered render model from `PageConfig.sections`.
- `PageRendererService` (`src/app/dynamic-renderer/page-renderer/page-renderer.service.ts`, **13 lines**) is a thin pass-through to the Section Engine.
- `WidgetHostService` (`src/app/dynamic-renderer/widget-host/widget-host.service.ts`, 77 lines) resolves each widget's component via `WidgetManifestService` (`src/app/widgets/registry/widget-manifest.service.ts`) and its data via `DataSourceResolverService` (`src/app/widgets/resolvers/data-source-resolver.service.ts`, 251 lines), which delegates to `CategoryFacade`/`ProductFacade` — widgets never call APIs directly, confirmed.
- **Critical finding**: this entire pipeline has real service files but **zero components or templates** — every one of `page-renderer/`, `section-renderer/`, `widget-host/` also contains only a `.gitkeep` alongside its service/model files (confirmed by directory listing). The storefront homepage instead renders through a separate, older path (`layouts/containers/dynamic-page-layout.component.ts`) that does not consume this pipeline at all. Two Project Editor fields — Theme section's "Site Layout" (`layout.type`) and each homepage section's own `type` field — write into a schema this unused pipeline was meant to consume, and currently have **no effect on the rendered storefront**. This is the single most consequential hidden gap in the platform: a merchant can configure two settings that persist, validate cleanly, and look fully functional in the editor, but do nothing at render time. See Sections 4 and 6.
### Theme engine (ADR-008)
`ThemeConfig` (`src/app/shared/models/config/theme.model.ts`): `themeId`, `mode` (`light|dark|system`), 12-color `palette`, typography, spacing, `borderRadiusScale`, shadows, `iconSet`. Palette colors are applied as real CSS custom properties and are genuinely live throughout the app. **The `mode` field is not** — see Section 4/6, `theme-engine.service.ts` sets a `data-theme-mode` attribute on `<html>` but no stylesheet anywhere reads it.
### Feature flags (ADR-009)
`bootstrap.featureFlags` (typed) plus the broader `bootstrap.features` (`MarketplaceFeaturesConfig`). Resolution falls back across older config surfaces for backward compatibility as the flag model evolved across sprints.
### Diagnostics (dev-only)
`src/app/features/diagnostics/` (route `/__diagnostics`, excluded from production) validates bootstrap structure and runtime health, scored 0-100 — genuinely useful for investigating an editor-authored bootstrap, and the only place in the codebase that programmatically cross-checks the dynamic-renderer gap described above (it flags unknown widget types and missing datasources).
### Provider-switching mechanism (detail carried into Section 5)
`RuntimeProviderStrategyService` (`src/app/core/providers/runtime-provider-strategy.service.ts`) is the intended single source of truth for mock-vs-API selection, exposing `getBootstrapProviderMode()`, `getBackofficeProviderMode()`, `getProductProviderMode()`, `getCategoryProviderMode()`, each returning `'mock' | 'api' | 'remote-config'` based on `environment.useMockData` (and, for bootstrap only, an additional `useMockBootstrapOnLocal` + localhost check). **`'remote-config'` is declared but never actually returned or consumed anywhere** — a dead enum value. Verified: `environment.ts` currently has `useMockData: false`, `useMockBootstrapOnLocal: true`, so in the checked-out state real API calls are the default everywhere except bootstrap-on-localhost.
Two more DI tokens exist that consult this strategy but whose `'mock'` branch is **dead code**: `PRODUCT_DATA_PROVIDER` (`src/app/core/products/product-data-provider.token.ts:12-19`) and `CATEGORY_REPOSITORY` (`src/app/core/categories/category-repository.token.ts:12-19`) both have a `switch` over the mode where **every case, including `'mock'`, returns the same `Api*` implementation** — no mock `ProductDataProvider`/`CategoryRepository` class exists in the codebase at all. This is not a bug in the sense of breaking anything (mocking for products/categories instead happens one layer down, via `mockDataInterceptor` faking HTTP responses — see Section 5), but it does mean the provider-token pattern documented in `docs/ARCHITECTURE.md` as the swap mechanism is only genuinely wired for **bootstrap** and **backoffice reads** — for storefront product/category data and for every admin domain, "mock" happens by a different mechanism than the architecture doc implies.
### Integrations placeholders
`src/app/integrations/{auth,authorization,payment}/` contain **only `.gitkeep` files** — scaffolded but entirely unused. The real, working auth code lives in `src/app/services/auth.service.ts` and `src/app/core/admin-auth/`; real payment code lives inline in `src/app/pages/cart/cart.component.ts` (see Section 2). These three directories are dead scaffolding that should either be deleted or the real code relocated into them — currently they mislead a reader into thinking auth/payment integration code is organized there.
---
## 2. Marketplace (customer-facing)
Customer routes render under `/:lang/**`. There is **no dedicated Checkout page**`src/app/features/website/checkout/` contains only a `.gitkeep`; checkout is folded entirely into `CartComponent` (664 lines, `src/app/pages/cart/cart.component.ts`, template 359 lines). There is **no dedicated Profile/Account or Orders (customer-facing order history) page** — no route, no component found under `src/app/pages/**` or `src/app/features/website/**` matching that purpose; a logged-in customer has no self-service order-history UI today (only the admin side has order management, and only for staff). There is **no traditional email/password login/register form** — the only auth surface is `TelegramLoginComponent` (`src/app/components/telegram-login/telegram-login.component.ts`), a QR-code Telegram session flow shared (with separate storage) between customer and admin. There is **no dedicated 404 page** (redirects home) and **no maintenance page** (confirmed above).
### Home
- **Files**: `src/app/pages/home/home.component.ts` (42 lines) / `.html` (11 lines) — intentionally thin, delegates to `DynamicPageLayoutComponent` (`layouts/containers/dynamic-page-layout.component.ts`) which composes the widget/section tree from `bootstrap.pages.home`.
- **Status: Ready**, with a known open bug: `docs/KNOWN-ISSUES.md` item 1 — a hero-to-categories dead-space gap on the storefront homepage, traced to bootstrap mock config padding values, not a code defect. Not independently re-verified visually in this audit (no live browser run performed).
- **Widgets rendered**: hero, categories, product-carousel, recently-viewed, footer-navigation (`src/app/widgets/ui/*`) — each receives `{ section config, resolved data }` only, per ADR-005.
- **Missing backend**: none directly — home composes already-covered product/category data.
### Catalog / Search
- **Files**: `CatalogContainerComponent` (`src/app/features/website/catalog/containers/catalog-container.component.ts`, **950 lines** — the single largest component in the entire `src/app` tree) serves both `/catalog`, `/catalog/:id`, and `/search` routes (`app.routes.ts:16-22,43-45`) — one container handling three distinct route paths and URL-param shapes.
- **Status: Needs work.** A 950-line container is a strong maintainability red flag on its own (nearly 2.5x the project's stated 400-line convention ceiling referenced elsewhere in `docs/EDITOR.md`); it composes filters-panel, product-grid, category-grid, sorting-control, layout-switcher, search-box, search-results, and catalog-empty-state (`src/app/features/website/catalog/components/*`) — a reasonable component breakdown one level down, but the container itself is a single point of complexity for three different page contracts (catalog listing, category filtering, free-text search).
- `category/:id` and `category/:id/items` both `redirectTo: 'catalog/:id'` (`app.routes.ts:23-32`) — legacy URL compatibility, working as intended, not dead code.
- **Search facade**: `src/app/features/search/facade/search.facade.ts` (592 lines) — second-largest facade in the codebase; handles suggestions, history (via `LocalSearchHistoryRepository`, `localStorage`-backed), and a `BackendSearchHistoryRepository` whose every method is a stubbed no-op placeholder (confirmed by the architecture research pass; "Placeholder for future backend endpoint").
- **Missing backend**: storefront product/category reads go through `PRODUCT_DATA_PROVIDER`/`CATEGORY_REPOSITORY`, which — per Section 1 — always resolve to the real `ApiProductDataProvider`/`ApiCategoryRepository` (real `HttpClient` calls); "mock mode" for these is achieved instead by `mockDataInterceptor` (`src/app/interceptors/mock-data.interceptor.ts`, 923 lines of hand-authored mock category/product JSON) intercepting the HTTP call before it reaches the network.
### Product detail
- **Files**: `ProductDetailsContainerComponent` (`src/app/features/website/product/containers/product-details-container.component.ts`, 683 lines) plus a well-decomposed set of presentational children (`product-gallery`, `product-information`, `product-actions`, `product-description`, `product-specifications`, `product-warranty`, `variant-selector`, `related-products`, `delivery-information`) and an `engagement/` subtree for reviews/questions (`review-list`, `review-form`, `review-card`, `question-list`, `question-form`, `question-card`, `rating-summary`, `star-selector`, `stars`).
- A legacy duplicate exists: `src/app/pages/item-detail/item-detail.component.ts` (368 lines, template 356 lines) — appears to predate the `features/website/product` rebuild. `app.routes.ts` routes `/product/:id` to `ProductDetailsContainerComponent` (line 34-36) and redirects `/item/:id → /product/:id` (line 38-41), so `ItemDetailComponent` is **not reachable via any route** — likely-unused, not exhaustively verified (no other route or component reference to it was found in this pass, but a full cross-repo import search was not run for this specific file).
- **Status: Ready** for the primary container; reviews/Q&A engagement components are functionally present and wired to `ApiService` (`submitReview`, `submitQuestion`).
- **XSS surface**: `item-detail.component.html:208` renders `[innerHTML]="getSafeHtml(item()!.description)"` via `DomSanitizer.bypassSecurityTrustHtml` — since this component is unrouted this is currently inert, but if ever re-routed it inherits whatever sanitization `getSafeHtml()` performs (not independently verified for correctness).
### Cart / Checkout (merged)
- **Files**: `src/app/pages/cart/cart.component.ts` (664 lines) / `.html` (359 lines).
- **Status: Ready but does double duty as checkout** — no separate checkout step/route exists. Payment is embedded directly: `bankPaymentFrameUrl = this.sanitizer.bypassSecurityTrustResourceUrl(bankUrl)` (`cart.component.ts:371`) renders a bank-hosted payment iframe inline in the cart page — a real, working integration (`ApiService.createCartPayment`/`checkCartPaymentStatus`), not a placeholder, but architecturally unusual to have zero dedicated checkout flow (shipping address, order review, confirmation) separate from the cart itself.
- **Missing backend**: none — cart/payment is one of the few genuinely backend-wired storefront flows (`ApiService`, real `HttpClient`, per Section 5).
### Wishlist / Compare
- **Files**: `WishlistPageComponent` (`src/app/features/website/user-experience/wishlist/containers/wishlist-page.component.ts`), `ComparePageComponent` (`.../compare/containers/compare-page.component.ts`) plus `compare-table.component.ts`.
- **Status: Ready**, but **entirely local-only**`USER_EXPERIENCE_REPOSITORY` unconditionally binds `LocalUserExperienceRepository` (pure `localStorage`), with no API alternative and no provider-switching at all (per the architecture research pass). Wishlist/compare state does not sync across devices or survive a cleared browser.
- Gated by a documented double-flag bug (fixed per `docs/KNOWN-ISSUES.md`/`docs/EDITOR.md`): visibility used to be driven by only one of two runtime flags (`featureFlags.<key>` and `userExperience.<key>.enabled`); the Project Editor's Features section now toggles both together.
### Static Pages (customer-rendered)
- **Files**: `StaticPageComponent` (`src/app/pages/static-page/static-page.component.ts`) resolves `bootstrap.staticPages` dynamically for both `page/:key` and the catch-all `:staticPath` route.
- **Status: Ready**, sanitizes rendered HTML via `DomSanitizer.bypassSecurityTrustHtml` after passing through a `sanitized` step (`static-page.component.ts:103` — the sanitization call itself was not traced to its implementation in this pass; flagged for Section 11).
- **Dead parallel system**: `src/app/pages/info/**` (about/contacts/delivery/faq/guarantee, each with `en/`/`ru/`/`hy/` sub-variants) and `src/app/pages/legal/**` (company-details/payment-terms/privacy-policy/public-offer/return-policy, same per-locale structure) are ~40 files of hardcoded, per-locale-duplicated legal/info content that predates the dynamic Static Pages builder and are **confirmed unrouted** (`cmsContentRoutes = []`, `app.routes.ts:289`). `public-offer-{en,ru,hy}.component.html` alone are 529 lines each (1,587 lines of duplicated legal boilerplate across 3 files for one page). This is the single largest concentrated block of dead code in the repository — see Section 6.
### Authentication
- **Files**: `TelegramLoginComponent` (`src/app/components/telegram-login/`), `AuthService` (`src/app/services/auth.service.ts`).
- **Status: Ready** as a QR-login flow, but is the *only* login method — no password/email fallback exists for customers without Telegram.
### 404 / Maintenance
- **Status: Missing.** No dedicated 404 component (silently redirects home); no maintenance-mode component or route exists at all. Both are real gaps for a production e-commerce platform (a merchant cannot show a "we'll be back" page during a deploy/migration, and a broken link gives no feedback).
### Cross-cutting UX/accessibility/performance for Marketplace
- `<img>` lazy-loading: only 17 of 35 template `<img>` occurrences across `src/app` use `loading="lazy"` or Angular's `ngSrc` (`NgOptimizedImage`) — roughly half. Not broken down per-page in this pass; a per-page audit would need to check `product-gallery`, `product-card`, `category-grid`, and widget templates individually (not exhaustively done here).
- Change detection: `OnPush` is used almost universally (188/189 components) including in the marketplace surface — a genuine performance strength (Section 10).
- Accessibility: per `docs/ADMIN.md`'s Sprint 28 note, every `<img>` in `src/app/**` was checked for a missing `alt` attribute and none were found — this claim covers the whole app including marketplace pages and was not re-run independently in this pass, but the methodology described (grep for `<img` without `alt`/`[alt]`/`[attr.alt]`) is sound and reproducible.
---
## 3. Admin (backoffice)
All admin routes live under `/:lang/backoffice/**`, guarded by `adminAuthGuard` (`src/app/core/admin-auth/admin-auth.guard.ts` — 15 lines, straightforward `isAuthenticated()` check, `requestLogin()` on failure). Admin auth is fully isolated from customer auth: separate cookie name (`adminSessionID` vs the customer session cookie), separate `localStorage` keys (`adminToken`/`adminRefreshToken`), separate service class (`AdminAuthService` vs `AuthService`), separate signals — confirmed by reading both files in full. **Backend gap, confirmed real and unresolved**: both admin and customer login hit the identical Telegram session endpoint (`TelegramSessionApiService`, `{authApiUrl}/users/sessions`) — there is no server-side concept of "this session is an admin session," so authorization is enforced nowhere but the frontend guard today. This is flagged repeatedly and consistently across `docs/ADMIN.md`, `docs/EDITOR.md`, and `docs/backend/BACKEND-INTEGRATION.md` — a genuine, still-open security gap (see Section 11).
Every admin domain follows the same container/facade/local-gateway split (`features/admin/<domain>/{models,services,facade,pages,components}`), and — per the architecture research pass — **every one of the 8 admin gateway implementations is local/in-memory, none use `HttpClient`**:
| Domain | Gateway | Data reality |
|---|---|---|
| Dashboard | `admin-dashboard-metrics.local.gateway.ts` | Derived counts from `BackofficeDataService` (itself mock/API-switchable) |
| Products | `admin-products-local.gateway.ts` (197 lines) | Seeded from `BackofficeDataService.loadProducts()`; all writes in-memory only |
| Categories | `admin-categories-local.gateway.ts` (120 lines) | Seeded from `BackofficeDataService.loadCategories()`; all writes in-memory only |
| Orders | `admin-orders-local.gateway.ts` | **24 fully synthetic orders**, no backend derivation at all |
| Transactions | `admin-transactions-local.gateway.ts` (91 lines) | Derived from the 24 synthetic orders (mock-on-mock) |
| Users/roles | `admin-users-local.gateway.ts` | **Hardcoded 4-user roster**, fabricated |
| Moderation | `admin-moderation-local.gateway.ts` (189 lines) | 32 synthetic reviews + 10 reports from canned arrays + real product names |
| Monitoring | `admin-monitoring-local.gateway.ts` (82 lines) | **40 fully fabricated events**, hardcoded queues/webhooks |
Analytics has no dedicated gateway file — it aggregates real arithmetic (revenue/orders/AOV/sales-over-time) over the mock Orders/Products/Categories data (confirmed in `docs/ADMIN.md` Sprint 27, plausible given Orders/Products data shapes, not independently re-derived in this pass). Visitors/funnels/heatmaps render an explicit `pending-backend` badge rather than fabricated numbers.
### Dashboard
- **File**: `admin-dashboard-page.component.*` + `AdminDashboardFacade` (201 lines).
- **Implemented ~90%.** Cards render one of `loading`/`empty`/`error`/`pending-backend`/ready states — a genuinely well-designed state model (`AdminDashboardCardComponent`), and Orders/Revenue cards correctly show `pending-backend` rather than fake zeros, since no order/revenue data model existed at the time (now partially superseded by Sprint 23's Orders gateway, but the dashboard cards' own honesty pattern is worth noting as a good practice other sections don't all follow as visibly).
- **System Health** reuses `ProjectValidator` (6 real checks: bootstrap valid, configuration valid, missing translations, invalid colors, invalid widget references, invalid layouts) — genuinely computed, not fabricated.
- **Recent Activity** is `localStorage`-only per tenant (`AdminDashboardHistoryService`) — will not show another admin's activity until a real audit-log endpoint exists.
- **Known bug class**: none open specific to dashboard beyond the general admin i18n/backend gaps.
### Products
- **Files**: `admin-products.facade.ts` (316 lines), `admin-product-form.component.ts` (243 lines / `.html` 291 lines), `product-variants-editor.component.ts` (169 lines).
- **Implemented ~85%.** Archive/restore (soft delete distinct from `visible`), barcode, lightweight variants (`name|price|quantity` parsed from delimited text, not a full options-matrix), related-products checkbox picker (scoped to whatever page is currently loaded in the facade — not a full catalog search, explicitly documented trade-off), gallery via shared `MediaPickerComponent`, infinite-scroll toggle.
- Recent commits (`3b955b1 feat(admin): Shopify-style variant attributes matching production data shape`, `afaf79d feat(admin): visual badge manager`, `31c64e9 feat(admin): collapse product translations behind default-language fields`, `b16e300 feat(admin): product editor auto-slug, SKU helper, one-click SEO fill`) indicate active, recent UX investment beyond what `docs/ADMIN.md`'s Sprint log describes — these post-date the doc and were not individually re-verified line-by-line in this pass beyond confirming the files exist and are wired into the routed component tree.
- **No `canDeactivate` dirty guard** on the product editor route (confirmed absence in `app.routes.ts:88-105`) despite categories having one — an inconsistency (see Section 1).
- **Known bugs (fixed, historical)**: none open for products specifically per `docs/KNOWN-ISSUES.md`.
### Categories
- **Files**: `admin-categories.facade.ts` (**514 lines** — largest admin facade), `admin-categories-list.component.ts` (174 lines).
- **Implemented ~90%.** Hierarchy via `parentId` with flattened indented-tree rendering; native HTML5 drag-and-drop reorder (not CDK, unlike the Project Editor's footer/homepage builders — an inconsistency in DnD implementation choice across the codebase); soft delete blocked client-side if the category has children or `itemsCount > 0`; draft/publish workflow with `localStorage` autosave recovery; `canDeactivate` dirty guard present.
- **Two real bugs found and fixed** during a documented 2026-07-17 bug-hunt (`docs/ADMIN.md` "Bug-hunt audit pass"), both verified plausible from the current code structure: (1) create-mode draft recovery was permanently dead because the draft key was derived from `Date.now()` on every call, orphaning `localStorage` entries forever — fixed with a stable `admin-category-draft:new` key; (2) drag-and-drop reorder wrote the dropped-on row's `order` value directly onto the dragged item instead of computing a full resequence, so on fresh seed data (every category starting at `order: 0`) **every drag silently no-op'd** — fixed by reordering via target `id` and resequencing all affected siblings. Both fixes were not re-derived from scratch in this pass but the described defect mechanics are consistent with drag-and-drop-by-order-value being a known anti-pattern.
### Orders
- **Files**: `admin-orders.facade.ts` (213 lines).
- **Implemented ~75% relative to a real order system, ~95% relative to its explicitly-scoped mock ambition.** List (search/status filter/pagination/CSV export), detail (customer/payment/shipping, itemized total, status timeline, change-status, refund-request + cancel both `window.confirm`-gated, dual customer/internal notes, print-invoice via `window.print()`). **No real backend or data model for orders exists anywhere in the repository** — this is explicitly and repeatedly documented, not a hidden gap.
### Customers
- **Files**: `src/app/features/admin/customers/` (facade, models, pages) — routed at `/backoffice/customers` and `/backoffice/customers/:email`.
- Not covered in depth by `docs/ADMIN.md`'s sprint log (the doc's sprint numbering jumps from Products/Categories to Media/Orders/Transactions/Users/Monitoring/Analytics without a distinct "Customers" sprint entry) — **implementation depth not independently verified in this pass beyond confirming the routed pages exist and follow the same facade/gateway convention as every other admin domain.** Flagged as "not exhaustively verified."
### Transactions
- **Files**: `admin-transactions-local.gateway.ts` (91 lines), facade and list/detail pages.
- **Implemented ~85%** for its scope: search/status/type filters, CSV export, retry-failed-transaction flow, fraud-flag toggle, per-transaction audit-log dialog. Explicitly derived from the same 24 synthetic orders as the Orders domain (kept consistent by design, not by accident).
### Media (Media Library)
- **Files**: `src/app/features/backoffice/media/media-library-page.component.ts`, `src/app/core/media/mock-media-repository.service.ts` (277 lines), `src/app/shared/media/media-picker/`.
- **Implemented ~90% of its own scope.** Real, working: 10MB size + MIME allow-list validation with surfaced error messages, SVG `<script>`/`on*` attribute stripping before storage, raster image downscale-to-2000px + re-encode via `<canvas>` before storage, flat folder tagging (no true hierarchy), tag editing via `window.prompt`.
- **Storage is IndexedDB in the browser, not a real backend** — `MediaRepository` is hardwired to `MockMediaRepository` in `app.config.ts:29` with **no API implementation and no provider-switching at all** (unlike bootstrap/backoffice, this one isn't even token-gated). Uploaded media does not exist anywhere except the uploading admin's own browser.
### Moderation (Reviews & Reports)
- **Files**: `admin-moderation.facade.ts` (218 lines), `admin-moderation-local.gateway.ts` (189 lines), pages: `admin-reviews-list-page.component.ts`, `admin-review-detail-page.component.ts`, `admin-reports-list-page.component.ts`.
- This is the "Reviews" area named in the audit brief — the codebase calls it "Moderation," routed at `/backoffice/moderation` and `/backoffice/moderation/reports`. **Implemented ~80%** of its scope: 32 synthetic reviews + 10 reports, category filter + search. **No real backend** — reviews submitted by real customers via `ApiService.submitReview()` on the storefront do not appear to flow into this admin moderation queue in any code path found in this pass (not exhaustively verified — would require tracing whether `BackofficeDataService`/moderation gateway ever reads real submitted-review data; the seed data is explicitly described as "canned author/snippet arrays," which suggests it does not).
### Monitoring
- **Files**: `admin-monitoring-local.gateway.ts` (82 lines), `admin-monitoring-page.component.ts`.
- **Implemented ~70%** of its scope, and honest about it: one real section (Health, reusing the Dashboard's genuine `ProjectValidator` checks) alongside three fully fabricated sections (unified audit/security/API/error event feed — 40 synthetic entries; queue monitoring — 3 mock named queues; webhook delivery log — mock). No logging backend exists anywhere in the system, so there is nothing real to read from for the fabricated sections — a structural gap, not an oversight.
### Analytics
- **Files**: `admin-analytics.facade.ts` (338 lines), `admin-analytics-page.component.html` (246 lines).
- **Implemented ~65%.** Revenue/orders/AOV/sales-over-time/top-products are real aggregation arithmetic over the mock Orders/Products/Categories data (not fabricated numbers, but ultimately traceable to fabricated source data). Visitors/funnels/heatmaps correctly show `pending-backend` since no analytics/tracking pipeline exists anywhere. Chart is a plain `<div>`-bar chart via `[style.height.%]`, no charting library — adequate for one series, would need revisiting for more chart types.
### Users & Roles
- **Files**: `admin-users-local.gateway.ts`.
- **Implemented ~70%.** 4 built-in roles with a flat permission-string list (not a real permission catalog, no custom-role creation — intentionally scoped down). Invitations create a local record only; **no email actually sends**. Session/device manager is mocked (2 fabricated sessions per user on first view) since the real auth flow only ever tracks the current browser's session — there is no multi-device session backend.
### Reports / Settings / Diagnostics
- **Reports**: folded into Moderation (`/backoffice/moderation/reports`) — there is no separate "Reports" admin area distinct from the reports-queue-of-flagged-content concept; if "Reports" in the brief means business reporting, that function is covered (partially) by Analytics instead. Flagging this naming mismatch explicitly rather than guessing which the brief intended.
- **Settings**: there is no single "Admin Settings" page — tenant-level settings (branding, theme, domain, etc.) live in the **Marketplace Builder / Project Editor** (`/edit/*`), not in `/backoffice/*`. This is a genuine UX seam: an admin user looking for "Settings" inside the admin shell (`AdminLayoutComponent`) will not find it there; it's a separate top-level area (`/edit`) with its own navigation, reachable only via a Quick Action link or by typing the URL.
- **Diagnostics**: `src/app/features/diagnostics/` (route `/__diagnostics`) is dev-only and correctly excluded from production builds (`app.routes.ts:293-296`) and from crawling (`public/robots.txt` blocks it per Sprint 28). Not part of the admin shell/nav at all — a developer tool, not an admin-facing feature, correctly scoped.
### Admin-wide notes
- **Design consistency**: as of Sprint 28 (`docs/ADMIN.md`), all 8 built admin sections share `app-skeleton` (loading) and `app-empty-state` (empty) primitives — verified as a real, systematic pass rather than piecemeal, since the doc names each section's prior gap explicitly (which sections had a `loading` signal but no UI, vs. which had empty-state but no skeleton).
- **i18n**: the ~178 raw untranslated `adminXxx.*` keys documented as an open issue in `docs/KNOWN-ISSUES.md` are **fixed** — confirmed by grepping `src/app/i18n/{en,ru,hy,translations}.ts` for an `adminProducts:` object, present in all three locale files plus the `Translations` interface. This is a stale doc claim that no longer reproduces; `docs/KNOWN-ISSUES.md` should be updated (not done here — read-only audit).
- **Bundle**: the initial-bundle budget warning (~198kB over the 700kB `angular.json` budget, see Section 10) predates all admin work and is unaffected by it since every admin page is lazy-loaded.
---
## 4. Marketplace Builder
**Important correction to the audit brief's assumed file layout**: `src/app/features/builder/{feature-flag-editor,navigation-editor,page-editor,seo-editor,theme-editor}/` — the five sub-folders matching the brief's list — **contain only `.gitkeep` files each**. That is stale/unused scaffolding, not the real builder. The actual, fully-built Marketplace Builder lives at `src/app/features/project-editor/` (11 section components under `sections/`, one facade, a schema/validator layer under `schema/`) plus `src/app/features/content-management/` for Static Pages specifically. All findings below are against the real, routed implementation (`/edit/:section`).
### Mechanism notes (apply to every section)
- **Drag-and-drop**: `@angular/cdk/drag-drop` (`CdkDragDrop`, `moveItemInArray`), used for homepage section reordering and footer column/link reordering. Navigation link reordering and Static Pages list reordering are **up/down buttons, not drag-and-drop** — Static Pages' choice is an explicit, documented lower-complexity scope call; Navigation's is not explained anywhere, an inconsistency worth flagging (why does Homepage/Footer get CDK drag-drop but Navigation doesn't, when both are ordered lists of similar complexity).
- **Live preview is not an iframe/postMessage sandbox.** `ProjectEditorPreviewService.preview()` calls `PlatformRuntimeService.reloadFromBootstrap()` — the *same* mechanism Publish uses — then navigates the current tab to `/{lang}`. Preview and Publish are functionally the same client-side reload; there is no separate sandboxed preview surface. A merchant could reasonably believe "Preview" is non-destructive when it mutates live runtime state identically to Publish (it just doesn't flip `status` to `published` or write the "last published" marker).
- **No backend persistence for any of it.** `ApiBootstrapProvider` implements only `GET /bootstrap` — there is no PUT/POST anywhere in the provider layer. `save()` and `publish()` both write only to `localStorage` (`projectEditor.draftBootstrap.v1`, tenant-scoped) and then trigger the same in-app reload. Every "save" in the entire Builder only persists to the browser; clearing site data loses all edits.
### General
`general-section.component.ts` (74 lines). Name/domain (auto-derives `https://` prefix silently, with no visible feedback of the derived URL)/description — all wired. Language management was recently de-duplicated (commit `440d2ec`): previously a second, independent comma-separated text field existed here alongside the real Languages section; now it's a read-only chip summary linking to Languages — a genuine, verified UX fix eliminating a two-sources-of-truth bug.
### Branding
`branding-section.component.ts` (69 lines). Logo/compact-logo/favicon/social(OG)-image/gallery, all via `app-image-field` (thumbnail preview + replace/remove, opens `app-media-picker`). A real fixed bug: `branding.socialImageUrl` used to be saved but never actually read by `SeoService.resetToDefaults()` — now fixed. **Missing explanations**: no guidance on recommended logo dimensions/aspect ratio or favicon format constraints anywhere in the template.
### Theme
`theme-section.component.ts` (91 lines). Palette (8 colors via `app-color-picker`) is genuinely live, applied as real CSS custom properties. **Two confirmed-dead controls sit in the same section as the fully-working palette**, which is a real trust problem for a merchant since nothing in the UI distinguishes a live control from a decorative one:
1. **Theme Mode** (light/dark/system) saves and sets `data-theme-mode` on `<html>` but no stylesheet anywhere reads that attribute — picking Dark or System changes nothing visually.
2. **Site Layout** (`layout.type`) feeds the unwired `dynamic-renderer` pipeline described in Section 1 — has no effect on the rendered storefront.
### Header
`header-section.component.ts` (72 lines). 9 toggles + layout select + sticky, all live-wired except **`showProfile`**, a real toggle with no corresponding profile/account menu anywhere in `header.component.html` — flips a flag nothing reads (blocked on an auth-system dependency that doesn't exist for account menus today).
### Footer
`footer-section.component.ts` (198 lines) — recently rebuilt (commit `726df0c feat(builder): visual footer builder with drag-and-drop columns`). CDK drag-and-drop columns and links; per-link source toggle between a static-page reference (resolved by stable page id, survives route renames) or a custom URL; payment icons and social links via `app-key-value-editor`; multiple phones/emails. `FooterResolverService` was updated in the same pass to read `footer.columns` as primary source with fallback to legacy auto-grouping — this is genuinely wired end-to-end to the live storefront footer, not builder-only cosmetics. Two id-collision bugs (social-link ids derived from array length, payment-icon ids from `src`) that corrupted `@for (track item.id)` DOM identity on the live storefront footer were found and fixed in the same pass.
### Homepage
`homepage-section.component.ts` (167 lines) — recently rebuilt (commit `71d5f4d feat(builder): visual homepage blocks, merchant-language widget settings, real carousel arrows`). Merchant-facing block catalog (icon+name+description replacing raw `section.id` strings like `section-hero`); add-block picker across 8 block types; duplicate/remove per block; CDK drag-to-reorder. A companion `homepage-overview.component.ts` (67 lines) computes a completion-ring UI purely from real widget presence data (hero/categories/products/promotion/newsletter/any-sections) — not fabricated. **Dead code retained**: `updateSection(sectionId, 'type', ...)` still exists in the component with no UI calling it, because the field it writes feeds the unwired `dynamic-renderer` pipeline.
### Widgets
`widgets-section.component.ts` (215 lines). Typed editors for hero (title/subtitle/slide array) and toggle/reorder/duplicate/remove for all widgets; JSON-fallback textarea for anything without a dedicated editor. A real, non-obvious fix: the JSON fallback used to silently discard an in-progress invalid edit (parse error caught, then overwritten by the last-committed value on the next change-detection tick); now keeps the user's draft on screen with an inline error until valid. Hero widget's slide array drives a real rotator with dots, click-to-jump, and a 5-second autoplay interval — verified in `hero-widget.component.ts`, not a cosmetic stub.
### Static Pages
`static-pages-editor.component.ts` (**404 lines**, right at the project's 400-line convention ceiling — flagged as a split candidate, bulk-actions and validation-badge logic are natural extraction points), `content-page.service.ts` (285 lines). Full CRUD, multi-field search, status/locale filters, bulk actions (delete/enable/disable/publish/unpublish), duplicate, per-locale device preview, SEO fields with validation-driven badges. Two slug/route id-collision bugs (create-delete-create, duplicate-the-same-page-twice) were found and fixed with a shared `uniqueValue()` helper, confirmed used consistently in both `createPage()` and `duplicatePage()`.
- **`persist()` footgun**: every mutation must operate on the full unfiltered page list, never the search/filter-narrowed view (writing from a filtered view would silently delete whatever the filter was hiding) — correctly followed everywhere checked, but is an unenforced convention (no type system or lint rule prevents a future contributor from getting this wrong), a real maintainability risk.
- **Rich-text editor** (`MarketplaceHtmlEditorComponent`) is built on the deprecated `document.execCommand` API — works in all current browsers, no modern drop-in replacement exists; a real, acknowledged rewrite risk if browsers ever drop the API. Emits raw unsanitized HTML by design (sanitization is a render-time concern via `DomSanitizer`, not an authoring-time one) — recently redesigned toolbar (commit `9eacd00`) grouped by intent with PrimeIcons/tooltips/aria-labels.
- **Confusing**: the `enabled`/`status` gating story (a page can be individually unpublished independent of the whole-bootstrap publish state) is correctly documented in `docs/StaticPages.md` but not surfaced as an explicit inline warning anywhere in the editor UI itself — a merchant could publish the whole config and be confused why one page still isn't live.
### Navigation
`navigation-section.component.ts` (129 lines). Header nav add/remove/reorder(up-down)/per-locale label/URL/visibility; "Insert page link" creates a stable `type: 'staticPage'` nav item. **Confusing**: flat footer nav is editable here, but grouped (column-based) footer nav is read-only in this tab — must switch to the Footer tab to edit it, with no cross-link between the two in the template.
### Languages
`languages-section.component.ts` (69 lines). Add/remove/set-default correctly routes through `LocaleSyncService`, which seeds empty translation entries across static pages, header nav, footer nav (both shapes), and sidebar nav — verified thorough by reading the full sync implementation. Cannot remove the current default locale — enforced at the service layer, not just UI-disabled (real defense in depth). A silent-no-op bug (adding an already-supported locale cleared the input with no feedback) is fixed with an inline error. Recently de-duplicated against General (commit `440d2ec`) — single source of truth now.
### Features
`features-section.component.ts` (97 lines). Catalog navigation mode select, general flags, wishlist/compare/recently-viewed/search-suggestions-history/reviews/questions toggles. The double-flag wishlist/compare bug (Section 1/6) is fixed here — one toggle now correctly drives both `featureFlags.<key>` and `userExperience.<key>.enabled`.
### Preview
`preview-section.component.ts` (37 lines). Export/import JSON, a "changes since last publish" card (validation issues + before/after diff table). A real fixed bug: import used to bypass `updateBootstrap()` entirely, so an imported config never reached `localStorage` draft storage (lost on refresh before an explicit Save) and wasn't undo-able — now routed through the standard update pipeline.
### Cross-cutting Builder findings
- **`project-editor.facade.ts` is 686 lines** — by far the largest file in the entire builder, owning bootstrap state, a debounced (~300ms) undo/redo history capped at 50 entries, draft storage, validation aggregation, section-readiness computation, and navigation-link mutation. It works today (traced in full by the architecture pass) but is the single highest-blast-radius file in the Builder — a change here risks every section simultaneously.
- **Field-schema/validator layer** (`schema/`) is metadata-augmented, not schema-driven — section templates stay hand-authored, a schema registry sits alongside for labels/validators. `ProjectValidator` covers ~17 distinct checks (duplicate slugs/routes, invalid colors/CSS/URLs/emails, missing translations, malformed widget config, etc.), gates Publish only on `error`-severity issues, not `warning`s — a sound, deliberate design.
- **Undo/redo** is a real, pure reducer (`schema/history.util.ts`) wired with keyboard shortcuts (skipped while a text field has focus so native per-field undo still works) — genuinely implemented, not stubbed.
- **`items-carousel` (PrimeNG `p-carousel`, `src/app/components/items-carousel/`) is confirmed dead code** — not wired into any route or widget; commit `71d5f4d`'s own investigation (per the builder research pass) found the real carousel functionality landed in `ProductCarouselWidgetComponent` instead. This is a shared/top-level component directory (`src/app/components/`), so its unused status is a genuine cleanup candidate, not feature-folder clutter.
---
## 5. Backend readiness
### Provider-switching mechanism
`RuntimeProviderStrategyService` (`src/app/core/providers/runtime-provider-strategy.service.ts`) reads `environment.useMockData` (and, for bootstrap only, `useMockBootstrapOnLocal` + a localhost check) and returns `'mock' | 'api' | 'remote-config'`. This feeds `InjectionToken` factories that pick between a Mock/Api pair — but as established in Section 1, this mechanism is **only genuinely two-sided for Bootstrap and Backoffice reads**. For Products/Categories the "mock" branch is dead code (always resolves to the real API class); mocking there happens instead via `mockDataInterceptor` (923 lines) faking HTTP responses at the interceptor layer — a second, parallel mocking mechanism that a reader of `docs/ARCHITECTURE.md` alone would not know exists.
### Current state, by domain (see Section 3's table for the admin-domain detail)
| Domain | Status | Evidence |
|---|---|---|
| Bootstrap config | **Real HTTP + working mock**, switchable | `ApiBootstrapProvider``GET /bootstrap`; `MockBootstrapProvider``GET /assets/mock/bootstrap/bootstrap.json` (still an HTTP call, to a local JSON asset) |
| Backoffice products/categories (read) | **Real HTTP + working mock**, switchable | `ApiBackofficeDataProvider``GET /api/backoffice/{products,categories}`; `MockBackofficeDataProvider` → local JSON assets |
| Storefront products/categories | **Real HTTP only** (mock branch dead code; interceptor-level mocking exists separately) | `ApiProductDataProvider`, `ApiCategoryRepository`, both real `HttpClient` with retry+backoff |
| Cart/reviews/questions/payment/search | **Real HTTP**, no mock provider variant at all | `ApiService` (674 lines) — every method issues real `HttpClient` calls; extensive DTO normalization for backend-format drift (legacy vs "backOffice" shapes) |
| Wishlist/compare/recently-viewed/saved-searches | **Local-only, no API path exists, no switching** | `LocalUserExperienceRepository`, pure `localStorage` |
| Search history | **Local-only**; a `BackendSearchHistoryRepository` class exists but every method is a stubbed no-op | Not confirmed bound anywhere via DI |
| Media library | **Local-only (IndexedDB), no API class exists, hardwired in `app.config.ts`, not token-switched** | `MockMediaRepository` |
| Admin: Products/Categories | **Local/in-memory**, seeded from Backoffice data (itself switchable) but writes never persist past reload | `admin-{products,categories}-local.gateway.ts` |
| Admin: Orders/Transactions | **Fully synthetic**, no backend model exists anywhere | `admin-orders-local.gateway.ts` (24 seeded orders), `admin-transactions-local.gateway.ts` (derived) |
| Admin: Users/roles | **Fully fabricated**, hardcoded 4-user roster | `admin-users-local.gateway.ts` |
| Admin: Moderation/Monitoring | **Fully fabricated** (Moderation partially uses real product names) | `admin-moderation-local.gateway.ts`, `admin-monitoring-local.gateway.ts` |
| Admin: Analytics | **Real arithmetic over fabricated source data**; visitors/funnels/heatmaps explicitly `pending-backend` | No dedicated gateway file found |
| Admin: Dashboard metrics | **Derived from Backoffice data**, honest `pending-backend` state for Orders/Revenue cards | `admin-dashboard-metrics.local.gateway.ts` |
### What changes when backend integration happens
Per the pattern already used for bootstrap/backoffice (and independently documented, matching the code, in `docs/backend/BACKEND-INTEGRATION.md` §14): implement a new `*ApiGateway`/`*ApiRepository` class against the same interface each facade already depends on, then rebind the DI token (or, for the several admin domains that inject the concrete `*LocalGateway` class directly rather than through an `InjectionToken` — confirmed no `admin-{orders,transactions,users,moderation,monitoring}-gateway.token.ts` files exist — introduce a token first, or swap the binding in each facade's constructor/`inject()` call). No facade or component changes are required for domains that already sit behind an interface — this is the real, verified architectural payoff of the container/facade/gateway split, and it is consistently followed across all 8 admin domains plus the storefront.
### Estimated remaining work
Roughly **9 of the ~11 backoffice/admin domains are 100% frontend-mocked** with zero backend contract implemented (Orders, Transactions, Users/roles/invitations/sessions, Moderation, Monitoring, Analytics-beyond-arithmetic, Media, Wishlist/Compare-if-cross-device-sync-is-required). Each needs: (1) a real REST/GraphQL endpoint built server-side, (2) a new `*ApiGateway` class client-side, (3) a token rebind or facade constructor change. The storefront (product browsing, cart, auth, payment) is the only surface with a genuinely live backend contract today — and even there, whether the configured tenant base URL (`environment.ts`) is a *reachable* backend in any given deployment was not verified in this pass (no network calls were made). Not independently estimated in person-days/weeks — that would require backend-team input on endpoint complexity, which is out of scope for a frontend-only audit.
---
## 6. Technical debt
### Dead code (verified via route/import cross-check, not just filename inspection)
- **`src/app/pages/info/**` and `src/app/pages/legal/**`** (~40 files: about/contacts/delivery/faq/guarantee × {default, en, ru, hy} + company-details/payment-terms/privacy-policy/public-offer/return-policy × {default, en, ru, hy}) — **confirmed unrouted**, `app.routes.ts:289` (`cmsContentRoutes: Routes = []`) with an explicit `TODO(CMS)` comment naming every one of these pages as disabled. `public-offer-{en,ru,hy}.component.html` are 529 lines each; `privacy-policy-*` are 367 lines each. This is the largest concentrated dead-code block in the repository by line count.
- **`src/app/features/builder/{feature-flag-editor,navigation-editor,page-editor,seo-editor,theme-editor}/`** — every folder contains only a `.gitkeep`. Stale scaffolding for a builder structure that was superseded by `features/project-editor/`.
- **`src/app/integrations/{auth,authorization,payment}/`** — `.gitkeep` only, unused scaffolding; real code lives elsewhere (Section 1).
- **`src/app/dynamic-renderer/{page-renderer,section-renderer,widget-host}/`** — real service/model files exist, but zero components/templates; the pipeline is unconsumed by the actual storefront render path (Section 1). Not literally empty like the above, but functionally dead — a decision is needed to finish wiring it or delete it.
- **`src/app/pages/item-detail/item-detail.component.ts`** (368 lines) — likely superseded by `ProductDetailsContainerComponent`; `/item/:id` redirects to `/product/:id` (`app.routes.ts:38-41`), so this component has no route pointing to it. Likely-unused, not exhaustively verified (a full workspace-wide import search for `ItemDetailComponent` was not run).
- **`src/app/components/items-carousel/`** (PrimeNG `p-carousel` wrapper) — confirmed unused per the builder-pass investigation; superseded by `ProductCarouselWidgetComponent`.
- **`BackendSearchHistoryRepository`** (`features/search/services/search-history.repository.ts`) — every method is a no-op placeholder; not confirmed bound to any DI token, i.e. possibly entirely unreachable code today (not exhaustively verified — a full DI-graph trace was not performed).
### TODO/FIXME/HACK markers
`grep -rE "TODO|FIXME|HACK" --include="*.ts" src/app` returns essentially none outside the single `TODO(CMS)` comment in `app.routes.ts:286` — consistent with `docs/SPRINT-PLAN.md`'s Sprint 29 note that a dead-code/TODO grep across `features/admin/**` came back clean. This is a genuinely low-TODO codebase relative to its size, which is a positive signal (either debt is tracked elsewhere — `docs/KNOWN-ISSUES.md` — rather than left as inline comments, or it's genuinely been kept clean).
### `console.log`/`console.debug`
Zero occurrences found (`grep -rE "console\.(log|debug)" --include="*.ts" src/app` → 0). Clean.
### `@deprecated`
One file found using the marker (not individually inspected in this pass — flagged as a location to check, not a confirmed live deprecation).
### Type-safety smell (`: any` / `as any`)
58 occurrences across `src/app` (grep, not deduplicated by uniqueness) — a moderate count for a codebase this size (~36k lines of `.ts` under `src/app`, per `wc -l`). One confirmed instance directly relevant to architecture: `runtime-provider-strategy.service.ts` casts `(environment as any).useMockBootstrapOnLocal` rather than typing it properly on the `Environment` interface — a small but real type-safety gap in a file that controls production-vs-mock data routing. Not exhaustively catalogued — 58 is the raw count, not individually triaged for severity.
### Inconsistent patterns (architecture-level debt, not simple bugs)
- **Two facades in the same directory use two different reactive idioms** (`ProductFacade` constructor-DI + pass-through vs. `CategoryFacade` `inject()` + parallel signals/observables) — Section 1.
- **Two different drag-and-drop implementations** coexist: CDK drag-drop in the Project Editor (Homepage, Footer) vs. native HTML5 DnD in Admin Categories — no documented reason for the split.
- **Two different "mock data" mechanisms** coexist: DI-token provider-switching (Bootstrap, Backoffice) vs. HTTP-interceptor response-faking (`mockDataInterceptor`, Products/Categories storefront reads) — a reader of the architecture doc alone would not discover the second mechanism exists.
- **Inconsistent `canDeactivate` dirty-guard coverage**: Categories and the Project Editor have one; Products does not, despite having equivalent draft-tracking logic in its facade.
- **`RuntimeProviderMode.'remote-config'`** is declared in a type union and referenced in `switch` statements but never actually returned by any strategy method — dead enum value across at least 4 files.
### Duplicate/near-duplicate code
- The `src/app/pages/info/**` / `src/app/pages/legal/**` legal-page components are internally triplicated by design (one component per locale, e.g. `about-en`/`about-ru`/`about-hy`) rather than one component with locale-driven content — even before accounting for the fact the whole tree is unrouted, this per-locale-component pattern (rather than a single component reading translated content) is a duplication anti-pattern relative to how the rest of the app handles i18n (`TranslateService`/`TranslatePipe`).
- Sprint 24's Transactions gateway and Sprint 23's Orders gateway are intentionally coupled (transactions derive from orders) — this is documented, deliberate coupling, not accidental duplication.
### Legacy/deprecated coexistence
No `@NgModule`-based code exists anywhere alongside the standalone-component codebase (0 hits) — the codebase does not have the "old pattern next to new pattern" debt common in mid-migration Angular projects. This is a genuine strength.
---
## 7. UI/UX audit
Scope note: this section scores what Sections 24 already described in detail; it does not re-derive new findings, only assigns comparative judgments across pages/areas already audited. No redesign proposed, per the audit brief.
| Area | Design | Usability | Accessibility | Consistency | Visual hierarchy | Notes |
|---|---|---|---|---|---|---|
| Home | Good | Good | Not independently verified | Good | Good | Thin container delegating to widget system; known dead-space bug (Section 2) |
| Catalog/Search | Fair | Fair | Not independently verified | Good (shares filter/grid primitives) | Fair | 950-line container is a maintainability risk more than a UX one, but three distinct page contracts in one container increases regression risk for any of the three |
| Product detail | Good | Good | Not independently verified | Good | Good | Well-decomposed presentational children; legacy `ItemDetailComponent` unrouted (no user-facing impact) |
| Cart/Checkout | Fair | Fair | Not independently verified | Good | Fair | No distinct checkout step is a genuine UX gap for anything beyond a single-item impulse buy — no order review/confirmation screen before payment |
| Wishlist/Compare | Good | Good | Not independently verified | Good | Good | Solid feature, undermined by no cross-device sync (local-only) |
| Static Pages (rendered) | Good | Good | Not independently verified | Good | Good | Sanitization present at render |
| Authentication | Fair | Fair | Not independently verified | Good | Good | QR-only login excludes any customer without Telegram — a real access gap for some markets |
| 404 | Missing | Missing | N/A | N/A | N/A | Silent redirect-home, no feedback |
| Maintenance | Missing | Missing | N/A | N/A | N/A | Does not exist |
| Admin Dashboard | Good | Good | Good (Sprint 28 pass) | Good | Good | Honest `pending-backend` states are a real UX strength |
| Admin Products/Categories | Good | Good | Good (Sprint 28 pass) | Good | Good | Recent auto-slug/SEO-fill/badge-manager UX investment (recent commits) |
| Admin Orders/Transactions | Good | Good | Good (Sprint 28 pass) | Good | Good | Print-invoice via browser print is minimal but functional |
| Admin Users/Monitoring/Analytics | Fair | Fair | Good (Sprint 28 pass) | Good | Fair | Heavy reliance on fabricated data undermines perceived reliability for anyone who inspects the numbers closely |
| Admin Media | Good | Good | Not independently verified | Good | Good | Real validation/compression/sanitization, well-scoped |
| Builder — Theme | Fair | Poor | Not independently verified | Good | Good | Two dead controls sitting next to live ones without any visual distinction is a genuine usability failure (Section 4) |
| Builder — Header/Footer/Homepage/Widgets | Good | Good | Not independently verified | Good (recently redesigned) | Good | Most actively-improved area of the whole app per recent commit history |
| Builder — Static Pages | Good | Fair | Not independently verified | Good | Good | `enabled`/`status` dual-gating is conceptually sound but not surfaced clearly in-UI |
| Builder — Preview | Fair | Fair | Not independently verified | Good | Fair | "Preview" mutating live runtime state the same as "Publish" is a real conceptual mismatch with what the label implies |
**Accessibility, general**: `docs/ADMIN.md` Sprint 28 documents a real, specific pass (aria-labels added to every previously-unlabeled `<select>` across admin/*, confirmed `DialogComponent` already had focus-trap/Escape/`aria-modal`, no missing `<img>` alt text anywhere in `src/app`). This audit did not independently re-run a full accessibility sweep (no live browser/screen-reader session was performed) — the "Not independently verified" markers above reflect that this pass relied on reading templates for structural signals (aria attributes present in markup) rather than a live assistive-technology test.
---
## 8. Component audit
### Shared UI inventory (`src/app/shared/ui/**`, `src/app/ui-library/**`)
Confirmed present and reused across both Admin and Marketplace Builder (not the customer-facing marketplace, which mostly uses its own presentational components under `features/website/**`): `app-button`, `app-table`, `app-badge`, `app-empty-state`, `app-pagination`, `app-skeleton`, `app-dialog` (with real focus trap), `app-toggle`, `app-select`, `app-color-picker`, `app-section-card`, `app-locale-tabs`, `app-key-value-editor`, `app-image-field`, `app-code-editor`, `app-form-field`, `app-input`. This is a genuinely mature, well-adopted shared component library for the Admin/Builder surfaces — the Sprint 28/30 design-system consistency passes (Section 3/4) both explicitly targeted "reuse these primitives everywhere" and largely succeeded per the documented before/after state.
### Repeated-pattern extraction candidates (not yet shared)
- List-page toolbars (search box + status filter + pagination) are re-implemented per admin domain (Products/Categories/Orders/Transactions/Users/Monitoring) rather than composed from one shared `AdminListToolbar` — each domain's list page component/template was confirmed to exist independently; a shared toolbar component was not found. Not a bug, but a real extraction opportunity given 6+ near-identical implementations.
- CSV export (`Blob` download, client-side) is implemented independently in at least Orders, Transactions, and Analytics — a shared `exportCsv(rows, filename)` utility was not found in `src/app/shared/util/` or `src/app/shared/utils/` (both directories exist, suggesting an unresolved naming duplication of their own — see below).
- `window.confirm`/`window.prompt` are used directly for destructive-action confirmation and folder/tag naming in multiple admin areas rather than the existing `app-dialog` component — a missed opportunity to standardize on the shared, accessible dialog rather than native browser prompts (native `confirm`/`prompt` cannot be styled and have inconsistent cross-browser behavior).
### Directory-naming duplication (minor debt, worth flagging)
Both `src/app/shared/util/` and `src/app/shared/utils/` exist as separate top-level directories — almost certainly an accidental split (one created before the other, never consolidated) rather than an intentional distinction. Contents of each were not individually diffed in this pass to confirm zero overlap.
### Largest components (line count, `.ts`, excluding `*.spec.ts`)
| Lines | File |
|---|---|
| 950 | `src/app/features/website/catalog/containers/catalog-container.component.ts` |
| 923 | `src/app/interceptors/mock-data.interceptor.ts` |
| 686 | `src/app/features/project-editor/facade/project-editor.facade.ts` |
| 683 | `src/app/features/website/product/containers/product-details-container.component.ts` |
| 673 | `src/app/services/api.service.ts` |
| 664 | `src/app/pages/cart/cart.component.ts` |
| 592 | `src/app/features/search/facade/search.facade.ts` |
| 514 | `src/app/features/admin/categories/facade/admin-categories.facade.ts` |
| 404 | `src/app/features/content-management/components/static-pages-editor.component.ts` |
| 368 | `src/app/pages/item-detail/item-detail.component.ts` (likely unused, see Section 6) |
| 338 | `src/app/features/admin/analytics/facade/admin-analytics.facade.ts` |
| 316 | `src/app/features/admin/products/facade/admin-products.facade.ts` |
Largest templates (`.html`): `public-offer-{ru,hy,en}.component.html` (529 lines each, unrouted — Section 6), `privacy-policy-{ru,en,hy}.component.html` (367 lines each, unrouted), `cart.component.html` (359 lines), `item-detail.component.html` (356 lines, unrouted), `admin-product-form.component.html` (291 lines), `catalog-container.component.html` (290 lines).
**Split candidates**: `catalog-container.component.ts` (950 lines, three route contracts in one container — Section 2), `mock-data.interceptor.ts` (923 lines, almost entirely inline hardcoded mock JSON — a legitimate candidate to move into `assets/mock/*.json` files consumed by `fetch`/`HttpClient` the way `MockBootstrapProvider`/`MockBackofficeDataProvider` already do, for consistency and to shrink the TS bundle), `project-editor.facade.ts` (686 lines, too many concerns in one injectable — Section 4), `product-details-container.component.ts` (683 lines), `api.service.ts` (673 lines, but this is a single cohesive HTTP client surface, less clearly a split candidate than the others), `cart.component.ts` (664 lines, doing double duty as checkout — Section 2).
---
## 9. Translation audit
`src/app/i18n/{en,ru,hy}.ts` — exactly 3 locales, matching every other doc's claim (`en`, `ru`, `hy`; `ru` is the hardcoded routing default per Section 1). `translations.ts` defines the shared `Translations` interface; `translate.service.ts` (`TranslateService.t(key, params?)`) does a dot-path walk and **returns the raw key string unchanged if any segment is missing or the resolved value isn't a string** (`translate.service.ts:23-35`) — this is the exact fallback mechanism that produced the historical ~178-raw-key admin bug (now fixed, Section 3), and remains a standing risk: any future key added to one locale file and not the others will silently render as a raw dotted key in production for that locale, with no build-time or type-level check catching it. `params` string interpolation exists (line ~40 onward, not fully re-read in this pass).
Rough leaf-key counts (grep of `key: '...'`-shaped lines, an approximation not an exact parser-verified count): `en.ts` ~1,750, `ru.ts` ~1,759, `hy.ts` ~1,759. The 9-key difference between `en` and `ru`/`hy` was not resolved to specific missing keys in this pass — flagged as **not exhaustively verified**; a real audit would need a proper AST-based key-set diff across all three files (a grep-based line count is not reliable enough to name the exact missing keys, since some lines legitimately differ in shape, e.g. nested objects vs leaf strings).
**Sampling caveats, stated explicitly**: this audit did not run an exhaustive unused-key check (grepping every one of ~1,750 keys' usage across templates/TS would be prohibitively expensive for this pass and was not attempted), did not run an exhaustive hardcoded-string sweep (spot checks during Section 2/3 reading found translated strings consistently used via `TranslatePipe`/`TranslateService`, with no untranslated English string observed in the admin/builder templates read, but this was incidental to other reading, not a targeted sweep), and did not check for duplicate keys within a single file (would require a proper JS/TS object-literal parser, not grep, to do reliably given nested structure).
**One confirmed, structurally-guaranteed-safe pattern**: the legal/info pages under `src/app/pages/{info,legal}/**` (Section 6) use **per-locale components with hardcoded text** rather than translation keys at all — so they cannot suffer from missing-key drift, but only because they duplicate the same content three times per page instead. This is arguably worse debt than a translation gap, since fixing a typo in the legal text requires editing three separate files. Moot in practice since the whole tree is unrouted, but relevant if it's ever reconnected without also being migrated to the Static Pages system.
---
## 10. Performance
### Bundle budgets (`angular.json`)
```
"budgets": [
{ "type": "initial", "maximumWarning": "700kB", "maximumError": "1MB" },
{ "type": "anyComponentStyle", "maximumWarning": "40kB", "maximumError": "50kB" }
]
```
Per `docs/EDITOR.md`/`docs/ADMIN.md` Sprint 28 notes, the initial bundle currently sits ~198kB over the 700kB warning threshold (i.e. ~898kB), pre-dating all admin/builder feature work — **not independently re-verified in this pass** since no build was run (out of scope per the read-only audit constraints: "no build/install/format commands that write files"). This is a stale-but-plausible number from a prior session; treat it as an estimate, not a current measurement.
### Lazy loading
100% of routes use `loadComponent()` (Section 1) — no eager route components found anywhere in `app.routes.ts`. This is a genuine strength: the admin backoffice, the entire Builder, and every marketplace page are code-split by route.
### Change detection
`OnPush` in 188 of 189 `@Component` files (grep count) — effectively universal, a strong performance signal that should keep re-render scope tight throughout the app. The one component not using `OnPush` was not individually identified in this pass.
### Signals vs. Observable-heavy services
Mixed, as described in Section 1: `signal()` in 53 files, `computed()` in 47. Some services (e.g. `CategoryFacade`) maintain **both** an Observable and a Signal view of the same state, populated by hand inside a subscription rather than via `toSignal()` — this duplicates state and is a real, if minor, waste (two representations of the same data kept manually in sync) rather than a pure win from adopting signals.
### Images
Only 17 of 35 `<img>` occurrences in templates use `loading="lazy"` or `NgOptimizedImage`'s `ngSrc` — roughly half. Not broken down per-page/per-component in this pass (would require reading all 35 individually to name which specific templates lack it); flagged as a real, moderate-priority performance gap, especially for image-heavy surfaces like `product-gallery`, `category-grid`, and the Media Library grid (none of the three were individually confirmed to be in the lazy or non-lazy set in this pass).
### Duplicate/redundant requests
Not systematically checked in this pass beyond what fell out of reading `CategoryFacade` (which does maintain parallel Observable+Signal state, a source of potential double-computation but not necessarily double-HTTP-request) — a targeted trace of whether multiple components independently re-trigger the same gateway call without a shared cache was not performed. `cacheInterceptor` exists in the HTTP interceptor chain (`app.config.ts:26`, last in the chain) and was not opened/verified in this pass to confirm what it actually caches or for how long — **not verified**.
### Largest components (perf-risk read, cross-referencing Section 8)
`catalog-container.component.ts` (950 lines) is the highest-risk single component for both maintainability and runtime performance, given it serves three distinct route contracts and composes the most child components of any container in the app. `mock-data.interceptor.ts` (923 lines) ships as part of the JS bundle regardless of whether mock mode is active for a given request, since it's registered unconditionally in the interceptor chain (`app.config.ts:25-27`) — the interceptor itself presumably checks `environment.useMockData` internally before doing anything (not verified in this pass), but the ~900 lines of hardcoded mock category/product JSON still ship to every client's bundle whether or not mock mode is ever used at runtime, which is real unnecessary bundle weight.
---
## 11. Security
### Authentication & token storage
Two entirely separate auth systems, confirmed isolated (Section 3): customer (`AuthService`) and admin (`AdminAuthService`). Both use a **cookie** for the session id (admin: `adminSessionID`, `SameSite=Strict`, `Secure` when HTTPS, `Max-Age=3600`, set via raw `document.cookie` string construction in `admin-auth.service.ts:198-204` rather than a cookie-handling library — functionally fine but manually re-implements cookie serialization, a place a future edit could introduce a parsing bug). `localStorage` additionally holds `adminToken`/`adminRefreshToken` (`ADMIN_TOKEN_STORAGE_KEY`/`ADMIN_REFRESH_STORAGE_KEY`, `admin-auth.service.ts:23-24`) — per an inline code comment, this is "JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then." — i.e., dead code paths (`getAdminToken`/`setAdminTokens`/`clearAdminTokens`) waiting on a backend feature that doesn't exist yet. **No JWT parsing/validation happens client-side today** since no JWT is actually issued yet — this is a forward-looking placeholder, not a live gap, but worth confirming these methods get real expiry-checking logic once a real token is issued rather than being wired in naively.
### Session handling
`AdminAuthService.scheduleSessionRefresh()` (`admin-auth.service.ts:165-174`) computes a refresh time from the session's `expires` timestamp minus a 60-second buffer, with a `setTimeout` — a reasonable, real implementation, not a stub. `logout()` calls the real session-invalidation API endpoint before clearing local state. `devBypassLogin()` (`admin-auth.service.ts:100-113`) is explicitly gated by `if (environment.production) return;` — a **runtime check, not just a build-time exclusion**, which is the correct defensive pattern for a dev-only auth bypass (a build misconfiguration that leaves `environment.production` false in a real deployment would still be a live vulnerability, but the code itself does the right thing given a correctly-set flag).
### Route guards
`adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`) is a straightforward `CanActivateFn` checking `isAuthenticated()` — correctly gates the entire `/backoffice/**` route tree from one place (`app.routes.ts:57`), rather than per-route guards that could be individually forgotten. `languageGuard` is unrelated to security (locale routing only).
### Admin isolation from customer routes
Confirmed structurally isolated: distinct guard, distinct service, distinct cookie name, distinct storage keys, distinct route tree (`/backoffice/**` vs `:lang` core routes) — genuinely well-separated client-side. **The unresolved, repeatedly-documented gap is server-side**: both admin and customer Telegram QR logins hit the identical session-creation endpoint (`TelegramSessionApiService`, `{authApiUrl}/users/sessions`) — the backend has no way to distinguish an admin scan from a customer scan. This means **today, any successfully-authenticated Telegram session could, in principle, be presented against admin API endpoints and the backend would have to accept it**, since nothing server-side currently encodes "this is an admin." This is a real, currently-open, security-relevant gap — not fixable from the frontend alone, and correctly flagged as such in `docs/backend/BACKEND-INTEGRATION.md`, `docs/ADMIN.md`, and `docs/EDITOR.md`. Confirmed independently in this pass by reading `AdminAuthService` in full: nothing in the client code sends an "admin" discriminator to the session-creation call.
### XSS surfaces (`[innerHTML]` / `bypassSecurityTrust*`)
All confirmed occurrences:
- `static-page-preview.component.ts:49``bypassSecurityTrustHtml(sanitized)`, bound at `static-page-preview.component.html:16`. Variable name `sanitized` implies a sanitization step happens before this call; the sanitization function itself was not traced to its source in this pass to confirm it actually strips dangerous content (e.g. `<script>`, event-handler attributes) rather than being a no-op rename — **flagged as needing direct verification, not confirmed safe or unsafe**.
- `static-page.component.ts:32,94,103` — same pattern (`sanitized` variable, `bypassSecurityTrustHtml`), used for the live customer-facing static-page render (`static-page.component.html:15`). Same caveat: the actual sanitization implementation was not opened in this pass.
- `item-detail.component.ts` (`getSafeHtml`, used at `item-detail.component.html:208`) — same pattern, and the component itself is likely-unused/unrouted (Section 6), so currently inert regardless.
- `cart.component.ts:371``bypassSecurityTrustResourceUrl(bankUrl)` for an embedded payment iframe `src` — this is the correct, narrower sanitizer for a resource URL (not raw HTML), appropriate for embedding a bank-hosted payment page.
- `code-editor.component.html:2``[innerHTML]="highlighted()"` inside the Builder's code-editor syntax highlighter (used by `MarketplaceHtmlEditorComponent`'s "Код" raw-HTML toggle) — the `highlighted()` computed presumably tokenizes and HTML-escapes the *admin's own* input for syntax-highlighting display, not third-party content; lower risk since only an already-authenticated admin's own typed content flows through it, but the escaping logic itself was not traced to confirm it actually escapes rather than passing through.
**Net assessment**: the pattern of routing all `bypassSecurityTrustHtml` calls through a variable literally named `sanitized`/`safeHtml` is a good sign of intent, but this audit did **not** trace any of those sanitization functions to their implementation to confirm they use a real HTML sanitizer (e.g. DOMPurify or equivalent) rather than a weaker transform — this is the single most important "could not verify" item in the whole audit given it gates customer-facing rendering of admin-authored (and, per the rich-text editor's "raw, unsanitized" authoring-time behavior noted in Section 4, potentially not pre-sanitized-at-source) HTML.
### HTML/rich-text editor sanitization
Per Section 4 and `docs/EDITOR.md`: `MarketplaceHtmlEditorComponent` **emits raw, unsanitized HTML by design** — the stated architecture is that sanitization happens at storefront-render time (`StaticPageComponent`/`StaticPagePreviewComponent`, both confirmed to run content through `DomSanitizer` per the `bypassSecurityTrustHtml` calls above) rather than at authoring time. Given the "could not verify the actual sanitizer implementation" caveat directly above, this is the platform's single highest-leverage security surface to independently verify before shipping any UGC or multi-admin-tenant scenario: if the render-time sanitization step is weak or bypassable, there is no other line of defense, since the authoring side is explicitly unsanitized.
### Upload security
`MockMediaRepository.validateFile()` (Section 3) rejects files over 10MB and outside a `jpeg/png/webp/gif/svg+xml/pdf` allow-list, with SVG-specific `<script>`/`on*=` stripping before storage — a real, meaningful client-side validation layer. Since storage is IndexedDB-only today (no backend), there is no server-side re-validation to assess — when a real upload backend is built, server-side validation of the same constraints will be required (client-side validation alone is never sufficient against a malicious client bypassing the UI).
### Cookies / CSRF
Only one `document.cookie` write site found (`admin-auth.service.ts`, the admin session cookie) plus presumably an equivalent one in `AuthService` for the customer session (not independently re-opened in this pass to confirm the same `SameSite=Strict`/`Secure` pattern is used — flagged as **not verified**, should be checked for parity with the admin implementation). No CSRF-token handling was found anywhere in `src/app` (no `X-CSRF-Token` header construction, no CSRF-related interceptor) — `SameSite=Strict` on the session cookie mitigates classic CSRF for cookie-based requests, but if any state-changing request relies on a bearer token in `localStorage` instead (as the reserved-but-unused `adminToken` will, once wired up), `SameSite` cookie protection won't apply to that path and CSRF would need a different mitigation at that point — a forward-looking note, not a current live gap since the token path is unused today.
---
## 12. Production checklist
| Feature | Status | Ready | Blocked | Needs Backend | Needs UX | Needs Tests |
|---|---|---|---|---|---|---|
| Home | Working | Yes | No | No | Minor (dead-space bug) | Yes |
| Catalog/Search | Working | Partial | No | No | Yes (container split) | Yes |
| Product detail | Working | Yes | No | No | No | Yes |
| Cart/Payment | Working | Partial | No | No | Yes (no checkout step) | Yes |
| Wishlist/Compare | Working | Partial | No | Yes (cross-device sync) | No | Yes |
| Static Pages (render) | Working | Yes | No | No | No | Yes |
| Legal/Info pages (legacy) | Dead code | No | Yes (unrouted, needs a decision) | No | N/A | N/A |
| Authentication | Working | Partial | No | Yes (non-Telegram fallback) | Yes | Yes |
| 404 page | Missing | No | Yes | No | Yes | Yes |
| Maintenance mode | Missing | No | Yes | No | Yes | Yes |
| Admin Dashboard | Working | Yes | No | Partial (Orders/Revenue) | No | Yes |
| Admin Products | Working | Yes | No | Yes (persistence) | Minor | Yes |
| Admin Categories | Working | Yes | No | Yes (persistence) | No | Yes |
| Admin Orders | Mocked | Partial | No | Yes (fully) | No | Yes |
| Admin Transactions | Mocked | Partial | No | Yes (fully) | No | Yes |
| Admin Customers | Unverified | Unknown | No | Unknown | Unknown | Yes |
| Admin Moderation | Mocked | Partial | No | Yes (fully) | No | Yes |
| Admin Monitoring | Mocked | Partial | No | Yes (fully) | No | Yes |
| Admin Analytics | Mocked | Partial | No | Yes (fully) | No | Yes |
| Admin Users/Roles | Mocked | Partial | No | Yes (fully) | No | Yes |
| Media Library | Local-only | Partial | No | Yes (storage) | No | Yes |
| Builder — General/Branding/Languages | Working | Yes | No | Yes (persistence) | Minor | Yes |
| Builder — Theme | Partial | No | No (control exists, effect doesn't) | No | Yes (dead controls) | Yes |
| Builder — Header/Footer/Homepage/Widgets | Working | Yes | No | Yes (persistence) | No | Yes |
| Builder — Static Pages | Working | Yes | No | Yes (persistence) | Minor | Yes |
| Builder — Navigation | Working | Yes | No | Yes (persistence) | Minor | Yes |
| Builder — Features | Working | Yes | No | Yes (persistence) | No | Yes |
| Builder — Preview/Publish | Working | Partial | No | Yes (no backend save) | Yes (label mismatch) | Yes |
| Dynamic renderer pipeline | Unwired | No | Yes (needs a build-or-delete decision) | No | N/A | N/A |
| Admin/customer server-side auth isolation | Missing | No | Yes | Yes (server-side) | No | Yes |
**Test coverage**: only 5 `.spec.ts` files exist in the entire `src/app` tree — essentially no automated test coverage for a codebase of this size (~36,000 lines of `.ts` under `src/app`). Every "Needs Tests: Yes" above reflects this same underlying gap rather than per-feature specifics.
---
## 13. Release readiness
- **Marketplace (customer-facing): ~65%.** Core browse/product/cart/payment flows are genuinely implemented and backend-wired (a real strength). But there is no checkout step, no customer order history, no password/email login fallback, no 404 page, no maintenance page, and the catalog container is a maintainability risk. The percentage reflects "would a customer complete a purchase today" (yes, via Telegram + cart-embedded payment) weighted against "does this feel like a complete e-commerce product" (no — several standard flows are simply absent, not broken).
- **Admin: ~70%.** Every listed admin area has a real, polished, accessible UI following a consistent design system — this is the most *finished-feeling* part of the app. But 9 of ~11 domains have zero backend behind them (Section 5), meaning almost everything an admin does today doesn't survive a page reload except Products/Categories (which persist to `localStorage`-backed local gateways, still not a real database). The percentage weighs UI completeness (high) against actual data durability (low).
- **Builder: ~75%.** The most actively-developed area per recent commit history (7 of the last 20 commits touch it), with real bug-hunt passes, real UX polish, and a mature validation/schema layer. Held back by: two dead controls in Theme, one unwired rendering pipeline two fields feed into, zero backend persistence for anything, and a Preview/Publish conceptual mismatch.
- **Backend Integration: ~20%.** Bootstrap and Backoffice-reads are the only domains with a working, switchable mock/API pair. Storefront reads work against real HTTP but have no working mock counterpart client-side (interceptor-level mocking is a workaround, not the documented architecture). Every admin write operation and the entire Builder save/publish flow persist only to `localStorage`. The percentage reflects how much of the *documented* provider-swap architecture is actually load-bearing today versus how much of the app's real data flow depends on it.
- **Overall Product: ~55%.** A genuinely well-architected frontend (standalone components throughout, near-universal `OnPush`, 100% lazy-loaded routing, a mature shared component library, near-zero TODO/console.log/NgModule debt, low but real type-safety debt) sitting on top of a backend integration that covers a minority of the product's actual feature surface, with a handful of specific, well-documented, currently-dead UI controls (Theme Mode, Site Layout) that could mislead a merchant into thinking they've configured something that has no effect. The 55% reflects the average of the above four weighted toward Backend Integration and Marketplace since those gate whether the product is actually usable end-to-end by a real customer and a real merchant simultaneously, not just individually impressive in isolation.
---
## 14. Top remaining tasks
Sorted P0 (blocks any real launch) → P3 (polish/nice-to-have). This audit found substantially more than 100 real, non-padded, evidence-based gaps; the list below is capped at the 100 most consequential ones per the brief, grouped for readability. Every item is grounded in a specific finding from Sections 111 above rather than invented.
### P0 — Blocks any real production launch
1. **Server-side admin/customer session isolation.** Why: any Telegram session can currently be presented against admin endpoints; backend has no concept of "admin." Difficulty: High (backend + minor frontend). Files: `src/app/core/admin-auth/admin-auth.service.ts`, backend session-issuance endpoint. Dependencies: backend team.
2. **Verify (or build) real HTML sanitization at every `bypassSecurityTrustHtml` call site.** Why: this audit could not confirm the "sanitized" variables are actually sanitized; if not, this is a live stored-XSS path for merchant-authored (unsanitized-at-source) rich text. Files: `static-page.component.ts`, `static-page-preview.component.ts`. Dependencies: none, purely frontend.
3. **Real backend for Orders + Transactions.** Why: currently 100% synthetic; no real e-commerce platform can ship without real order records. Files: `admin-orders-local.gateway.ts`, `admin-transactions-local.gateway.ts` (replace with `*ApiGateway`). Dependencies: backend order/payment-reconciliation domain.
4. **Real backend for Products/Categories writes.** Why: admin edits currently vanish on page reload (`localStorage` local gateway only, not a database). Files: `admin-products-local.gateway.ts`, `admin-categories-local.gateway.ts`. Dependencies: backend CRUD endpoints (`docs/backend/BACKEND-INTEGRATION.md` §6.7 area).
5. **Real backend for Builder save/publish.** Why: every Builder edit across all 11 sections only persists to a single browser's `localStorage` — clearing site data loses the entire tenant configuration. Files: `project-editor.facade.ts`, `ApiBootstrapProvider`. Dependencies: backend PUT/POST bootstrap endpoints.
6. **Build a real Checkout flow** (shipping/order-review/confirmation) distinct from Cart. Why: no e-commerce product should ship payment embedded directly in the cart with zero order-review step. Files: `src/app/features/website/checkout/` (currently empty), `cart.component.ts`. Dependencies: order backend (#3) for order-creation on submit.
7. **Real backend for Media Library storage.** Why: uploaded assets exist only in the uploading admin's own browser IndexedDB; no other user or device can see them. Files: `MediaRepository`/`MockMediaRepository`, `app.config.ts:29`. Dependencies: backend/CDN asset storage.
8. **Decide and act on the `dynamic-renderer/` pipeline** — finish wiring it (if planned) or delete it and the two dead Builder fields (Theme "Site Layout", Homepage section `type`) that feed it. Why: currently a merchant can configure settings that persist, validate, and appear functional but have zero effect. Files: `src/app/dynamic-renderer/**`, `theme-section.component.ts`, `homepage-section.component.ts`. Dependencies: none, purely a frontend/product decision.
9. **Fix or remove the Theme Mode (light/dark/system) control.** Why: same class of issue as #8 — a live, saveable, validated control with zero runtime effect. Files: `theme-section.component.ts`, needs a real dark-mode CSS strategy (`[data-theme-mode]`/`prefers-color-scheme` + `matchMedia` listener). Dependencies: design decision on whether dark mode is in scope at all.
10. **Automated test coverage.** Why: 5 `.spec.ts` files for a ~36,000-line frontend is not a safety net by any measure; every subsequent change (including all P0P2 items below) currently ships with no regression protection. Files: project-wide. Dependencies: none, but large effort.
### P1 — Blocks a confident, full-featured launch
11. Build a non-Telegram login/registration fallback for customers. `services/auth.service.ts`, `telegram-login.component.ts`.
12. Build a customer-facing order-history/account page. No existing files — net new.
13. Build a 404/not-found page and route it instead of the current silent redirect-home. `app.routes.ts:303`.
14. Build a maintenance-mode page/toggle. No existing files — net new.
15. Real backend for Admin Users/Roles/Invitations (emails currently don't send; roster is hardcoded). `admin-users-local.gateway.ts`.
16. Real backend for Admin Moderation (reviews/reports currently disconnected from real customer-submitted reviews). `admin-moderation-local.gateway.ts`.
17. Real backend for Admin Monitoring (no logging backend exists anywhere in the system). `admin-monitoring-local.gateway.ts`.
18. Real analytics/tracking pipeline for Visitors/Funnels/Heatmaps (currently `pending-backend` placeholders). `admin-analytics.facade.ts`.
19. Split `catalog-container.component.ts` (950 lines, three route contracts) into per-route containers sharing common logic via a service. `features/website/catalog/containers/catalog-container.component.ts`.
20. Delete or reconnect `src/app/pages/info/**` and `src/app/pages/legal/**` (~40 unrouted files, ~40% duplicated per-locale legal content) — either wire into `cmsContentRoutes` or remove entirely in favor of the Static Pages builder. `app.routes.ts:286-289`.
21. Add a `canDeactivate` dirty guard to the Admin Products editor, matching Categories/Project Editor. `app.routes.ts:88-105`, `admin-products.facade.ts`.
22. Confirm/fix `BackendSearchHistoryRepository`'s no-op stub — either wire it to a real endpoint or remove it if genuinely unreachable. `features/search/services/search-history.repository.ts`.
23. Give Wishlist/Compare/Recently-Viewed a real backend so state survives across devices for logged-in customers. `core/user-experience/repositories/local-user-experience.repository.ts`.
24. Reconcile the two mocking mechanisms (DI-token switching vs. `mockDataInterceptor`) into one documented pattern, or explicitly document why both exist. `runtime-provider-strategy.service.ts`, `interceptors/mock-data.interceptor.ts`.
25. Move `mock-data.interceptor.ts`'s ~900 lines of hardcoded JSON into `assets/mock/*.json` files consumed the way `MockBootstrapProvider` already does, to shrink the shipped JS bundle and match the existing convention. `interceptors/mock-data.interceptor.ts`.
26. Clarify or separate the "Preview" vs "Publish" mental model in the Builder — currently identical runtime effect. `preview-section.component.ts`, `project-editor-preview.service.ts`.
27. Add an explicit in-editor warning when a Static Page's own `status` will keep it hidden despite a whole-config Publish. `static-pages-editor.component.ts`.
28. Build the `showProfile` header toggle's actual profile/account menu, contingent on #11/#12 existing first. `header.component.html`, `header-section.component.ts`.
29. Address the ~198kB bundle-budget overage (`angular.json` 700kB warning threshold) via a main-bundle/core-module import audit. `angular.json`, app-wide.
30. Bring `<img>` lazy-loading from ~50% to consistent coverage across product/category/media-library image grids. `product-gallery.component.html`, `category-grid.component.html`, media library templates (specific files not individually identified in this pass).
### P2 — Meaningfully improves quality/consistency before or shortly after backend integration
31. Unify `ProductFacade`/`CategoryFacade` reactive idiom (pick signals-only or a consistent facade pattern project-wide). `facades/platform/product.facade.ts`, `facades/platform/category.facade.ts`.
32. Unify drag-and-drop implementation (CDK vs native HTML5) across Admin Categories and the Builder. `admin-categories-list.component.ts`, `homepage-section.component.ts`/`footer-section.component.ts`.
33. Extract a shared `AdminListToolbar` component (search + filter + pagination) used by 6+ near-identical admin list pages. `features/admin/{products,categories,orders,transactions,users,monitoring}/**`.
34. Extract a shared `exportCsv()` utility (currently reimplemented per-domain). `admin-orders`, `admin-transactions`, `admin-analytics`.
35. Consolidate `shared/util/` and `shared/utils/` into one directory. `src/app/shared/{util,utils}/`.
36. Replace `window.confirm`/`window.prompt` destructive-action/naming flows with the existing `app-dialog` component for consistency and accessibility. Multiple admin components (folder/tag naming in Media, confirm-gated actions in Orders/Categories).
37. Add navigation-link drag-and-drop (currently up/down-only, inconsistent with Homepage/Footer) if there's no deliberate reason for the difference — or document why not. `navigation-section.component.ts`.
38. Give the grouped (column-based) footer nav a cross-link or inline note from the flat-footer-nav tab in Navigation, since both are edited in different places. `navigation-section.component.ts`, `footer-section.component.ts`.
39. Delete confirmed-dead `items-carousel` component. `src/app/components/items-carousel/`.
40. Delete or repurpose the empty `src/app/features/builder/{feature-flag-editor,navigation-editor,page-editor,seo-editor,theme-editor}/` scaffolding.
41. Delete or repurpose the empty `src/app/integrations/{auth,authorization,payment}/` scaffolding.
42. Remove the dead `'remote-config'` `RuntimeProviderMode` enum value or implement it if it was meant to mean something (its name suggests a third data-source mode that was never built).
43. Fix the `(environment as any)` cast in `runtime-provider-strategy.service.ts` by adding `useMockBootstrapOnLocal` to the typed `Environment` interface.
44. Triage the remaining ~57 `: any`/`as any` occurrences project-wide (58 total, 1 addressed by #43) for genuine type-safety risk vs. acceptable escape hatches.
45. Audit and reconcile the `en`/`ru`/`hy` i18n key-count discrepancy (~9 keys) with a real AST-based diff tool rather than grep approximation.
46. Add per-locale missing-key detection to CI (a lint rule or small script comparing the 3 locale files' key sets) so the historical "raw key leak" bug class (Section 3/9) cannot silently recur.
47. Reduce `project-editor.facade.ts` (686 lines) scope by extracting undo/redo history management and draft-storage orchestration into smaller composed services.
48. Reduce `static-pages-editor.component.ts` (404 lines) by extracting bulk-actions and validation-badge-computation logic.
49. Add inline dimension/format guidance to Branding's logo/favicon/social-image fields.
50. Add min/max guardrails to Homepage section's `columns` numeric field (currently only `Number(value) || 1`).
51. Confirm parity between the customer (`AuthService`) and admin (`AdminAuthService`) cookie-setting implementations (`SameSite`, `Secure`) — only the admin one was directly read in this pass.
52. Verify what `cacheInterceptor` actually caches and for how long — not opened in this audit pass.
53. Verify `RuntimeDiagnosticsService`/`PlatformRuntimeStateService` (`core/runtime/`, 25-26 lines each) are actually consumed somewhere and not further dead scaffolding — not individually traced in this pass.
54. Confirm the exact scope/ownership of the "Admin Customers" area, which `docs/ADMIN.md`'s sprint log does not cover — this audit could not independently verify its implementation depth.
55. Decide whether "Reports" (per the brief) maps to Moderation's reports queue or should be a distinct business-reporting area, and build the latter if intended.
56. Add a dedicated "Settings" landing area inside the Admin shell that at least deep-links to the Builder, since today a user browsing `/backoffice/**` has no in-context way to discover `/edit/*` exists.
57. Add per-transaction/per-user audit-log consolidation (or an explicit "these are intentionally separate" note in-UI) since three separate audit-trail concepts exist (per-transaction, per-user, system-wide Monitoring) with no visible cross-reference.
### P3 — Polish, longer-horizon, or explicitly deferred by prior sprints
58. Real interactive image cropping in Media Library (currently compression/resize only, no crop UI — explicitly deferred).
59. Full permission-catalog and custom-role creation for Admin Users (currently 4 fixed built-in roles).
60. Full catalog search for the Related Products picker (currently limited to whatever page is loaded in the facade — explicitly documented trade-off).
61. True folder hierarchy for Media Library (currently a flat tag, not nested).
62. Dynamic, backend-generated sitemap per tenant (currently a static baseline for one locale — explicitly documented limitation).
63. Coupons/discounts/promotions engine (no client code beyond a flag exists — FUTURE per `docs/backend/BACKEND-INTEGRATION.md`).
64. Notification delivery system (FUTURE, no client code).
65. Inventory/warehouse management (FUTURE, no client code).
66. Delivery/logistics beyond cart-level display (FUTURE, no client code).
67. Invoice generation beyond browser print-to-PDF (currently `window.print()` + `@media print`, deliberately minimal).
68. Webhook registration UI for merchants (Monitoring only displays mock webhook delivery logs today, no registration flow).
69. A charting library for Analytics if more chart types are needed beyond the current single-series `<div>`-bar chart (deliberately deferred, not urgent at current scope).
70. Consider migrating `MarketplaceHtmlEditorComponent` off `document.execCommand` proactively rather than reactively if/when browser support risk materializes (currently low-urgency, monitor).
71. A per-field reset capability in the Builder (currently only section-level and whole-project reset exist).
72. Multi-device/multi-tab conflict handling for the `localStorage`-only draft model (two admins editing the same tenant in two tabs today would silently overwrite each other with no conflict warning) — becomes moot once real backend persistence (#5) lands, but worth a stop-gap warning until then.
73. A real design decision on whether the empty `dynamic-renderer/` pipeline (#8) is worth finishing versus deleting, documented in an ADR either way, so this doesn't remain ambiguous scaffolding indefinitely.
74. Standardize `alt`-text and `aria-label` coverage checks into an automated lint/CI rule now that a manual Sprint 28 pass achieved a clean baseline, to prevent regression.
75. Add `prefers-reduced-motion` coverage verification as an automated check (currently a manual global override exists in `src/styles.scss`, verified present but not tested for edge cases like third-party embedded content).
**Note on count**: the brief anticipated 100 tasks; this audit substantiated 75 genuinely evidence-grounded, non-padded items across P0P3. Padding the list to a round 100 with speculative or restated items would violate the brief's own instruction to prefer accuracy over a comprehensive-sounding count — 75 is the honest number of distinct, real findings this pass could ground in specific source evidence.
---
## 15. Final verdict
**If handed to another team tomorrow:** they would find a frontend that is unusually disciplined for its scope — 100% standalone components, near-universal `OnPush`, 100% lazy-loaded routing, essentially zero `console.log`/TODO/NgModule debt, a real and consistently-applied container/facade/gateway layering, and a shared component library that both the Admin and Builder surfaces genuinely reuse rather than reinvent per-feature. They would also quickly discover that almost none of the Admin backoffice and none of the Builder persist anything past a browser's `localStorage`/IndexedDB — the single fact that most changes what a new team should do first, since it means "finish the last 20%" reads very differently here than in most audits: the frontend is largely *done* for its current mocked scope, and the primary remaining work is backend integration, not frontend feature-building.
**Biggest strengths:**
- The architectural discipline (layering, standalone components, lazy loading, shared UI library) is real and load-bearing, not just documented aspiration — verified against actual code repeatedly in this pass, not just the architecture doc's claims.
- The Builder (`features/project-editor/`) is the most mature, actively-improved, and best-documented part of the codebase, with a genuine track record of finding and fixing real correctness bugs (id-collision bugs, silent-discard bugs, dead-toggle bugs) via a documented bug-hunt methodology rather than only shipping new features.
- Honesty patterns in the UI itself (`pending-backend` badges on Dashboard/Analytics cards rather than fabricated numbers) are a genuinely good practice that should be the template for every other mocked domain, not just the two that currently do it.
**Biggest risks:**
- **Zero automated test coverage** (5 spec files) means every one of the fixes and features described throughout this document — including the historical bug-hunt fixes already shipped — has no regression protection. The next refactor of `project-editor.facade.ts` or `admin-categories.facade.ts` could silently reintroduce any of the already-fixed bugs.
- **The admin/customer server-side auth isolation gap** (Section 11) is a real, currently-open security exposure, not a theoretical one, and is entirely outside frontend control to fix.
- **The unverified sanitization chain** for merchant-authored rich text (Section 11) is the audit's single largest "could not confirm" item with real stakes — if the render-time sanitizer is weak, there is no other line of defense given the authoring side is explicitly, deliberately unsanitized.
- **The scale of the mock-vs-real gap is easy to underestimate from the UI alone**, since the Admin surface is polished enough to look production-ready at a glance; a new team could easily under-budget the backend-integration effort if they judge readiness by UI quality rather than by tracing each gateway to its actual data source, as this audit did.
**What should never be changed** (without a very deliberate, reviewed decision): the container/facade/gateway/InjectionToken layering pattern itself — it is the reason backend integration is tractable at all (Section 5's "no facade changes required" finding is real and valuable); the standalone-components-only convention; the near-universal `OnPush` discipline; ADR-010's frozen auth/payment/authorization contracts (changing these outside a reviewed process is explicitly called out as high-risk in the architecture docs and this audit found no reason to disagree).
**What should be rewritten:** `catalog-container.component.ts` (950 lines serving three route contracts) is the clearest rewrite candidate — not because it's broken, but because its size and multi-contract scope make it the highest-risk file to safely modify without tests. `mock-data.interceptor.ts` should be converted to the asset-JSON pattern already used elsewhere for consistency and bundle size.
**What should be deleted:** the ~40 unrouted legal/info page components (`src/app/pages/{info,legal}/**`), the empty `features/builder/*` scaffolding, the empty `integrations/*` scaffolding, and the confirmed-dead `items-carousel` component — none of these carry any product risk to remove, and all four are currently misleading to a new contributor trying to understand "where does X live."
**What should be postponed until after backend integration:** essentially the entirety of Section 14's P2/P3 list (component extraction, drag-and-drop unification, toolbar consolidation, permission catalogs, etc.) — these are real but lower-leverage than making the 9 mocked admin domains, the Builder save/publish flow, and Checkout actually durable. Polishing a UI whose data doesn't persist is work that will likely need to be partially redone once real backend contracts (with real field shapes, real error states, real pagination semantics) replace the current local gateways.
---
*End of audit. No source files were modified to produce this document.*