docs: final documentation consolidation - one canonical doc set

Audited every *.md in docs/ and root. Merged five overlapping backend
docs (BACKEND_INTEGRATION.md + AUTHENTICATION.md + ERROR_CONTRACT.md +
MAINTENANCE_MODE.md + the already-archived BACKEND_API.md/
BACKEND_API_REMAINING_WORK.md) into one canonical docs/BACKEND.md
(4775 lines, 10 numbered sections) - deleted the four standalone
files outright now that their content is fully inlined.

Archived (not deleted - real historical value): ADMIN.md (Sprint
19-28 build log, sprint-report-shaped, not a living reference) and
FRONTEND-ROADMAP.md (despite its name, a shipped-history changelog
with detail no other doc has - not a forward roadmap, so keeping it
in root alongside NEXT_PHASE.md was exactly the "10 roadmaps"
confusion being cleaned up).

Deleted outright (zero value): SPRINTS.md - a leftover copy-pasted
sprint-kickoff prompt saved as a file, not documentation.

Rewrote docs/PROJECT_STATUS.md with completion-percentage estimates
per area (frontend/backend/UI/admin/storefront) and an explicit
first-customer-readiness call. Rewrote docs/NEXT_PHASE.md to the
strict 5-phase structure (backend integration -> production testing
-> performance -> monitoring -> v2 ideas), pointing to PRODUCT_BACKLOG
.md/FUTURE_FEATURES.md for phase 5 detail instead of duplicating it.

Rewrote root README.md - was stale (referenced deleted pages/info,
pages/legal folders from a prior RC pass), now covers architecture,
frontend/backend status, how to run, mock<->API switch mechanism
(useMockData in environment.ts), current folder structure, and a
documentation map.

Updated docs/PROJECT_INDEX.md (the stated entry point) to link only
the surviving doc set - every remaining document is reachable from it.

Fixed every broken/stale cross-reference to the deleted/renamed
backend docs across ARCHITECTURE.md, EDITOR.md, FRONTEND.md,
PROJECT-STRUCTURE.md, StaticPages.md, KNOWN-ISSUES.md (10 individual
link fixes, verified by repo-wide grep before and after). Left
CHANGELOG.md's two historical entries untouched - changelogs are
append-only history, not live navigation, editing past entries would
misrepresent what was true at the time.

Not touched (explicitly out of scope): docs/architecture/foundation/**
(enforced ADRs/governance, permanent not sprint-shaped),
docs/context/** (Barry Cache infrastructure, "do not edit by hand"
per CLAUDE.md), .claude/worktrees/** (separate git worktrees
containing an unrelated project's docs, not this repo's documentation).

docs/ root: 22 files -> 16. Plus 5 in docs/archive/ (was 3).
This commit is contained in:
sdarbinyan
2026-07-26 14:56:25 +04:00
parent d03ef2db50
commit 261ce6d55b
17 changed files with 536 additions and 2075 deletions

101
README.md
View File

@@ -1,65 +1,78 @@
# Marketplace Frontend # Marketplace Frontend
Angular marketplace frontend for the client demonstration. The app uses standalone components, signals, runtime branding/configuration, and a production build optimized for the current marketplace experience. Angular 21 multi-tenant marketplace platform frontend. Standalone components, signals, no NgRx. One codebase serves unlimited tenants ("marketplaces") via a per-tenant `bootstrap.json` fetched at runtime — no tenant-specific code paths.
## Features Three surfaces on this one codebase:
- **Storefront** (`/`) — the public shopping site: catalog, product pages, cart, static/CMS pages.
- **Builder / Project Editor** (`/edit/**`) — in-app editor that edits the tenant's `BootstrapConfig` (theme, nav, homepage sections, widgets, footer, languages, static pages).
- **Backoffice / Admin** (`/:lang/backoffice/**`) — products, categories, orders, transactions, users, moderation, media, monitoring, analytics.
- Responsive marketplace storefront ## Architecture
- Category browsing and product detail pages
- Search and shopping cart flows `Component (container) → Facade → Domain Service → Repository/Provider (DI token, swappable mock↔API) → Mock | API`
- Telegram login integration
- Payment handoff through the existing backend contract Enforced by `npm run arch:check` (import boundaries + circular deps), not just convention. Full detail: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), governance ADRs at `docs/architecture/foundation/**`.
- Runtime branding and configuration loading
- PWA manifest and service worker configuration ## Frontend status
**Release Candidate — feature-complete.** See [`docs/PROJECT_STATUS.md`](docs/PROJECT_STATUS.md) for the honest current-state breakdown (completion %, known limitations, readiness for demo/production/backend).
## Backend
**Not implemented yet — fully specified.** Every domain currently runs against an in-memory/mock gateway except Categories (the one domain wired to a real HTTP API). The complete contract a backend engineer needs — every endpoint, DTO, auth flow, error model, upload contract, and a step-by-step implementation checklist — lives in one canonical document:
**[`docs/BACKEND.md`](docs/BACKEND.md)**
## How to switch Mock ↔ API
Toggle `useMockData` in `src/environments/environment.ts` (or `environment.production.ts`). `RuntimeProviderStrategyService` (`src/app/core/providers/runtime-provider-strategy.service.ts`) reads this flag per-domain to decide whether a facade gets the mock or real gateway. On `localhost` with `useMockData: false`, some domains (bootstrap, categories) still fall back to mock automatically so local dev never silently hits a real backend by accident — see that service for the exact per-domain logic.
## Development ## Development
Install dependencies:
```bash ```bash
npm install npm install # install dependencies
npm start # local dev server
npm run build # production build -> dist/dexarmarket/
npm run arch:check # import-boundary + circular-dependency check
``` ```
Start the local development server: ## Folder structure
```bash
npm start
```
Build for production:
```bash
npm run build
```
The production build is written to `dist/dexarmarket/`.
## Project Structure
```text ```text
src/ src/
├── app/ ├── app/
│ ├── components/ # Shared storefront components │ ├── components/ # Shared storefront components (header, footer, product-card, etc.)
│ ├── core/ # Runtime, config, guards, providers, interceptors │ ├── core/ # Auth, admin-auth, config/tenant resolution, DI providers, interceptors
│ ├── dynamic-renderer/ # Page, section, and widget rendering pipeline │ ├── dynamic-renderer/ # Bootstrap JSON -> section/widget rendering pipeline (live homepage engine)
│ ├── facades/ # Runtime, website, builder, and backoffice facades │ ├── facades/ # Runtime, website, builder, and backoffice facades
│ ├── pages/ # Storefront, info, legal, cart, search, and item pages │ ├── features/ # Domain features: admin/*, project-editor, content-management, website/*
│ ├── services/ # API, cart, auth, SEO, Telegram, and language services │ ├── guards/ # Route guards (language, admin-auth, dirty-state, etc.)
── widgets/ # Dynamic renderer widgets ── i18n/ # Translation service, pipe, and locale packs (en/ru/hy)
├── assets/mock/ # Local mock configuration and catalog data │ ├── pages/ # Top-level routed pages: home, cart, static-page
├── environments/ # Development and production environment settings │ ├── services/ # API, cart, auth, SEO, Telegram, and language services
└── styles/ # Shared global styles and themes │ ├── shared/ # Shared UI primitives (button, dialog, confirm-dialog, table, etc.)
│ └── widgets/ # Dynamic-renderer widget components
├── assets/mock/ # Local mock configuration and catalog data
├── environments/ # Development and production environment settings (incl. useMockData)
└── styles/ # Shared global styles and themes
``` ```
## Useful Checks ## Documentation map
```bash Full index: [`docs/PROJECT_INDEX.md`](docs/PROJECT_INDEX.md). Key entry points:
npm run build
npm run arch:check | Doc | What it covers |
``` |---|---|
| [`docs/PROJECT_STATUS.md`](docs/PROJECT_STATUS.md) | Current completion status, honest limitations, demo/production readiness |
| [`docs/BACKEND.md`](docs/BACKEND.md) | The one canonical backend spec — endpoints, DTOs, auth, security, errors, uploads, checklist |
| [`docs/NEXT_PHASE.md`](docs/NEXT_PHASE.md) | Roadmap: backend integration → testing → performance → monitoring → v2 |
| [`docs/TODO.md`](docs/TODO.md) | Release blockers only |
| [`docs/KNOWN-ISSUES.md`](docs/KNOWN-ISSUES.md) | Real, reproducible, currently-open frontend bugs |
| [`docs/PRODUCT_BACKLOG.md`](docs/PRODUCT_BACKLOG.md) | Items needing a client/business decision |
| [`DESIGN.md`](DESIGN.md) | Visual design system |
| [`PRODUCT.md`](PRODUCT.md) | Product positioning |
## Notes ## Notes
- Authentication and payment integrations are intentionally left on their existing contracts. - Authentication and payment integrations are on their existing contracts — see `docs/BACKEND.md` for the auth/security contract a real backend must satisfy.
- Renderer and runtime architecture should remain stable during demo preparation.
- Client-facing content should avoid placeholder names, mock labels, and temporary routes. - Client-facing content should avoid placeholder names, mock labels, and temporary routes.

View File

@@ -41,7 +41,7 @@ Component (container) --> Facade --> Domain Service --> Repository/Provide
## Bootstrap / configuration engine ## Bootstrap / configuration engine
- `ConfigService` loads `BootstrapConfig` (see `docs/BACKEND_API.md#4-bootstrap`) once at startup; `PlatformRuntimeService` applies it (theme, branding, runtime state) and can `reloadFromBootstrap()` for in-memory preview without a full page reload. - `ConfigService` loads `BootstrapConfig` (see `docs/BACKEND.md#1-bootstrap`) once at startup; `PlatformRuntimeService` applies it (theme, branding, runtime state) and can `reloadFromBootstrap()` for in-memory preview without a full page reload.
- The bootstrap is the single source of truth for pages, sections, widgets, theme, navigation, footer, static pages, and feature flags (ADR-004). - The bootstrap is the single source of truth for pages, sections, widgets, theme, navigation, footer, static pages, and feature flags (ADR-004).
- The Project Editor mutates an in-memory draft of the same `BootstrapConfig` — there is no parallel editor-only model. - The Project Editor mutates an in-memory draft of the same `BootstrapConfig` — there is no parallel editor-only model.
@@ -65,7 +65,7 @@ Render pipeline: `page config -> section engine -> section renderer -> widget ho
## Feature flags / capability guards (ADR-009) ## Feature flags / capability guards (ADR-009)
- `bootstrap.featureFlags` (typed) plus the broader `bootstrap.features` (`MarketplaceFeaturesConfig`) surface for UI-facing toggles (wishlist, compare, reviews, recommendations, search history, etc.). - `bootstrap.featureFlags` (typed) plus the broader `bootstrap.features` (`MarketplaceFeaturesConfig`) surface for UI-facing toggles (wishlist, compare, reviews, recommendations, search history, etc.).
- Feature resolution falls back across older config surfaces to preserve behavior as the flag model evolved across sprints — see `docs/BACKEND_API.md#4-bootstrap` for the full field list. - Feature resolution falls back across older config surfaces to preserve behavior as the flag model evolved across sprints — see `docs/BACKEND.md#1-bootstrap` for the full field list.
## Diagnostics (dev-only) ## Diagnostics (dev-only)

View File

@@ -1,808 +0,0 @@
# Authentication — Complete Spec
Standalone, backend-implementable authentication contract for the marketplace
platform (Angular frontend, branch `B2B`). Derived directly from source —
`src/app/core/auth/**`, `src/app/core/admin-auth/**`,
`src/app/services/{auth,telegram-session-api}.service.ts`,
`src/app/components/telegram-login/**`, `src/app/guards/language.guard.ts`,
`src/app/app.routes.ts`, `src/app/app.config.ts`,
`src/app/core/config/tenant-resolver.service.ts` — plus
`docs/context/BACKEND-AUDIT.md` (this-session audit) and the prior
`docs/AUTH.md`. Where the frontend does not already imply a behavior, this
document says **"Requires backend decision"** rather than inventing one.
Two authentication mechanisms coexist in the codebase today, at different
maturity levels:
| Mechanism | Used by | Status |
|---|---|---|
| Telegram QR / deep-link session auth | Storefront customers **and** admin/backoffice (same API) | **LIVE** — real endpoints, in production use |
| Ed25519 challenge/response admin auth | Admin/backoffice (intended replacement) | **Frontend fully wired, backend endpoints do not exist yet** (404s today) |
Both are documented in full below. Nothing here should be read as "the
platform has JWTs today" — it does not, except inside the not-yet-live
Ed25519 flow.
---
## Table of contents
1. [Mechanism A — Telegram QR / session login (LIVE)](#1-mechanism-a--telegram-qr--session-login-live)
2. [Mechanism B — Ed25519 challenge/response admin auth (NOT LIVE)](#2-mechanism-b--ed25519-challengeresponse-admin-auth-not-live)
3. [JWT structure](#3-jwt-structure)
4. [Refresh token](#4-refresh-token)
5. [Token expiration handling](#5-token-expiration-handling)
6. [Token rotation](#6-token-rotation)
7. [Logout](#7-logout)
8. [Session invalidation](#8-session-invalidation)
9. [Role hierarchy](#9-role-hierarchy)
10. [Tenant isolation](#10-tenant-isolation)
11. [Permission model / route guards](#11-permission-model--route-guards)
12. [Open items — "Requires backend decision"](#12-open-items--requires-backend-decision)
---
## 1. Mechanism A — Telegram QR / session login (LIVE)
Single source for **both** customer and admin login:
`TelegramSessionApiService` (`src/app/services/telegram-session-api.service.ts`).
There is no separate admin backend endpoint — the same three calls back the
customer `AuthService` (`src/app/services/auth.service.ts`) and the admin
`AdminAuthService` (`src/app/core/admin-auth/admin-auth.service.ts`). Only the
**storage** differs (cookie name, in-memory signal), so an admin QR scan
never authenticates the customer session or vice versa.
### 1.1 Endpoints (base = `environment.authApiUrl`, e.g. `https://api.dexarmarket.ru:445`)
| Method | Path | Request | Response |
|---|---|---|---|
| POST | `/users/sessions` | body `{ webSessionID }` (client-generated GUID), header `WebSessionID: <same guid>` | `{ webSessionID, url }``url` is the Telegram bot deep link |
| GET | `/users/sessions/{id}` | — | Session object, heavily field-tolerant (see §1.3) |
| DELETE | `/users/sessions/{id}` | header `WebSessionID: <id>` | ignored/discarded |
### 1.2 Frontend-driven flow
The frontend, not the backend, generates the session id. Sequence:
1. User opens login (storefront "Sign in" or admin `/admin-login` gate).
2. Frontend generates a random GUID client-side (`generateGuid()`,
`src/app/shared/util/guid.util.ts`) — this **is** the `webSessionID`, sent
to the backend, not received from it.
3. `POST {authApiUrl}/users/sessions` with `{ webSessionID }` body and
`WebSessionID` header set to the same value. Backend response's own id
field is preferred if present (see `extractSessionId` — checks
`webSessionID/WebSessionID/webSessionId/sessionID/SessionID/sessionId/id/ID`
in that order), else the frontend's generated GUID is used as fallback.
4. Frontend builds two login links from the returned id:
- Web: `https://t.me/{bot}?start={webSessionID}` (`getBotLoginUrl`)
- App deep link: `tg://resolve?domain={bot}&start={webSessionID}`
(`getBotAppLoginUrl`)
- `bot` = `environment.telegramBot` (`'myAMLKYCBOT'` in current env config;
code fallback `'DexarSupport_bot'` if the env key is absent).
5. Frontend renders both as a QR code (external image generator
`https://api.qrserver.com/v1/create-qr-code/...` — not a backend of this
platform, purely a QR bitmap renderer for the `url`) plus the app deep
link for mobile. This is orchestrated by `QrLoginEngine`
(`src/app/shared/qr-login/qr-login.engine.ts`) shared identically by both
customer and admin modes via `TelegramLoginComponent`
(`src/app/components/telegram-login/telegram-login.component.ts`,
`[mode]="'customer' | 'admin'"`).
6. User scans the QR (or taps the deep link on mobile) and completes the
Telegram bot interaction out-of-band. The **backend** is expected to mark
that `webSessionID` as active/logged-in once the Telegram bot confirms the
user, associating a Telegram user identity with it.
7. Frontend polls `GET /users/sessions/{id}` (`checkSessionOnce`, driven by
`QrLoginEngine`'s polling loop) until the session normalizes to `active:
true`, or the user cancels/times out.
8. On an active session, the frontend calls `activateSession()` internally
(sets in-memory signal, stores the id per §1.4, schedules a re-check —
see §5) and redirects: customer → wherever the login was triggered from;
admin → `/{lang}/backoffice/dashboard` (hardcoded in
`telegram-login.component.ts`).
### 1.3 Session response normalization (backend field tolerance)
`TelegramSessionApiService.normalizeWebSession()` is deliberately
tolerant of multiple backend field-naming conventions (evidence the backend
contract was never fully pinned down). A backend implementation can emit any
of these; the frontend reads the **first key found** in this priority order:
- **Active/status**: `status`, `Status`, `active`, `Active`, `loggedIn`,
`LoggedIn`, `isLoggedIn`, `IsLoggedIn`, `authenticated`, `Authenticated`.
Value is considered "active" if boolean `true`/`1`, or (case-insensitive)
one of `true, 1, active, authenticated, confirmed, success, logged_in`.
- **User object**: nested under `user`/`User`/`telegramUser`/`TelegramUser`,
else the top-level response object itself is used as the user record.
- **Username**: `username`/`Username` (user object first, then top-level).
- **First/last name**: `firstName`/`first_name`/`FirstName`/`First_name`,
`lastName`/`last_name`/`LastName`/`Last_name` — joined with a space if both
present.
- **Display name**: explicit `displayName`/`DisplayName`/`name`/`Name` (user
object or top-level) wins; else falls back to `username`; else falls back
to the joined full name; else literal `'Telegram User'`.
- **Telegram user id**: `userId`/`telegramUserId`/`telegramUserID`/
`TelegramUserID`/`id`/`ID` (user object), else `userId`/`telegramUserId`/
`telegramUserID`/`TelegramUserID`/`userID`/`UserID`/`UserId` (top-level).
- **Session id**: same priority list as `extractSessionId` above.
- **Expiry**: `expiresAt`/`ExpiresAt`/`expires`/`Expires` (ISO 8601 string);
if absent, the frontend fabricates `now + 3600s` client-side — **the
backend should always send a real `expiresAt`/`expires`** so the frontend's
refresh-scheduling (§5) reflects the true session lifetime rather than a
guessed one.
Normalized shape consumed by the frontend (`AuthSession`,
`src/app/models/auth.model.ts`):
```ts
interface AuthSession {
sessionId: string;
userId: number | null;
username: string | null;
displayName: string;
active: boolean;
expires: string; // ISO 8601
}
```
### 1.4 Storage (customer vs admin — kept fully separate)
| | Customer (`AuthService`) | Admin (`AdminAuthService`) |
|---|---|---|
| Cookie name | `webSessionID` | `adminSessionID` |
| Cookie attrs | `Max-Age=3600; Path=/; SameSite=Lax` (+`Secure` over HTTPS) | `Max-Age=3600; Path=/; SameSite=Strict` (+`Secure` over HTTPS) |
| In-memory state | `sessionSignal`, `statusSignal` (`unknown\|checking\|authenticated\|expired\|unauthenticated`) | Same shape, separate signals |
| Extra storage | — | Reserved JWT-pair slots `localStorage['adminToken']` / `localStorage['adminRefreshToken']`**unused today**, see §12 |
Both send a `WebSessionID` header on every marketplace API request via
`apiHeadersInterceptor` (see `docs/context/BACKEND-AUDIT.md` §3) — this is
the anonymous-or-authenticated session identity the backend correlates
requests against; there is no `Authorization: Bearer` header in this
mechanism.
### 1.5 Session re-check / soft refresh (not a token refresh)
Both `AuthService` and `AdminAuthService` self-schedule a re-check of
`GET /users/sessions/{id}` 60 seconds before `expires`, minimum 30s out
(`scheduleSessionRefresh`). This is **not** a refresh-token exchange — it
just re-polls the same session-status endpoint and re-activates if still
active, or clears local state if not. There is no rotation of the
`webSessionID` itself in this mechanism.
### 1.6 Admin dev bypass (non-production only)
`AdminAuthService.devBypassLogin()` fabricates a local session
(`sessionId: 'dev-bypass-{timestamp}'`, `active: true`, 1-hour expiry) and
activates it directly, skipping the QR flow entirely. Guarded by
`environment.production` at runtime (not just build-time) — the checked
condition is inside the function body, so it is dead code in a production
build.
### 1.7 Sequence diagram — storefront/admin Telegram-QR login
```mermaid
sequenceDiagram
participant User as User (browser)
participant FE as Frontend (AuthService / AdminAuthService)
participant BE as Backend (authApiUrl)
participant TG as Telegram bot
User->>FE: Open login (customer checkout, or /admin-login gate)
FE->>FE: generate webSessionID (client GUID)
FE->>BE: POST /users/sessions { webSessionID } (header WebSessionID)
BE-->>FE: 200 { webSessionID, ... }
FE->>FE: build QR + tg:// deep link from webSessionID
FE-->>User: render QR code / "Open in Telegram" button
User->>TG: scan QR / tap deep link, confirm in bot
TG->>BE: (out of band) associate webSessionID with Telegram user
loop poll every N seconds
FE->>BE: GET /users/sessions/{webSessionID}
BE-->>FE: session (active:false while pending)
end
BE-->>FE: session (active:true, user fields, expires)
FE->>FE: activateSession(): store cookie, set signals,<br/>schedule re-check at expires-60s
alt mode = admin
FE-->>User: redirect to /{lang}/backoffice/dashboard
else mode = customer
FE-->>User: close dialog, resume prior action (e.g. checkout)
end
```
---
## 2. Mechanism B — Ed25519 challenge/response admin auth (NOT LIVE)
**Status: frontend fully implemented and wired to real `HttpClient` calls;
the backend does not implement these endpoints yet — calls 404/error today.**
No route currently requires this flow (`ed25519AuthGuard` is not referenced
by any route in `app.routes.ts`; the live admin gate is still
`adminAuthGuard` / Telegram QR, §1). This is the target contract for closing
the security gap in §1: today the Telegram session API has no concept of
"admin," so the backend cannot distinguish an admin login attempt from a
customer one at the moment of login. Ed25519 closes that by requiring proof
of possession of a specific, pre-registered private key before any session
is issued.
### 2.1 Key generation (device-local, once per device)
`Ed25519KeypairService` (`src/app/core/auth/services/ed25519-keypair.service.ts`):
- `getOrCreateKeyPair()`: generates a **non-extractable** Ed25519 keypair via
`crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify'])`
(real WebCrypto Ed25519 — RFC 8032, not a placeholder), persists the raw
`CryptoKey` handles in IndexedDB (`admin-auth-ed25519` DB, object store
`keypair`, single record `id: 'device-keypair'`).
- The private key is never exported, serialized, or transmitted — by
construction (`extractable: false`), not by convention or policy.
- `sign(message)`: signs a UTF-8-encoded string with `crypto.subtle.sign
('Ed25519', privateKey, ...)`, returns a base64-encoded signature.
- `clear()`: deletes the IndexedDB record ("forget this device"). A new
keypair generated after this requires re-registration with the backend
(§2.2) before it can complete a login.
- **Public key registration is explicitly out of scope for the frontend.**
An Owner/Administrator must associate a new device's `publicKeyBase64`
with an admin account through some out-of-band mechanism (backend admin
tool, one-time enrollment link, etc.) — not prescribed here (see §12).
### 2.2 Login flow, step by step
Orchestrated by `AuthService.login()` (`src/app/core/auth/services/auth.service.ts`,
distinct from the customer/admin `AuthService` in §1 despite the identical
class name — different module, `core/auth/` vs `services/`):
1. `GET {authApiUrl}/api/admin/auth/challenge` → `AuthChallenge { nonce,
issuedAt, expiresAt }` (all ISO 8601 except `nonce`, an opaque string).
2. `Ed25519KeypairService.getOrCreateKeyPair()` (generates on first use).
3. `Ed25519KeypairService.sign(nonce)` — signs the **raw nonce string
exactly as received**, no additional framing/prefix/hashing applied
client-side.
4. `POST {authApiUrl}/api/admin/auth/verify` with body
`VerifySignatureRequest { publicKey, signature, nonce }` (`publicKey` =
base64 raw Ed25519 public key, `signature` = base64 signature over the
nonce, `nonce` = the same value echoed back).
5. Backend must: re-derive the exact signed message from the nonce it
issued, verify the signature against its own `publicKey → admin account`
mapping, confirm the nonce hasn't expired or been used before, and only
then issue tokens.
6. On success: `200 AuthTokenPair { token, refreshToken }`.
`SessionService.activate(tokens)` decodes the JWT (§3), stores both
tokens (§4), and schedules the next refresh (§5).
7. On failure: `401`/`403` → `AuthService` maps it through
`authErrorCodeFromStatus()` to `invalid-signature` (or a more specific
code — see §2.5) and the UI routes to
`/admin-login/error/invalid-signature`.
### 2.3 API contracts (all under `{environment.authApiUrl}/api/admin/auth`)
| Method | Path | Request body | Response | Notes |
|---|---|---|---|---|
| GET | `/challenge` | — | `200 AuthChallenge` | `{ nonce, issuedAt, expiresAt }` |
| POST | `/verify` | `VerifySignatureRequest { publicKey, signature, nonce }` | `200 AuthTokenPair` \| `401` \| `403` | Issues `{ token, refreshToken }` |
| POST | `/refresh` | `RefreshTokenRequest { refreshToken }` | `200 AuthTokenPair` \| `401` | Rotation expected — see §6 |
| POST | `/logout` | `{ refreshToken }` | `204` (frontend clears local state regardless of response code/body) | Should revoke server-side |
Types: `src/app/core/auth/models/auth-api.model.ts`. HTTP client:
`src/app/core/auth/services/auth-api.service.ts` (`AuthApiService`) — thin
wrapper, no retries, no fabricated mock responses.
### 2.4 Sequence diagram — admin login with Ed25519 signing
```mermaid
sequenceDiagram
participant Admin as Admin (browser)
participant FE as Frontend (AuthService, core/auth)
participant Key as Ed25519KeypairService (WebCrypto + IndexedDB)
participant BE as Backend
Admin->>FE: Click "Sign in"
FE->>BE: GET /api/admin/auth/challenge
BE-->>FE: 200 { nonce, issuedAt, expiresAt }
FE->>Key: getOrCreateKeyPair() (generate on first use, non-extractable)
Key-->>FE: { publicKeyBase64 }
FE->>Key: sign(nonce)
Key-->>FE: signature (base64)
FE->>BE: POST /api/admin/auth/verify { publicKey, signature, nonce }
alt signature valid & publicKey is a provisioned admin key & nonce fresh/unused
BE-->>FE: 200 { token, refreshToken }
FE->>FE: SessionService.activate(tokens):<br/>decode JWT claims, persist, schedule refresh
FE-->>Admin: redirect to /backoffice
else invalid signature / unknown key / expired or reused nonce
BE-->>FE: 401 / 403
FE-->>Admin: redirect to /admin-login/error/invalid-signature
end
```
### 2.5 Error screens
Single component `AuthErrorPageComponent` at route `/admin-login/error/:code`
renders all five, keyed by route param. `authErrorCodeFromStatus()`
(`src/app/core/auth/models/auth-error.model.ts`) maps HTTP status → code:
`401→unauthorized`, `403→forbidden`, `0→backend-unavailable`,
`5xx→backend-unavailable`, else `unauthorized`.
| Code | Trigger | User action offered |
|---|---|---|
| `session-expired` | Refresh token rejected/expired | Sign in again |
| `invalid-signature` | `verify` returns 401/403 during login, or any client-side failure in the challenge→sign→verify chain that isn't a clearer HTTP-derived code | Try again |
| `unauthorized` | Route guard sees no active session | Sign in |
| `forbidden` | `permissionGuard` denies (authenticated but insufficient role) | Back to dashboard |
| `backend-unavailable` | Network error / 5xx / status 0 | Retry |
### 2.6 Interceptor status — NOT registered
`src/app/core/auth/interceptors/auth.interceptor.ts` exists (adds
`Authorization: Bearer` + reactive 401-refresh-and-retry, see §5) but **is
not included** in `app.config.ts`'s `withInterceptors([...])` list today.
Confirmed in `app.config.ts`:
```
withInterceptors([mockDataInterceptor, apiBaseUrlInterceptor,
apiHeadersInterceptor, adminAuthHeadersInterceptor, cacheInterceptor])
```
`authInterceptor` is absent. Until it is registered, no request in the app
automatically attaches the Ed25519-flow JWT as a bearer token — this
confirms the mechanism is fully dormant, not partially live.
### 2.7 Module map
```
src/app/core/auth/
├── auth.routes.ts # /admin-login, /admin-login/error/:code
├── models/
│ ├── auth-api.model.ts # AuthChallenge, VerifySignatureRequest, AuthTokenPair, JwtClaims
│ ├── auth-error.model.ts # AuthErrorCode, authErrorCodeFromStatus()
│ └── permission.model.ts # AdminRole, Permission, ROLE_PERMISSIONS
├── services/
│ ├── ed25519-keypair.service.ts # WebCrypto keygen/sign, IndexedDB persistence
│ ├── auth-api.service.ts # HttpClient calls to the 4 endpoints in §2.3
│ ├── jwt.service.ts # decode-only JWT parsing
│ ├── session.service.ts # token/claims state, persistence, refresh scheduling
│ ├── permission.service.ts # role -> permission set
│ ├── auth.service.ts # orchestrates challenge -> sign -> verify -> refresh -> logout
│ └── auth-facade.service.ts # public surface for components
├── interceptors/
│ └── auth.interceptor.ts # Authorization: Bearer + 401 refresh-and-retry (NOT registered, §2.6)
├── guards/
│ ├── ed25519-auth.guard.ts # requires SessionService.isAuthenticated() (not referenced by any route)
│ └── permission.guard.ts # permissionGuard(permission) factory
└── pages/
├── admin-login-page.component.* # sign-in UI
└── auth-error-page.component.* # parameterized error screen (§2.5)
```
`AuthFacade` (`src/app/core/auth/services/auth-facade.service.ts`) is the
only thing components/pages should depend on; `AuthService`/
`SessionService`/`PermissionService` are internal collaborators.
---
## 3. JWT structure
Only defined for Mechanism B (Ed25519 flow) — Mechanism A (§1) issues no JWT,
only an opaque session id. Expected claims
(`src/app/core/auth/models/auth-api.model.ts::JwtClaims`):
```ts
interface JwtClaims {
sub: string; // admin account id
role: AdminRole; // 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly'
iat: number; // seconds since epoch (standard `iat`)
exp: number; // seconds since epoch (standard `exp`)
publicKey: string; // the Ed25519 public key this token was issued for
}
```
`JwtService.decode()` (`src/app/core/auth/services/jwt.service.ts`) does
**decode-only** parsing (base64url payload → JSON), and validates only the
minimal shape: `sub` is a string, `role` is a string, `exp` is a number — if
any of these three checks fail, decoding returns `null` and the caller
(`SessionService`) discards the session as malformed.
The frontend never verifies the JWT signature — it has no trusted key to
check it against; that is exclusively the backend's job on every
subsequent admin request. A decoded-but-unverified claim is UX (role-gated
menus, expiry countdowns) — never proof of authorization to any client-side
check.
---
## 4. Refresh token
Defined only for Mechanism B. `AuthTokenPair { token, refreshToken }` is
returned by both `/verify` and `/refresh`. Storage
(`SessionService`, `src/app/core/auth/services/session.service.ts`):
- `localStorage['ed25519AdminToken']` — access token (JWT)
- `localStorage['ed25519AdminRefreshToken']` — refresh token (opaque to the
frontend; never decoded, only round-tripped)
Both are written together in `activate()` and cleared together in `clear()`.
There is no separate expiry tracked for the refresh token client-side —
the frontend only reacts to a `401` from `/refresh` (see §5/§6).
Separately, `AdminAuthService` (Mechanism A, Telegram) reserves
`localStorage['adminToken']` / `localStorage['adminRefreshToken']` with
`getAdminToken()`/`setAdminTokens()`/`clearAdminTokens()` methods —
**written by no code path today** ("reserved for once the backend issues
admin access/refresh tokens... unused until then," per the source comment).
These are a distinct, currently-dead pair of storage keys from the Ed25519
ones above; do not conflate them.
---
## 5. Token expiration handling
### 5.1 Mechanism A (Telegram session) — expiry via re-poll
See §1.5. `expires` from the session payload drives a `setTimeout` at
`max(expiresMs - now - 60_000, 30_000)` that re-calls `GET /users/sessions/
{id}`; if the backend now reports inactive, local state is cleared to
`unauthenticated`. There is no interceptor-level reactive handling for this
mechanism — a 401/expired session surfaces only through the next explicit
`checkSessionOnce()` poll or session re-check timer, not a per-request
retry.
### 5.2 Mechanism B (Ed25519/JWT) — proactive + reactive
`SessionService.scheduleRefresh(claims)`: computes
`refreshInMs = max(claims.exp*1000 - now - 60_000, 5_000)` and sets a timer.
When it fires, `AuthService.refresh()` runs automatically
(`session.onRefreshDue(callback)` wiring, set up once in `AuthService`'s
constructor to avoid a circular DI dependency between the two services).
`SessionService.restore()` (intended to run once at app bootstrap, from an
`APP_INITIALIZER` calling `AuthFacade.restoreSession()` — **not yet wired
into the bootstrap process today**, see §12): reads persisted tokens,
decodes claims, and either resumes with a scheduled refresh or marks
`expired` immediately without any network call, so a stale session is
caught before any component/guard runs.
`authInterceptor` (present in source, not registered — §2.6) is documented
as: catch a 401 on any admin-gated request → attempt one `refresh()` →
retry the original request once on success → route to `session-expired` on
failure. Does not retry more than once; a second 401 after an
apparently-successful refresh is treated as a server-side problem, not a
transient race.
### 5.3 Sequence diagram — token expiration / refresh (Ed25519 flow)
```mermaid
sequenceDiagram
participant FE as Frontend (SessionService)
participant IC as authInterceptor (not yet registered, §2.6)
participant BE as Backend
Note over FE: Timer fires ~60s before JWT exp
FE->>BE: POST /api/admin/auth/refresh { refreshToken }
alt refresh token still valid
BE-->>FE: 200 { token, refreshToken }
FE->>FE: activate(tokens) - reschedules next refresh
else refresh token expired/revoked
BE-->>FE: 401
FE->>FE: SessionService.markExpired()
FE-->>FE: route to /admin-login/error/session-expired
end
Note over IC: Reactive path - any 401 on an admin request<br/>(inactive until authInterceptor is registered)
IC->>BE: Admin API request (expired token)
BE-->>IC: 401
IC->>BE: POST /api/admin/auth/refresh (single retry)
alt refresh succeeds
BE-->>IC: 200 tokens
IC->>BE: retry original request with new token
else refresh fails
IC-->>FE: propagate error, route to session-expired
end
```
---
## 6. Token rotation
**Mechanism A**: no token to rotate — the `webSessionID` itself is stable
for the life of the session; expiry is handled by re-polling status (§5.1),
not by issuing a new id.
**Mechanism B**: rotation is *expected* by the frontend but not verifiable
until the backend exists. Per the source comment in `AuthApiService` and the
security notes in the prior `docs/AUTH.md`:
- Every `POST /refresh` response is expected to include a **new**
`refreshToken`; the backend should invalidate the one just used
(single-use refresh tokens).
- The frontend always stores whatever pair it receives from `/verify` or
`/refresh` and never reuses an old refresh token after a successful
rotation — there is no client-side retry logic that would resend a stale
refresh token.
- **Requires backend decision**: refresh-token reuse detection / revocation
cascade (e.g. if a rotated-out refresh token is presented again, should the
backend revoke the entire token family as a compromise signal?). Nothing
in the frontend implies or depends on this — it is a pure backend policy
choice.
---
## 7. Logout
**Mechanism A** (`AdminAuthService.logout()` / `AuthService.logout()` in
`src/app/services/auth.service.ts`): `DELETE /users/sessions/{id}` with
`WebSessionID` header, then unconditionally clears local state (cookie,
signals, timers) regardless of the HTTP result.
**Mechanism B** (`AuthService.logout()` in `src/app/core/auth/services/`):
clears `SessionService` state **immediately and unconditionally** (before
the network call resolves), then best-effort calls
`POST /api/admin/auth/logout { refreshToken }` if a refresh token was
present; any error from that call is swallowed (`catchError(() =>
throwError(() => null))`). If no refresh token exists locally, no network
call is made at all. **Backend implication**: the frontend cannot be relied
upon to reliably deliver the logout call (network failure, tab closed
mid-request, etc.) — server-side session/token expiry must not depend on a
client-issued logout ever arriving. `AuthFacade.logout()` additionally
always navigates to `/admin-login` (default) via `finalize()`, regardless of
API outcome.
### 7.1 Sequence diagram — logout (both mechanisms)
```mermaid
sequenceDiagram
participant User
participant FE as Frontend
participant BE as Backend
User->>FE: Click "Log out"
FE->>FE: Clear local session state immediately<br/>(cookie / tokens / signals / timers)
alt Mechanism A (Telegram session)
FE->>BE: DELETE /users/sessions/{id} (header WebSessionID)
BE-->>FE: any response (ignored)
else Mechanism B (Ed25519/JWT) - only if a refresh token existed
FE->>BE: POST /api/admin/auth/logout { refreshToken }
BE-->>FE: 204 (or error, swallowed)
end
FE-->>User: redirect to login page
```
---
## 8. Session invalidation
Client-side triggers that clear local auth state, both mechanisms:
- Explicit logout (§7).
- Session status re-check (Mechanism A) returning `active: false` (§1.5).
- JWT decode failure on restore (Mechanism B) — a malformed/unparsable
stored token is treated as no session at all (`SessionService.restore()`
calls `clear()`).
- Refresh failure (Mechanism B) — any error from `/refresh` calls
`SessionService.markExpired()`.
- `AdminAuthService.clearAuthState()` also clears the reserved
`adminToken`/`adminRefreshToken` keys (§4) even though nothing currently
writes them, for forward-compatibility once Mechanism A gains a token
pair.
**Requires backend decision**: server-side session/token revocation
propagation — e.g., can an Owner revoke another admin's active session
remotely (relevant given `AdminUsersGateway.revokeSession` already exists as
a **mock-only** admin-users gateway method per
`docs/context/BACKEND-AUDIT.md` §14)? If so, the frontend has no push
mechanism (no websocket, no polling of "is my token still valid" beyond the
scheduled refresh) to learn about a remote revocation before its next
refresh/request attempt — a session could remain "authenticated" client-side
for up to the refresh interval after a backend-side revocation. If real-time
revocation is required, that is new frontend work, not something already
implied by existing code.
---
## 9. Role hierarchy
**Discrepancy flagged by the backend audit — `AdminRole` is defined twice
with different meanings. Reconciliation needed before backend
implementation:**
1. `src/app/core/auth/models/permission.model.ts` — a **string union** used
by the Ed25519/JWT flow's `role` claim and `PermissionService`:
```ts
type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';
```
Ordered highest-to-lowest privilege by convention (not enforced in code —
`PermissionService` does not rely on ordering, only exact role → permission
set lookup).
2. `src/app/features/admin/users/models/admin-user.model.ts` — an
**interface** describing an admin-users-management row (`{ id, name, ...
}`), unrelated in shape to #1 and used only by the mock admin-users
gateway/facade (`AdminUsersGateway`, MOCK-ONLY, no backend seam per the
audit).
These two `AdminRole` symbols do not currently reference each other and are
imported from different modules by different features. A backend
implementer should treat #1 (the permission-model union) as the JWT/role
claim contract for auth purposes, and flag #2 for a naming rename (e.g.
`AdminUserRoleRecord`) rather than assuming they describe the same concept.
This document does not resolve the collision — it is called out so a human
reconciles it before building the backend role table.
### 9.1 Permission-to-role mapping (from `ROLE_PERMISSIONS`)
| Role | Permissions |
|---|---|
| `Owner` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage`, `settings.manage` |
| `Administrator` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage` |
| `Editor` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write` |
| `Support` | `backoffice.read` |
| `ReadOnly` | `backoffice.read`, `builder.read` |
This is deliberately coarse and mirrors the bootstrap-level
`PermissionsConfig` shape (`src/app/shared/models/config/permissions.model.ts`).
Finer-grained, per-domain permissions (e.g. "can edit prices but not delete
products") do not exist anywhere client-side and stay server-side —
**Requires backend decision** if finer granularity is ever needed.
---
## 10. Tenant isolation
`TenantResolverService` (`src/app/core/config/tenant-resolver.service.ts`)
resolves tenant by **subdomain**, not by header or path prefix:
```ts
getTenantKey(): string {
if (isLocalhost()) return environment.fallbackTenantKey ?? 'default';
const segments = hostname.split('.').filter(Boolean);
if (segments.length === 0) return environment.fallbackTenantKey ?? 'default';
if (segments[0] === 'www' && segments.length > 1) return segments[1];
return segments[0];
}
```
- `isLocalhost()` matches `localhost`, `127.0.0.1`, `::1`.
- On a real host, the tenant key is the **first DNS label**, skipping a
leading `www`. E.g. `dexarmarket.api.dexarmarket.ru` → tenant key
`dexarmarket`; `www.acme.com` → `acme`.
- This tenant key feeds `ApiConfigService.getBaseUrl()`
(`src/app/core/config/api-config.service.ts`, documented in
`docs/context/BACKEND-AUDIT.md` §2) to pick the marketplace API base URL:
localhost → `environment.localhostApiUrl` (`/api`); else
`environment.tenantApiBaseUrls[tenantKey]`; else
`environment.tenantApiTemplate` with `{tenant}` substituted; else
(gated by `allowBootstrapApiOverride`, off by default) a value read out of
the already-loaded bootstrap document
(`bootstrap.apiEndpoints.website.baseUrl` /
`bootstrap.tenant.apiBaseUrl`).
- **No `X-Tenant` header or `/tenant/{id}/...` path prefix is sent by the
frontend anywhere** — tenant isolation for the marketplace API is achieved
purely by **which base URL/subdomain the request is sent to**, not by a
request attribute the backend reads per-call. Auth (both mechanisms) does
**not** carry any tenant identifier in its request bodies or headers
either — `POST /users/sessions`, `/api/admin/auth/challenge`, etc. are all
called against `environment.authApiUrl`, a single fixed origin, with no
per-tenant variation in the auth-flow code today.
- **Requires backend decision**: if auth (session creation, Ed25519
challenge/verify) must be tenant-scoped (e.g. an admin's Ed25519 public key
should only authorize them for one tenant's backoffice), the frontend
currently has no mechanism to communicate which tenant a login attempt is
for beyond whatever the backend can infer from the request's origin/
Referer header — nothing in the auth payloads carries a tenant id
explicitly. This would be new frontend work if required.
---
## 11. Permission model / route guards
### 11.1 `adminAuthGuard` (live, Mechanism A) — `src/app/core/admin-auth/admin-auth.guard.ts`
```ts
export const adminAuthGuard: CanActivateFn = () => {
const adminAuth = inject(AdminAuthService);
if (adminAuth.isAuthenticated()) return true;
adminAuth.requestLogin();
return false;
};
```
Checks only `AdminAuthService.isAuthenticated()` (Telegram session status
`=== 'authenticated'`) — no role/permission check at all. Applied to
`/edit`, `/edit/:section`, and `/backoffice` (and its children) in
`app.routes.ts`. This guard **cannot** distinguish admin roles from each
other — it is purely "is there an active admin Telegram session," which is
the exact gap Mechanism B is meant to close.
### 11.2 `ed25519AuthGuard` (dormant, Mechanism B) — `src/app/core/auth/guards/ed25519-auth.guard.ts`
Requires `SessionService.isAuthenticated()` (JWT status `=== 'authenticated'`).
Not referenced by any route in `app.routes.ts` today — confirmed by source
search. Exists purely as the cutover target (see §2's "not live" status).
### 11.3 `permissionGuard(permission)` — `src/app/core/auth/guards/permission.guard.ts`
Factory guard that checks `PermissionService.has(permission)` against the
Mechanism B role → permission table (§9.1). Also unused by any live route
until Mechanism B is cut over, but ready to gate specific admin sub-routes
by permission once it is (e.g. `permissionGuard('users.manage')` on a users
page).
### 11.4 `languageGuard` — `src/app/guards/language.guard.ts`
Not an authentication guard, but gates every localized route (`:lang`
segment wraps the entire route tree in `app.routes.ts`). Behavior:
- If `:lang` param is a known, enabled language: preload its translation
pack (`TranslateService.preloadLanguage`), set it as current
(`LanguageService.setLanguage`), allow navigation.
- If known but **disabled**: redirect to the current default language,
preserving the rest of the path.
- If unrecognized entirely: treat the URL as a legacy no-lang-prefix URL and
redirect to `/{defaultLang}{originalUrl}`, preserving query string/fragment
via `router.parseUrl` (not a hand-built `UrlTree`, to avoid double-encoding
the query string into the path segment).
### 11.5 `canDeactivate` guards (dirty-state guards, not auth)
Also not authentication, but listed since the task asked for "what guards
check": `projectEditorDirtyGuard`, `adminProductDirtyGuard`,
`adminCategoryDirtyGuard` — all gate navigation *away* from an in-progress
editor (builder section, product editor, category editor) to warn about
unsaved changes. They read editor dirty-state signals, not auth state, and
are unrelated to session/token validity.
### 11.6 What the frontend actually gates, summarized
| Concern | Mechanism | Guard/service |
|---|---|---|
| "Is there an active admin session at all" | Telegram (A) | `adminAuthGuard` → `AdminAuthService.isAuthenticated()` |
| "Is there an active admin JWT session" | Ed25519 (B), not live | `ed25519AuthGuard` → `SessionService.isAuthenticated()` |
| "Does this role have permission X" | Ed25519 (B), not live | `permissionGuard(permission)` → `PermissionService.has()` |
| "Is `:lang` valid/enabled" | n/a | `languageGuard` |
| "Unsaved editor changes" | n/a | `*DirtyGuard` (project editor / product / category) |
**Every one of these is a client-side UX gate only.** None of them are a
substitute for server-side authorization — the backend must independently
verify role/permission on every admin mutation regardless of what a route
guard decided, per the security note already present in the prior
`docs/AUTH.md` and repeated here: a passing client-side check is not proof
of anything to the backend.
---
## 12. Open items — "Requires backend decision"
Consolidated list of everything this document could not derive from
existing frontend code and therefore does not prescribe:
- **Public-key enrollment mechanism** (§2.1) — how an admin's Ed25519
`publicKeyBase64` gets associated with an account/role server-side (admin
tool? one-time enrollment link? manual DB entry?). Zero frontend code
exists for this by design.
- **Refresh-token reuse/compromise detection** (§6) — whether presenting an
already-rotated-out refresh token should revoke the whole token family.
Not implied by any frontend behavior.
- **Session/token revocation propagation** (§8) — whether/how a
remotely-revoked admin session (e.g. via the mock `AdminUsersGateway.
revokeSession`) is communicated to an already-logged-in client before its
next refresh cycle. No push/poll mechanism exists today.
- **Tenant scoping of auth requests** (§10) — whether login/challenge/verify
need an explicit tenant identifier in the payload, versus relying on
request origin. Not present in any current auth payload.
- **Relationship between the two mechanisms at cutover** — replace
`adminAuthGuard` with `ed25519AuthGuard` outright, or run both and let
role/tenant config decide? Explicitly called out in the prior
`docs/AUTH.md` as "a product decision, not made here," and nothing has
changed that.
- **`AdminAuthService`'s reserved JWT-pair slots** (`adminToken`/
`adminRefreshToken`, §4) — whether Mechanism A is ever meant to gain its
own token pair (as the reserved-but-unused storage suggests) independent
of the Ed25519 migration, or whether that code is dead and should be
removed. Not resolved by current usage (nothing writes to it).
- **`AdminRole` naming collision** (§9) — a reconciliation/rename decision
between `core/auth/models/permission.model.ts`'s string union and
`features/admin/users/models/admin-user.model.ts`'s interface; flagged,
not resolved, by this document.
- **Fine-grained/per-domain permissions** (§9.1) — the current model is
intentionally coarse; whether a richer permission model is ever needed is
a backend/product decision.
- **`APP_INITIALIZER` wiring for `AuthFacade.restoreSession()`** (§5.2) — the
code comment says this should be wired in before the Ed25519 flow goes
live, but it is not wired in today. This is frontend follow-up work, not a
backend decision, but is listed here because it changes what "session
restored on refresh" means in practice until it lands.

View File

@@ -1,6 +1,6 @@
# Backend Integration — Canonical Specification # Backend — Canonical Specification
**This is the single source of truth for backend implementation.** It supersedes and merges `docs/archive/BACKEND_API.md`, `docs/AUTH.md`, `docs/ADMIN.md`, and `docs/BACKEND_API_REMAINING_WORK.md` (all archived — see `docs/archive/` and the note at the end of this file). It incorporates `docs/AUTHENTICATION.md` (§4) and `docs/ERROR_CONTRACT.md` (§6) in full; those files remain in place as standalone references but this document is authoritative. `docs/MAINTENANCE_MODE.md` is a companion doc, referenced from §6. **This is the ONE document a backend engineer needs.** It is the single source of truth for backend implementation — architecture, bootstrap, authentication, JWT, Ed25519 public-key login, permissions, maintenance mode, error contract, every endpoint, DTOs, request/response schemas, uploads, pagination, filters, sorting, publish workflow, media, builder, examples, and a top-to-bottom implementation checklist. It supersedes and fully merges `AUTHENTICATION.md`, `ERROR_CONTRACT.md`, `MAINTENANCE_MODE.md`, and (already archived, content re-derived from current source) `docs/archive/BACKEND_API.md` and `docs/archive/BACKEND_API_REMAINING_WORK.md`. Those standalone files no longer exist — everything they contained lives here.
Everything here is derived from the actual current frontend source code (branch `B2B`), not from prior/stale documentation. Primary input: `docs/context/BACKEND-AUDIT.md` (exhaustive audit of every HTTP call, gateway, facade, and model in the frontend). Everything here is derived from the actual current frontend source code (branch `B2B`), not from prior/stale documentation. Primary input: `docs/context/BACKEND-AUDIT.md` (exhaustive audit of every HTTP call, gateway, facade, and model in the frontend).
@@ -17,6 +17,7 @@ Everything here is derived from the actual current frontend source code (branch
7. [Uploads](#7-uploads) 7. [Uploads](#7-uploads)
8. [Real Backend Implementation Guide](#8-real-backend-implementation-guide) 8. [Real Backend Implementation Guide](#8-real-backend-implementation-guide)
9. [Backend Checklist](#9-backend-checklist) 9. [Backend Checklist](#9-backend-checklist)
10. [Maintenance Mode](#10-maintenance-mode)
--- ---
## 1. Bootstrap ## 1. Bootstrap
@@ -4350,6 +4351,409 @@ independent. Section references point to the assembled backend-integration docum
--- ---
---
## 10. Maintenance Mode
Frontend contract for backend maintenance/availability signals: global, per-tenant,
per-module, read-only, scheduled, and single-feature-disable scenarios. Written from
the current source tree (branch `B2B`) — see `docs/context/BACKEND-AUDIT.md` for the
full backend surface this builds on.
**Existing frontend handling today: none.** There is no maintenance concept anywhere
in the frontend — no model field, no interceptor branch, no route, no component. This
document proposes a contract and marks every open question explicitly as either
"Requires backend decision" (the backend hasn't decided the signal shape) or "No
frontend UI currently exists for this - requires a future frontend task" (the signal
is plausible but no UI has been built to react to it).
The one adjacent, already-built pattern worth reusing is `AuthErrorPageComponent`
(`src/app/core/auth/pages/auth-error-page.component.ts`): a single component keyed by
an error-code route param, rendering `EmptyStateComponent` +
`ButtonComponent`, with a `Record<Code, {title, description, actionLabel}>` copy table
and a `retry()` handler. Section 7 proposes the maintenance screens follow this exact
shape rather than inventing a new one.
---
### 1. Global maintenance
Whole platform down for all tenants.
**What the backend should send:** `503 Service Unavailable` on every endpoint
(including `GET /bootstrap`), with a `Retry-After` header (seconds) and a structured
JSON body (see §7 for exact shape). `GET /bootstrap` is the critical path — it is the
first call the frontend makes (`ApiBootstrapProvider.loadBootstrap()`,
`src/app/core/bootstrap/providers/api-bootstrap.provider.ts`, `GET /bootstrap`) and
every facade that renders anything (`UiRuntimeFacade`, `WebsiteRuntimeFacade`,
`ProjectEditorFacade`, `ContentManagementFacade`, `DiagnosticsFacade`) depends on it
resolving.
**What the frontend currently does:** nothing maintenance-specific. Tracing the call
chain in `src/app/core/config/config.service.ts`:
```ts
this.bootstrap$ = this.provider.loadBootstrap().pipe(
tap(config => { this.bootstrapSnapshot = config; ... }),
shareReplay(1),
catchError(error => {
this.bootstrap$ = undefined;
this.bootstrapSnapshot = null;
return throwError(() => error);
})
);
```
Any bootstrap failure (503 or otherwise) just rethrows. Every one of the ~14 call
sites of `configService.loadBootstrap()` (footer, theme engine, branding engine,
platform-runtime, page-resolver, static-page-resolver, footer-resolver, diagnostics,
etc. — see `Grep` results for `loadBootstrap()` across `src/app`) either does not
subscribe to the error channel at all, or handles it locally and inconsistently.
There is no global "the whole app is down" screen.
**No frontend UI currently exists for this — requires a future frontend task.** A
clean contract would intercept a `503` on the bootstrap call specifically (distinct
from a 503 on a leaf endpoint, which should degrade that one section instead — see
§3) and route to a full-page takeover, structurally identical to
`AuthErrorPageComponent`: a `maintenance-page.component.ts` using
`EmptyStateComponent` + `ButtonComponent`, keyed off the response body's `reason`
(§7), with a retry button that calls `configService.loadBootstrap(true)`.
**Requires backend decision:** whether maintenance state is signaled by response
status alone (`503` on `/bootstrap`) or also via a dedicated
`GET /status` / `GET /maintenance` probe the frontend could poll while showing the
takeover screen, to auto-recover without the user manually retrying.
---
### 2. Per-tenant maintenance
Single tenant disabled while others operate normally.
This ties directly into tenant resolution: `TenantResolverService`
(`src/app/core/config/tenant-resolver.service.ts`) determines the tenant key before
`ApiConfigService.getBaseUrl()` resolves which base URL to call (`tenantApiBaseUrls`
map, or `tenantApiTemplate` with `{tenant}` substituted — see
`docs/context/BACKEND-AUDIT.md` §2). Because tenant resolution happens client-side
before any network call, a per-tenant maintenance signal can only surface through the
response to that tenant's own `GET /bootstrap` call — there is no separate
"is this tenant up" check today.
**What the backend should send:** the *same* `503` + structured body as global
maintenance (§7) on that tenant's `/bootstrap` response. The frontend has no way to
distinguish "this tenant is down" from "the whole platform is down" except by the
response body's content — so the body must carry enough to tell (e.g. a `scope` field:
`"global" | "tenant"`).
**What the frontend currently does:** nothing. `ConfigService.loadBootstrap()` is
tenant-agnostic from the frontend's point of view — it just calls whatever base URL
`ApiConfigService` resolved and doesn't know if a 503 means "this tenant" vs.
"everything."
**Requires backend decision:** the `scope` discriminator mentioned above, and whether
a disabled tenant's static/marketing content (branding, footer) should still resolve
from a cached/last-known bootstrap so the takeover page can show the tenant's own
logo, or whether it's a fully generic (unbranded) page. Given
`BootstrapConfig.branding`/`theme` are only available *after* a successful bootstrap
load, a tenant-branded maintenance page is not achievable without a design decision
here (e.g. serving branding via a separate lightweight endpoint that stays up even
when the tenant is otherwise disabled).
**No frontend UI currently exists for this — requires a future frontend task.** Same
takeover component as §1 can likely serve both scopes once the backend supplies
`scope`, but nothing renders differently for tenant-vs-global today because nothing
renders a maintenance screen at all yet.
---
### 3. Per-module maintenance
E.g. payments down but catalog still browsable.
**Existing granularity concept:** `BootstrapConfig.featureFlags`
(`FeatureFlagsConfig`, `src/app/shared/models/config/feature-flags.model.ts`) —
a flat `Record<string, boolean>` with known keys `wishlist, compare, reviews,
questions, comments, recommendations, blog, chat, analytics, notifications, coupons,
loyalty, giftCards, invoices` and an index signature for tenant-specific extras. This
is a **static, bootstrap-time** on/off switch per feature — not a live "is this
service currently degraded" signal, and it has no `payments` or `catalog` key today.
It's read once at bootstrap load and doesn't change until the next bootstrap refresh.
There is no separate "module health" concept distinct from `featureFlags`. The admin
dashboard's `healthChecks()` / `homeHealthChecks()` (`AdminDashboardFacade`,
`src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts`) are **not**
module-availability checks — they validate the *local bootstrap document itself*
(schema version present, no missing translations, no invalid colors/widget refs/
layouts, draft-exists, etc.), entirely client-side, with no backend health probe
behind any row except product/category counts (which reflect load success/failure of
`ProductFacade`/`CategoryFacade`, not an explicit "payments module is down" signal).
`AdminMonitoringPageComponent` reuses the same boolean-shaped `healthChecks()` — it is
not a live service-status board either.
**What the backend should send:** each domain-specific endpoint (e.g. `POST /cart`,
`POST /orders`, `{qrApiUrl}/qr`) should independently return `503` with the structured
body (§7) with `scope: "module"` and a `module` field (e.g. `"payments"`) when that
subsystem specifically is down, while unrelated endpoints (`GET /category`,
`GET /items/{id}`) keep responding normally. This requires no new bootstrap field —
it's a per-request response behavior, consistent with REST conventions (the resource
itself is unavailable, not the whole API).
**What the frontend currently does:** nothing differentiates a per-module outage from
any other request failure. `ApiService` (`src/app/services/api.service.ts`) has no
per-endpoint error branching for 503; a failed `createCartPayment()`/`createOrder()`
call surfaces through whatever generic error handling the checkout components already
have for network failures (out of scope for this doc — see the sibling
`ERROR_CONTRACT.md` task for the general error-response shape).
**No frontend UI currently exists for this — requires a future frontend task.** The
checkout flow would need a "payments unavailable" inline state (banner or disabled
submit + tooltip, per §7) distinct from a generic error toast, and catalog browsing
would need to keep working untouched — which it structurally already would, since
`ProductFacade`/`CategoryFacade` and the payment calls are fully independent code
paths today (no shared failure state). That independence is a real asset: a payments
outage cannot accidentally break catalog browsing given the current facade
separation, but no UI exists yet to *tell the user* payments specifically are down
rather than "something went wrong."
---
### 4. Read-only mode
Writes disabled, reads still work.
**Does the frontend already assume this is possible?** Partially, structurally, but
not deliberately. Cart state is `LOCAL-ONLY` (`CartService`,
`src/app/services/cart.service.ts`, signal-based, persisted to `localStorage` key
`marketplace_cart`) — adding items to cart, changing quantities, and browsing the cart
UI works entirely client-side with **no backend call at all** until checkout. The
only writes that hit a backend are at the checkout boundary: `POST /cart`
(`createCartPayment`), `POST /orders` (`createOrder`), `POST /purchase-email`, and the
QR/card payment polling. So today, if the backend rejected writes only, catalog
browsing, search, wishlist/compare (also `LOCAL-ONLY`,
`LocalUserExperienceRepository`), and cart-building would all continue working simply
because they never touch the backend — but reviews (`POST /items/{id}/callback`) and
questions (`POST /items/{id}/questiion`) are also writes and would fail the same as
checkout, since both are LIVE endpoints via `ProductDataProvider`.
There is no code today that *checks for* a read-only flag and proactively disables
write UI (e.g. graying out "Add to cart" or the checkout button ahead of time). A
write attempt would only be discovered to be blocked when the write call itself
fails.
**What the backend should send:** `503` (or `403`, see note below) with the
structured body (§7), `scope: "readonly"`, on write endpoints specifically —
`POST /cart`, `POST /orders`, `POST /purchase-email`, `POST /items/{id}/callback`,
`POST /items/{id}/questiion`, `POST /websession/{sessionId}` (cart sync) — while GET
endpoints keep working. `403 Forbidden` is arguably more correct REST semantics for
"this resource forbids this method during a maintenance window" than `503`, but `503`
+ `Retry-After` communicates "temporary" more clearly to a client and is
recommended so the frontend can offer a countdown/retry consistent with §5's pattern.
**Requires backend decision:** which status code is authoritative — this should be
pinned down jointly with whatever `ERROR_CONTRACT.md` settles on for its 5xx
conventions, since read-only is really "a subset of write endpoints return
maintenance-503."
**No frontend UI currently exists for this — requires a future frontend task.** No
bootstrap flag exists to proactively disable checkout/review/question submission
ahead of a failed request (e.g. `featureFlags.readOnly` or a dedicated
`platformStatus.readOnly` field would need to be added to `BootstrapConfig` if the
product wants a proactive banner instead of a reactive failure). Reactive handling
(showing an error when the write call 503s) can reuse the same inline
error-state pattern as §3/§6 once `ERROR_CONTRACT.md` defines the generic error body
handling.
---
### 5. Scheduled maintenance
Advance notice pattern (banner / countdown) ahead of a maintenance window.
**What exists in the frontend today:** nothing. No banner component, no countdown
component, no bootstrap field for an upcoming maintenance window.
**Requires backend decision — proposed minimal contract:** add an optional field to
`BootstrapConfig` (loaded once per session/on refresh via `GET /bootstrap`), e.g.:
```ts
interface ScheduledMaintenanceNotice {
startsAt: string; // ISO 8601
endsAt?: string; // ISO 8601, optional if duration is unknown
scope: 'global' | 'tenant' | 'module';
module?: string; // present when scope === 'module'
messageKey?: string; // optional i18n key/translated string for custom copy
}
```
surfaced as `bootstrap.maintenanceNotice?: ScheduledMaintenanceNotice | null`. This
keeps the mechanism consistent with how the platform already declares other
runtime-configured, backend-authored state (feature flags, tenant config, API
endpoint records all live in the bootstrap document per
`docs/context/BACKEND-AUDIT.md` §6) rather than inventing a new polling endpoint. A
polling `GET /maintenance-notice` endpoint is an alternative if the notice needs to
appear/change without a full bootstrap refresh — that tradeoff is the backend
decision.
**No frontend UI currently exists for this — requires a future frontend task.** A
dismissible banner component reading `bootstrap.maintenanceNotice` and showing a
localized "maintenance starts in Xh Ym" countdown would need to be built and mounted
at a layout level (header or a global banner slot) — no such banner or countdown
component exists in `src/app/shared/ui/` today.
---
### 6. Temporary feature disable
Single feature toggled off without full maintenance — e.g. reviews temporarily
disabled while the rest of the product page works.
**This is the one scenario the frontend already has a real mechanism for**, via
`BootstrapConfig.featureFlags` (§3). Setting `featureFlags.reviews = false` in the
bootstrap document is exactly the existing, live mechanism for "reviews are off right
now" — it's read by whatever consumes `FeatureConfigService`
(`src/app/core/config/*`) and gates the relevant UI. This is a **deploy/config-time**
toggle (changes on next bootstrap load), not a live incident-response toggle, but
structurally it is the same shape a backend team would use to kill a misbehaving
feature quickly: update the bootstrap document (or whatever backend-side config
drives it), and the next bootstrap fetch picks it up.
**Recommendation:** reuse `featureFlags` for this scenario rather than introducing a
parallel mechanism — it already exists, is already wired through to the UI in the
relevant places, and matches the "temporary, single-feature, not a full outage"
framing exactly. No backend decision needed for the *mechanism*; only for *process*
(how fast a flag flip propagates — depends on bootstrap cache/refresh cadence, which
is outside this doc's scope).
**Gap:** `featureFlags` has no `payments` or `catalog` key and is a boolean only — it
can't express "reviews disabled with reason X, back at time Y" the way §5's proposed
`maintenanceNotice` can. If product wants a "reviews are temporarily unavailable —
back tomorrow" message rather than the feature silently disappearing, that needs the
richer shape from §5, scoped to `module`, not a plain `featureFlags` boolean.
---
### 7. Recommended API responses
All maintenance-scenario responses use HTTP `503 Service Unavailable` (except the
read-only debate in §4) with a `Retry-After` header (seconds, standard HTTP) and a
JSON body. This is written to be consistent with, not contradict, whatever
`ERROR_CONTRACT.md` (sibling task, in progress) settles on for its general
structured-error envelope — if that doc defines a different top-level error
shape (e.g. `{ error: { code, message, ... } }` vs. a flatter shape), this body
should be nested under that envelope rather than duplicating a competing shape.
Pending that reconciliation, the fields below are what the frontend needs regardless
of the outer envelope:
```json
{
"status": 503,
"code": "maintenance",
"scope": "global",
"module": null,
"reason": "scheduled",
"message": "The marketplace is temporarily unavailable for scheduled maintenance.",
"retryAfter": 1800,
"startedAt": "2026-07-26T02:00:00Z",
"expectedEndAt": "2026-07-26T03:00:00Z"
}
```
Field notes:
- `scope`: `"global" | "tenant" | "module" | "readonly"` — lets the frontend pick the
right UI (full takeover vs. inline banner vs. disabled control) without guessing
from status code alone.
- `module`: present only when `scope === "module"` (e.g. `"payments"`, `"reviews"`).
- `reason`: `"scheduled" | "incident" | "disabled"` — free-form enough for the
frontend to choose copy tone (planned vs. unplanned) without needing new fields
per scenario.
- `retryAfter`: mirrors the `Retry-After` header in the body too, so a client that
only reads JSON (not headers) still gets it — useful since some HttpClient error
paths surface the body more readily than headers depending on interceptor
structure.
- `startedAt` / `expectedEndAt`: optional, ISO 8601, for countdown/banner copy (§5).
Per-scenario summary:
| Scenario | Status | `scope` | Notes |
|---|---|---|---|
| Global | 503 | `"global"` | On every endpoint, especially `/bootstrap` |
| Per-tenant | 503 | `"tenant"` | On that tenant's `/bootstrap` and all its endpoints |
| Per-module | 503 | `"module"` | Only on that module's endpoints (e.g. `/cart`, `/orders`) |
| Read-only | 503 or 403 | `"readonly"` | Only on write endpoints; GETs unaffected — pin down with `ERROR_CONTRACT.md` |
| Scheduled (advance notice) | 200, via `bootstrap.maintenanceNotice` | n/a | Not an error response — a proactive field on the normal `/bootstrap` payload, see §5 |
| Temporary feature disable | 200, via `bootstrap.featureFlags.<key> = false` | n/a | Not an error response — existing bootstrap mechanism, see §6 |
---
### 8. Frontend behavior
Grounded in the UI patterns that already exist (`EmptyStateComponent`
(`src/app/shared/ui/empty-state/empty-state.component.ts`), the `errorTitle` /
`error` / `retry` i18n-key convention used across catalog, product details, and
generic list widgets (`src/app/i18n/en.ts`), and `AuthErrorPageComponent`'s
code-keyed full-page pattern). No new UI concepts are invented below beyond composing
these.
| Scenario | Recommended UI | Existing pattern reused | Status |
|---|---|---|---|
| Global maintenance | Full-page takeover, replaces the entire app shell (no header/footer, since branding may be unavailable — see §2) | `AuthErrorPageComponent` shape: `EmptyStateComponent` + `ButtonComponent`, code-keyed copy, `retry()` action | No frontend UI currently exists for this — requires a future frontend task |
| Per-tenant maintenance | Same full-page takeover as global, ideally tenant-branded if the backend decision in §2 allows branding to still resolve | Same as above | No frontend UI currently exists for this — requires a future frontend task |
| Per-module maintenance | Inline empty-state/banner scoped to the affected section only (e.g. checkout step shows `EmptyStateComponent` with `errorTitle`/`error`/`retry` copy; catalog pages untouched) | `EmptyStateComponent` + the `errorTitle`/`error`/`retry` i18n triple already used in `catalog`/`productDetails`/generic-list translations | No frontend UI currently exists for this — requires a future frontend task |
| Read-only mode | Disabled write control (e.g. "Add to cart" / "Submit review" button) + tooltip explaining why, OR a reactive error state on submit if no proactive flag exists (§4) | Disabled-button-plus-tooltip is a common pattern in the design system but not wired to any maintenance signal today | No frontend UI currently exists for this — requires a future frontend task |
| Scheduled maintenance | Dismissible banner at layout/header level with countdown copy | No banner/countdown component exists in `src/app/shared/ui/` today | No frontend UI currently exists for this — requires a future frontend task |
| Temporary feature disable | Feature's own UI simply doesn't render (existing `featureFlags` gating), optionally with a short "temporarily unavailable" note if `messageKey` (§5) is present | Existing `featureFlags` boolean gating (already live) | Existing mechanism works; richer messaging is the only gap |
---
### Summary: what's proposed/new vs. what already exists
**Already exists and can be reused as-is:**
- `BootstrapConfig.featureFlags` — static per-feature kill switch (§3, §6).
- `EmptyStateComponent` + `errorTitle`/`error`/`retry` i18n convention — the inline
error-state building block for any scenario.
- `AuthErrorPageComponent` — the full-page-takeover shape (code-keyed copy record,
`EmptyStateComponent` + `ButtonComponent`, `retry()` handler) to model a maintenance
page after.
- Cart's `LOCAL-ONLY` design already means most of "read-only browsing" works
incidentally, since browsing/cart-building never call the backend.
**Proposed/new (this document introduces):**
- The `scope`/`module`/`reason` structured 503 body (§7).
- `bootstrap.maintenanceNotice` (§5) for scheduled-maintenance advance notice.
- A dedicated `maintenance-page.component.ts` full-page takeover (§1/§2).
- Inline per-module/read-only error and disabled-control states wired to the new 503
shape (§3/§4).
---
### Requires backend decision (full list)
- §1: whether a dedicated `GET /status`/`GET /maintenance` probe should exist for
auto-recovery polling, beyond a plain 503 on `/bootstrap`.
- §2: the `scope` discriminator (`"global"` vs `"tenant"`) so the frontend can tell
the two apart from a single tenant's bootstrap response; and whether a
disabled tenant's branding can still resolve for a branded takeover page.
- §3: none beyond adopting the §7 response shape per-endpoint — this one is mostly
frontend-gap, not backend-undecided.
- §4: which status code is authoritative for read-only (`503` vs `403`) — to be
pinned down jointly with `ERROR_CONTRACT.md`.
- §5: whether scheduled-maintenance notice ships via a `bootstrap.maintenanceNotice`
field (proposed) or a separate polling endpoint.
- §7: how this document's 503 body nests inside whatever outer envelope
`ERROR_CONTRACT.md` defines.
### No frontend UI currently exists for this — requires a future frontend task (full list)
- Global maintenance full-page takeover component.
- Per-tenant maintenance takeover (branded or not, pending §2's backend decision).
- Per-module inline maintenance banner/empty-state wiring on checkout/payment flows.
- Proactive read-only disabling of write controls (Add to cart / Submit review /
Submit question / Checkout) ahead of a failed request.
- Scheduled-maintenance banner + countdown component at the layout/header level.
- Richer "temporarily unavailable, back at X" messaging for `featureFlags`-gated
features (today they just silently don't render — no explanatory copy).
---
## Appendix: `docs/TODO.md` items merged into this document (2026-07-26) ## Appendix: `docs/TODO.md` items merged into this document (2026-07-26)
Final Project Closeout moved every backend-shaped item out of `docs/TODO.md` into this Final Project Closeout moved every backend-shaped item out of `docs/TODO.md` into this

View File

@@ -2,7 +2,7 @@
Replaces the old `docs/Project-Editor.md` (content merged in below and extended with the Sprint 19 field-description/dropdown work). Replaces the old `docs/Project-Editor.md` (content merged in below and extended with the Sprint 19 field-description/dropdown work).
The Project Editor (`src/app/features/project-editor/`) edits the tenant's `BootstrapConfig` (`docs/BACKEND_API.md#4-bootstrap`) directly — no parallel model. It is out of scope for products, categories, orders, or analytics management (those live under `features/admin/*`/`features/backoffice/*`, see `docs/ADMIN.md`). The Project Editor (`src/app/features/project-editor/`) edits the tenant's `BootstrapConfig` (`docs/BACKEND.md#1-bootstrap`) directly — no parallel model. It is out of scope for products, categories, orders, or analytics management (those live under `features/admin/*`/`features/backoffice/*`, see `docs/BACKEND.md` §3 CRUD Contracts).
``` ```
src/app/features/project-editor/ src/app/features/project-editor/
@@ -46,7 +46,7 @@ Route: `/edit/:section` or `/{lang}/edit/:section`. `/backoffice/static-pages` (
- **Reset section**: reverts one section's bootstrap keys (per `EDITOR_SECTION_BOOTSTRAP_KEYS` in `models/project-editor.model.ts`) to `originalBootstrap`. Confirmation required. - **Reset section**: reverts one section's bootstrap keys (per `EDITOR_SECTION_BOOTSTRAP_KEYS` in `models/project-editor.model.ts`) to `originalBootstrap`. Confirmation required.
- **Reset draft**: reverts the entire bootstrap to `originalBootstrap` and clears the persisted local draft. Confirmation required. - **Reset draft**: reverts the entire bootstrap to `originalBootstrap` and clears the persisted local draft. Confirmation required.
- **Per-field reset is not implemented** — no per-field default registry exists; only section- and project-level reset. - **Per-field reset is not implemented** — no per-field default registry exists; only section- and project-level reset.
- **No backend persistence exists for any of this today** — see `docs/BACKEND_API.md#67-builder--bootstrap-draftpublishvalidate-planned-highest-priority` for the endpoints needed. - **No backend persistence exists for any of this today** — see `docs/BACKEND.md` §1 (Bootstrap: Draft vs Published) and §8 (Real Backend Implementation Guide) for the endpoints needed.
## Configuration schema, form engine, and validation architecture (Sprint X+1) ## Configuration schema, form engine, and validation architecture (Sprint X+1)
@@ -88,7 +88,7 @@ No changes to `ProjectEditorIoService` (export/import), `ProjectEditorDraftStora
## Admin Authentication (QR reuse) ## Admin Authentication (QR reuse)
Admin login shares the exact same Telegram QR/session backend and `TelegramLoginComponent` as customer login (`mode: 'admin'` vs `'customer'`) — only the cookie name/`SameSite` policy, token storage keys, and guard differ. **Backend gap:** because both flows hit the same session endpoint, the backend cannot distinguish an admin scan from a customer scan today — real admin authorization must be enforced server-side. Full detail: `docs/BACKEND_API.md#25-the-admin-authorization-gap-critical--security-relevant-unresolved`. Admin login shares the exact same Telegram QR/session backend and `TelegramLoginComponent` as customer login (`mode: 'admin'` vs `'customer'`) — only the cookie name/`SameSite` policy, token storage keys, and guard differ. **Backend gap:** because both flows hit the same session endpoint, the backend cannot distinguish an admin scan from a customer scan today — real admin authorization must be enforced server-side. Full detail: `docs/BACKEND.md` §4 Authentication and §5 Security (permission matrix).
## Design system primitives (post-Sprint 30 redesign) ## Design system primitives (post-Sprint 30 redesign)
@@ -125,7 +125,7 @@ Interaction feedback + motion applied consistently, all gated behind `prefers-re
**HTML-mode validation (Sprint X+2):** switching from raw-HTML back to the visual surface now runs `schema/validators/primitives.validateHtml` (stack-based tag-balance check) first; a malformed edit (unclosed/mismatched tag) stays in code mode with an inline error instead of silently corrupting the visual editor. **HTML-mode validation (Sprint X+2):** switching from raw-HTML back to the visual surface now runs `schema/validators/primitives.validateHtml` (stack-based tag-balance check) first; a malformed edit (unclosed/mismatched tag) stays in code mode with an inline error instead of silently corrupting the visual editor.
**Caveat:** implemented on the deprecated `document.execCommand` API. It works in all current browsers today but is a legacy web API with no modern drop-in replacement; if a future browser drops it, this component needs a rewrite (e.g. a maintained rich-text library). By design it emits **raw, unsanitized** HTML — sanitization is a storefront-render concern, not an authoring one (see `docs/BACKEND_API.md#68-builder--content-pages--cms-planned` on server-side content moderation on publish; `StaticPageComponent` and `StaticPagePreviewComponent` both run content through `DomSanitizer` before render). **Caveat:** implemented on the deprecated `document.execCommand` API. It works in all current browsers today but is a legacy web API with no modern drop-in replacement; if a future browser drops it, this component needs a rewrite (e.g. a maintained rich-text library). By design it emits **raw, unsanitized** HTML — sanitization is a storefront-render concern, not an authoring one (see `docs/BACKEND.md` §3 CRUD Contracts, CMS, on server-side content moderation on publish; `StaticPageComponent` and `StaticPagePreviewComponent` both run content through `DomSanitizer` before render).
## Field-description / dropdown UX (Sprint 19+) ## Field-description / dropdown UX (Sprint 19+)

View File

@@ -1,490 +0,0 @@
# Error Response Contract
Single unified error-response format the backend must return for every non-2xx
response across all API surfaces (marketplace API, payment/QR API, session
auth API, Ed25519 admin auth API, and any future admin/builder/backoffice
APIs). Derived by cross-referencing every place the Angular frontend
currently parses, catches, or reacts to an HTTP error — see
`docs/context/BACKEND-AUDIT.md` for the full backend-surface audit this is
based on.
**Finding: the frontend does not currently parse any backend error envelope.**
No `HttpInterceptor` in the pipeline (`src/app/app.config.ts`
`mockDataInterceptor, apiBaseUrlInterceptor, apiHeadersInterceptor,
adminAuthHeadersInterceptor, cacheInterceptor`) inspects error responses —
all five only touch outgoing requests or successful GET caching. Every
consumer that reacts to failure does so on the RxJS/`HttpErrorResponse`
level (`error.status`, `error.message`), never on a parsed JSON error body.
The one exception is the Ed25519 admin-auth flow, which has a client-side
`AuthErrorCode` union but (see §"Known frontend gap" below) currently derives
it from **HTTP status only**, not from any body field. Because of this, the
envelope below is a **clean proposal, not a reverse-engineered contract**
every shape decision is marked accordingly.
## The envelope
```json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"status": 422,
"requestId": "b3f1c2a0-4e21-4d3a-9e77-1e8f6a2d9c11",
"details": [
{ "field": "sku", "code": "REQUIRED", "message": "SKU is required." }
]
}
}
```
**Requires backend decision: adopt this envelope.** The frontend has no
existing opinion to preserve (no code reads `error.error.code` today), so
this is a recommendation, chosen to be consistent with the shapes the
frontend *does* already have opinions about:
- Top-level `{ code, message, status }` mirrors the existing `AuthError`
interface (`src/app/core/auth/models/auth-error.model.ts:13-18`) almost
field-for-field — reusing that shape means the Ed25519 auth module can
parse the new envelope with only a `status` fallback removed, not a
rewrite.
- `details[]` entries `{ field, code, message }` mirror the existing
client-side `ProjectValidationIssue` convention (`code, message, section,
fieldKey, severity``src/app/features/project-editor/services/
project-validator.service.ts:23-36`, consumed via `ProjectEditorFacade
.fieldError(fieldKey)`). No backend field-error shape exists to preserve
today (admin CRUD is 100% local/mock — see BACKEND-AUDIT.md §14), so this
is the closest existing frontend convention to align a real one to.
- `requestId` is new (no frontend code reads it yet) — recommended so
support/ops can correlate a user-visible failure to server logs. If
adopted, the frontend would need a small addition to surface it in
error-state UI (not present today).
Field notes:
| Field | Required | Notes |
|---|---|---|
| `error.code` | yes | Stable, machine-readable, `UPPER_SNAKE_CASE`. Never localized. This is what the frontend should branch on, not `message`. |
| `error.message` | yes | Human-readable fallback (English), safe to show only when the frontend has no i18n mapping for `code`. Never the sole signal for UI branching. |
| `error.status` | yes | Must equal the HTTP status of the response (redundant with the transport layer, but the frontend's own `AuthError.status` already carries this, so keep parity). |
| `error.requestId` | recommended | Opaque correlation id, echoed in logs. |
| `error.details` | only for 422 | Array of field-level issues, see §422 below. |
---
## Status-by-status contract
### 401 — Unauthenticated / expired token
```json
{
"error": {
"code": "UNAUTHENTICATED",
"message": "Authentication is required to access this resource.",
"status": 401,
"requestId": "…"
}
}
```
**Frontend reaction today:**
- **Admin Ed25519 flow** (`AuthService.login()`/`refresh()` in
`src/app/core/auth/services/auth.service.ts`): any `HttpErrorResponse` with
status 401 is mapped via `authErrorCodeFromStatus()``AuthErrorCode
'unauthorized'`, surfaced by `AuthErrorPageComponent`
(`src/app/core/auth/pages/auth-error-page.component.ts`) with copy
"Unauthorized… Sign in" and a button that calls `router.navigateByUrl
('/admin-login')`.
- **Customer Telegram session auth** (`TelegramSessionApiService`,
`AuthService` customer-facing, `src/app/services/auth.service.ts`): no
code branches on a 401 status anywhere — session validity is instead
polled via `checkSessionOnce()` returning `AuthSession | null`. **Requires
backend decision**: whether/how a mid-session 401 on a customer-facing
marketplace call (e.g. `POST /cart`, `POST /orders`) should be surfaced —
today it would fall through to each caller's generic `catchError`/`error:`
handler (if any) with no unified "session expired, please re-auth" UX.
- **Admin backoffice CRUD (products/orders/users/etc.)**: these facades
(`AdminUsersFacade`, `AdminOrdersFacade`, …) currently only ever talk to
local/mock gateways, so no real 401 has ever reached them. Their existing
generic `error` boolean signal + `common.errorTitle`/`common.errorDescription`
+ retry button (see "Generic list-page error UI" below) is the pattern a
real 401 would fall into **unless** the facades are updated to branch on
status — they don't today.
### 403 — Forbidden (wrong role or tenant)
```json
{
"error": {
"code": "FORBIDDEN",
"message": "Your account does not have permission to perform this action.",
"status": 403,
"requestId": "…"
}
}
```
**Frontend reaction today:** Ed25519 admin flow only. `authErrorCodeFromStatus(403)`
`'forbidden'``AuthErrorPageComponent` copy "Forbidden… Back to
dashboard", button `router.navigateByUrl('/backoffice')`. No tenant-scoping
distinction exists in this code path — a 403 caused by wrong role and a 403
caused by wrong tenant render identical copy today. **Requires backend
decision**: if tenant-mismatch should be visually distinct from
role-mismatch, it needs its own `error.code` (e.g. `TENANT_FORBIDDEN` vs
`ROLE_FORBIDDEN`) since the frontend has no other signal to key off besides
status today.
### 404 — Not found
```json
{
"error": {
"code": "NOT_FOUND",
"message": "The requested item could not be found.",
"status": 404,
"requestId": "…"
}
}
```
**Frontend reaction today:** No code path distinguishes 404 from any other
failure. `catalog-container.component.ts` and
`product-details-container.component.ts` both catch *any* load error and
render the same generic `catalog.errorTitle`/`productDetails.errorTitle`
empty-state (`en.ts:176,277`) — a real 404 (product deleted) and a 500
(server crash) look identical to the user today. **Requires backend
decision**: whether the frontend should be enhanced to show a distinct
"this product no longer exists" message for 404 specifically (would need a
status/code check added to those two containers — not present now).
### 409 — Conflict
```json
{
"error": {
"code": "CONFLICT",
"message": "A category with this slug already exists.",
"status": 409,
"requestId": "…"
}
}
```
**Frontend reaction today:** no code catches or branches on 409 anywhere.
The one related concept in the codebase is `AdminCategoriesGateway
.isSlugTaken(slug, excludingId)` (BACKEND-AUDIT.md §14) — a **proactive**
pre-check call the frontend makes *before* submitting, not a reaction to a
409 conflict response. **Requires backend decision**: whether create/update
endpoints should also return 409 on the same slug/uniqueness conflict as a
race-condition backstop, and whether the frontend should add a 409 handler
that surfaces `error.details` inline (there is no such handler today —
`isSlugTaken` is the only existing conflict-avoidance mechanism, and it is
best-effort/TOCTOU-prone).
### 422 — Validation failure
```json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"status": 422,
"requestId": "…",
"details": [
{ "field": "sku", "code": "REQUIRED", "message": "SKU is required." },
{ "field": "price", "code": "OUT_OF_RANGE", "message": "Price must be greater than 0." }
]
}
}
```
**Frontend reaction today:** no admin form currently parses a backend
validation-error body — all admin CRUD is local/mock (BACKEND-AUDIT.md §14),
so there has never been a real 422 to react to. The frontend **does** have
an established field-error UI convention worth preserving: `ProjectEditorFacade
.fieldError(fieldKey): string | null`
(`src/app/features/project-editor/facade/project-editor.facade.ts:371-374`)
reads from `issuesByField` (a `Map<fieldKey, ProjectValidationIssue[]>`) and
returns the first issue's `message`, for inline per-field template binding.
That mechanism is entirely client-side validation today (`ProjectValidator`
service), not backend-driven. **Requires backend decision**: adopting
`details[].field` as the join key would let a future `fieldError()`-style
adapter merge backend 422 errors into the same inline-error UI pattern
without inventing a second one — but the adapter itself does not exist yet
and would need to be built.
### 429 — Rate limited
```json
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please slow down.",
"status": 429,
"requestId": "…",
"retryAfterSeconds": 30
}
}
```
**Frontend reaction today: none whatsoever.** No interceptor, facade, or
component in the codebase references `429` or "rate limit" in any form (grepped
across `src/`). **Requires backend decision** on every aspect:
- Whether the backend sends a `Retry-After` HTTP header, a body field
(`retryAfterSeconds` above), or both.
- Whether the frontend should retry automatically (with backoff) or only
show the user a "please wait Ns" message. Recommend: since no retry
interceptor exists today, add one is a new build item, not a config
change.
### 500 — Server error
```json
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again.",
"status": 500,
"requestId": "…"
}
}
```
**Frontend reaction today:** falls into whichever generic catch-all a given
caller has:
- Ed25519 admin flow: `authErrorCodeFromStatus()` default branch → `status
>= 500 ? 'backend-unavailable' : 'unauthorized'` → same
"Backend unavailable… Retry" screen as a network-down 503 (see below) —
the frontend does not distinguish "server is up but this request 500'd"
from "server is completely unreachable."
- Admin list pages (`AdminUsersFacade` and siblings): generic `error`
boolean signal set to `true` in the RxJS `error:` callback, rendering
`common.errorTitle`/`common.errorDescription` + a retry button that
re-invokes the same load call. No status differentiation.
- Storefront catalog/product pages: same generic empty-state pattern as 404
above.
- `LocationService.getRegions()`-equivalent: falls back silently to 6
hardcoded regions on *any* error (including 500), no user-visible error at
all (`src/app/services/location.service.ts`).
### 503 — Maintenance / unavailable
```json
{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "The service is temporarily unavailable. Please try again shortly.",
"status": 503,
"requestId": "…"
}
}
```
**Frontend reaction today:** Ed25519 admin flow only, via the same
`status >= 500` branch as 500 above → `'backend-unavailable'` →
`AuthErrorPageComponent` "Backend unavailable… Retry." No other code path
reacts to 503 specifically today (marketplace API calls that 503 would just
fall into each caller's generic error handling, same as 500 above).
**Distinguishing signal from Maintenance mode (see next section):** use
`error.code`, not the HTTP status. A plain infra 503 (database down,
overload) should send `"code": "SERVICE_UNAVAILABLE"`; a deliberate
maintenance window should send `"code": "MAINTENANCE_MODE"` (still with HTTP
status 503, since it's a byte-identical "the service is not accepting
requests" situation, but a different reason). This is the only way for the
frontend to build a distinct maintenance-mode UX later, since status alone
is not enough. `docs/MAINTENANCE_MODE.md` (sibling task, in progress) owns
the UX/copy for the maintenance case — this document only fixes the wire
signal it must key off (`error.code === "MAINTENANCE_MODE"`), so the two
docs stay consistent without duplicating UX detail here.
### Maintenance mode
Same HTTP status as above (503), distinguished purely by `error.code`:
```json
{
"error": {
"code": "MAINTENANCE_MODE",
"message": "This marketplace is temporarily down for maintenance.",
"status": 503,
"requestId": "…",
"maintenanceUntil": "2026-07-26T04:00:00Z"
}
}
```
`maintenanceUntil` (ISO 8601, optional) lets the maintenance-mode UX (sibling
doc) show an ETA if the backend has one. **Requires backend decision:**
whether `maintenanceUntil` is populated reliably enough to promise in UI, or
should be treated as advisory-only.
**Frontend reaction today:** none — no maintenance-mode concept exists in
the frontend at all currently (confirmed: no matches for "maintenance" in
`src/`). This entire row is new; the sibling `docs/MAINTENANCE_MODE.md` task
should treat it as building from scratch, not preserving anything.
### Tenant disabled
```json
{
"error": {
"code": "TENANT_DISABLED",
"message": "This marketplace is not currently active.",
"status": 403,
"requestId": "…"
}
}
```
**Frontend reaction today: none.** Tenant resolution
(`TenantResolverService`, `src/app/core/config/tenant-resolver.service.ts`)
only ever resolves *which* tenant a request targets (via host/subdomain); no
code path in the audited surface handles a backend telling the frontend
"this tenant exists but is disabled." **Requires backend decision** end to
end: status code (403 recommended, to reuse the existing `forbidden`
auth-error screen plumbing, vs. a dedicated status), and whether this should
route to a dedicated "tenant disabled" screen or reuse
`AuthErrorPageComponent`'s `forbidden` copy (which currently says "Your
account role does not have permission" — wrong wording for a
tenant-disabled scenario, would need a new `AuthErrorCode` entry and copy if
reused).
### Rate limit
See **429** above — same contract, called out separately here only because
the task list asked for it as its own row. No additional distinguishing
signal needed beyond the 429 status + `RATE_LIMITED` code.
### Expired token
```json
{
"error": {
"code": "TOKEN_EXPIRED",
"message": "Your session has expired. Please sign in again.",
"status": 401,
"requestId": "…"
}
}
```
**Frontend reaction today — known gap, read carefully:** `AuthService
.refresh()` (`src/app/core/auth/services/auth.service.ts:65-76`) has a
client-side `AuthErrorCode` value `'session-expired'` and passes it as
`fallbackCode` into `handleAuthError()`. **However**, `toAuthErrorShape()`
(lines 110-118) only uses `fallbackCode` when the caught error is **not** an
`HttpErrorResponse` — for an actual HTTP error it always calls
`authErrorCodeFromStatus(error.status)`, which maps 401 → `'unauthorized'`,
never `'session-expired'`, regardless of `fallbackCode`. So today, a real
backend 401 on `/refresh` renders the **generic "Unauthorized" screen**, not
"Session expired" — the "Session expired" screen is only ever reached via
the *no-refresh-token-present* client-side branch (line 67-70), never from a
real HTTP response. **Requires backend decision + frontend fix**: for a
distinct "your session expired, please sign in again" screen to actually
render on a real backend 401, either (a) the backend returns a body
`error.code: "TOKEN_EXPIRED"` and the frontend is updated to read it instead
of relying solely on `authErrorCodeFromStatus(status)`, or (b) this
distinction is accepted as unreachable today and left as future work. Flag
this gap explicitly to whoever picks up the fix — it is a pre-existing bug,
not something this contract can silently paper over.
### Invalid signature
```json
{
"error": {
"code": "INVALID_SIGNATURE",
"message": "The signed challenge could not be verified.",
"status": 401,
"requestId": "…"
}
}
```
Ties to the Ed25519 admin-auth flow documented in `AUTHENTICATION.md`
(sibling task, in progress) — keep the `code` value (`INVALID_SIGNATURE`)
consistent with whatever that doc names the failure mode, since this
contract only defines the wire shape and that doc owns the auth-flow
narrative.
**Frontend reaction today:** same gap as "Expired token" above.
`AuthService.login()` passes `fallbackCode: 'invalid-signature'` into
`handleAuthError()`, but `toAuthErrorShape()` discards it for any real
`HttpErrorResponse` and maps a 401 from `/verify` to the generic
`'unauthorized'` screen via `authErrorCodeFromStatus()`. The dedicated
"Invalid signature… Try again" screen
(`src/app/core/auth/pages/auth-error-page.component.ts:21-25`) exists in the
copy table but is **currently unreachable from a real backend response** for
the same reason as `session-expired` above. **Requires backend decision +
frontend fix**: backend must send a body-level `error.code:
"INVALID_SIGNATURE"` and the frontend's `toAuthErrorShape()` must be updated
to prefer a body code over the status-only mapping, or this screen stays
dead code reachable only via non-HTTP error paths.
---
## Generic list-page error UI (for reference)
Every admin backoffice list page (`AdminUsersFacade`, `AdminOrdersFacade`,
`AdminMonitoringFacade`, `AdminModerationFacade`, `AdminTransactionsFacade`,
`AdminProductsFacade`, `AdminCategoriesFacade`, `AdminAnalyticsFacade`,
`AdminCustomersFacade`, `AdminDashboardFacade`) follows the same shape,
added by RC-02 (`e153a67 fix(backoffice): add error+retry states to Users,
Monitoring, Analytics, Reports`):
```ts
readonly error = signal(false);
// on load:
error: () => { this.items.set([]); this.loading.set(false); this.error.set(true); }
```
```html
@else if (facade.error()) {
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate">
<span slot="actions">
<app-button variant="primary" (click)="facade.loadAll()">{{ 'common.retry' | translate }}</app-button>
</span>
</app-empty-state>
}
```
This is a **boolean** error flag — it does not branch on HTTP status or
`error.code` today. Every status in this contract (401/403/404/409/422/429/
500/503) would currently collapse into the same generic "Something went
wrong / retry" UI on these pages **unless** the facades are individually
updated to inspect `error.code`/`status` and branch — none do today. Wiring
that up is out of scope for this document (it defines the wire contract);
flagging it here so whoever wires real backends into these facades knows
the current ceiling of frontend error UX is "generic retry," not
per-status handling, except in the Ed25519 admin-auth module.
---
## Summary: "Requires backend decision" items
- **Envelope adoption** — the `{ error: { code, message, status, requestId,
details? } }` shape itself; no frontend code parses any envelope today.
- **401 on customer-facing marketplace calls** (`/cart`, `/orders`, etc.) —
no unified "session expired, please re-auth" UX exists for the customer
Telegram-session flow.
- **403 tenant-mismatch vs role-mismatch** distinct copy/code.
- **404 vs generic-error distinct UX** on catalog/product pages (currently
identical).
- **409 conflict handling on submit** (today only a proactive
`isSlugTaken` pre-check exists; no reactive 409 handler).
- **422 `details[]` → inline field-error adapter** for admin forms (the
client-side `fieldError()` convention exists but nothing feeds it from a
backend response yet).
- **429 rate-limit contract end to end** — header vs body, retry-after
value, and whether the frontend auto-retries (nothing exists today).
- **Maintenance-mode `maintenanceUntil` reliability** — advisory only, or
can the frontend promise an ETA.
- **Tenant-disabled status code and screen** — reuse `forbidden` copy (wrong
wording today) vs. add a dedicated `AuthErrorCode`.
- **Expired-token / invalid-signature body-code fix** — both are
**pre-existing frontend bugs**, not just missing decisions:
`toAuthErrorShape()` in `auth.service.ts` currently derives the error code
from HTTP status only and ignores the `fallbackCode` for real HTTP errors,
so the `'session-expired'` and `'invalid-signature'` screens are dead code
from any real backend response today. Fixing this requires both a backend
body `error.code` and a frontend change to prefer it.

View File

@@ -25,7 +25,7 @@ src/app/
- Storefront: `/`, `/catalog`, `/catalog/:id`, `/product/:id` (legacy `/item/:id` and `/category/:id[/items]` redirect for compatibility). - Storefront: `/`, `/catalog`, `/catalog/:id`, `/product/:id` (legacy `/item/:id` and `/category/:id[/items]` redirect for compatibility).
- Static/CMS pages resolve dynamically: `/:lang/:staticPath` (legacy `/:lang/page/:key` kept for compatibility) — no hardcoded page list, resolved from `bootstrap.staticPages`. - Static/CMS pages resolve dynamically: `/:lang/:staticPath` (legacy `/:lang/page/:key` kept for compatibility) — no hardcoded page list, resolved from `bootstrap.staticPages`.
- Project Editor: `/edit/:section` or `/{lang}/edit/:section`. - Project Editor: `/edit/:section` or `/{lang}/edit/:section`.
- Admin/backoffice: `/:lang/backoffice/**`, guarded by `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`) — dashboard, products (fully wired), categories/static-pages/transactions/orders/media (routed to `BackofficeComingSoonPageComponent` placeholders pending features). See `docs/ADMIN.md`. - Admin/backoffice: `/:lang/backoffice/**`, guarded by `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`) — dashboard, products, categories, orders, transactions, users, moderation, media, monitoring, analytics. See `docs/BACKEND.md` for the data-source contract for each.
- Dev-only diagnostics: `/__diagnostics` (excluded from production). - Dev-only diagnostics: `/__diagnostics` (excluded from production).
## i18n system ## i18n system
@@ -51,4 +51,4 @@ src/app/
## Dynamic widget/section rendering from bootstrap JSON ## Dynamic widget/section rendering from bootstrap JSON
Full detail in `docs/ARCHITECTURE.md` and `docs/BACKEND_API.md#4-bootstrap`. Summary: `page config (bootstrap.pages) -> Section Engine (order/layout/visibility) -> Page Renderer -> Widget Host (resolves component via Widget Manifest + data via Data Source Resolver) -> widget component (props + resolved data only)`. Nothing in this pipeline calls an API directly except the Data Source Resolver, which delegates to `CategoryFacade`/`ProductFacade`. Full detail in `docs/ARCHITECTURE.md` and `docs/BACKEND.md#1-bootstrap`. Summary: `page config (bootstrap.pages) -> Section Engine (order/layout/visibility) -> Page Renderer -> Widget Host (resolves component via Widget Manifest + data via Data Source Resolver) -> widget component (props + resolved data only)`. Nothing in this pipeline calls an API directly except the Data Source Resolver, which delegates to `CategoryFacade`/`ProductFacade`.

View File

@@ -1,6 +1,6 @@
# Known Issues # Known Issues
Real, reproducible, currently-open frontend bugs only. Everything that needed a product/business decision moved to `docs/PRODUCT_BACKLOG.md`; everything nice-to-have moved to `docs/FUTURE_FEATURES.md`; everything backend-shaped moved to `docs/BACKEND_INTEGRATION.md`. Re-verified against source 2026-07-26. Real, reproducible, currently-open frontend bugs only. Everything that needed a product/business decision moved to `docs/PRODUCT_BACKLOG.md`; everything nice-to-have moved to `docs/FUTURE_FEATURES.md`; everything backend-shaped moved to `docs/BACKEND.md`. Re-verified against source 2026-07-26.
## Open ## Open
@@ -17,11 +17,11 @@ Real, reproducible, currently-open frontend bugs only. Everything that needed a
`'invalid-signature'`. Both screens exist and are wired, but are permanently `'invalid-signature'`. Both screens exist and are wired, but are permanently
unreachable from any real backend response today. unreachable from any real backend response today.
- **Fix requires both sides**: a backend that returns a distinguishable - **Fix requires both sides**: a backend that returns a distinguishable
`error.code` in the response body (see `docs/ERROR_CONTRACT.md`), and a small `error.code` in the response body (see `docs/BACKEND.md` §6 Error Model), and a small
frontend change to `toAuthErrorShape()` to prefer that body code over the frontend change to `toAuthErrorShape()` to prefer that body code over the
blanket status-based fallback. blanket status-based fallback.
- Found: 2026-07-26, Backend Finalization Sprint documentation pass (traced while - Found: 2026-07-26, Backend Finalization Sprint documentation pass (traced while
writing `docs/AUTHENTICATION.md`/`docs/ERROR_CONTRACT.md`). writing `docs/BACKEND.md` §4 Authentication / §6 Error Model).
## Fixed (this cycle) ## Fixed (this cycle)

View File

@@ -1,398 +0,0 @@
# Maintenance Mode
Frontend contract for backend maintenance/availability signals: global, per-tenant,
per-module, read-only, scheduled, and single-feature-disable scenarios. Written from
the current source tree (branch `B2B`) — see `docs/context/BACKEND-AUDIT.md` for the
full backend surface this builds on.
**Existing frontend handling today: none.** There is no maintenance concept anywhere
in the frontend — no model field, no interceptor branch, no route, no component. This
document proposes a contract and marks every open question explicitly as either
"Requires backend decision" (the backend hasn't decided the signal shape) or "No
frontend UI currently exists for this - requires a future frontend task" (the signal
is plausible but no UI has been built to react to it).
The one adjacent, already-built pattern worth reusing is `AuthErrorPageComponent`
(`src/app/core/auth/pages/auth-error-page.component.ts`): a single component keyed by
an error-code route param, rendering `EmptyStateComponent` +
`ButtonComponent`, with a `Record<Code, {title, description, actionLabel}>` copy table
and a `retry()` handler. Section 7 proposes the maintenance screens follow this exact
shape rather than inventing a new one.
---
## 1. Global maintenance
Whole platform down for all tenants.
**What the backend should send:** `503 Service Unavailable` on every endpoint
(including `GET /bootstrap`), with a `Retry-After` header (seconds) and a structured
JSON body (see §7 for exact shape). `GET /bootstrap` is the critical path — it is the
first call the frontend makes (`ApiBootstrapProvider.loadBootstrap()`,
`src/app/core/bootstrap/providers/api-bootstrap.provider.ts`, `GET /bootstrap`) and
every facade that renders anything (`UiRuntimeFacade`, `WebsiteRuntimeFacade`,
`ProjectEditorFacade`, `ContentManagementFacade`, `DiagnosticsFacade`) depends on it
resolving.
**What the frontend currently does:** nothing maintenance-specific. Tracing the call
chain in `src/app/core/config/config.service.ts`:
```ts
this.bootstrap$ = this.provider.loadBootstrap().pipe(
tap(config => { this.bootstrapSnapshot = config; ... }),
shareReplay(1),
catchError(error => {
this.bootstrap$ = undefined;
this.bootstrapSnapshot = null;
return throwError(() => error);
})
);
```
Any bootstrap failure (503 or otherwise) just rethrows. Every one of the ~14 call
sites of `configService.loadBootstrap()` (footer, theme engine, branding engine,
platform-runtime, page-resolver, static-page-resolver, footer-resolver, diagnostics,
etc. — see `Grep` results for `loadBootstrap()` across `src/app`) either does not
subscribe to the error channel at all, or handles it locally and inconsistently.
There is no global "the whole app is down" screen.
**No frontend UI currently exists for this — requires a future frontend task.** A
clean contract would intercept a `503` on the bootstrap call specifically (distinct
from a 503 on a leaf endpoint, which should degrade that one section instead — see
§3) and route to a full-page takeover, structurally identical to
`AuthErrorPageComponent`: a `maintenance-page.component.ts` using
`EmptyStateComponent` + `ButtonComponent`, keyed off the response body's `reason`
(§7), with a retry button that calls `configService.loadBootstrap(true)`.
**Requires backend decision:** whether maintenance state is signaled by response
status alone (`503` on `/bootstrap`) or also via a dedicated
`GET /status` / `GET /maintenance` probe the frontend could poll while showing the
takeover screen, to auto-recover without the user manually retrying.
---
## 2. Per-tenant maintenance
Single tenant disabled while others operate normally.
This ties directly into tenant resolution: `TenantResolverService`
(`src/app/core/config/tenant-resolver.service.ts`) determines the tenant key before
`ApiConfigService.getBaseUrl()` resolves which base URL to call (`tenantApiBaseUrls`
map, or `tenantApiTemplate` with `{tenant}` substituted — see
`docs/context/BACKEND-AUDIT.md` §2). Because tenant resolution happens client-side
before any network call, a per-tenant maintenance signal can only surface through the
response to that tenant's own `GET /bootstrap` call — there is no separate
"is this tenant up" check today.
**What the backend should send:** the *same* `503` + structured body as global
maintenance (§7) on that tenant's `/bootstrap` response. The frontend has no way to
distinguish "this tenant is down" from "the whole platform is down" except by the
response body's content — so the body must carry enough to tell (e.g. a `scope` field:
`"global" | "tenant"`).
**What the frontend currently does:** nothing. `ConfigService.loadBootstrap()` is
tenant-agnostic from the frontend's point of view — it just calls whatever base URL
`ApiConfigService` resolved and doesn't know if a 503 means "this tenant" vs.
"everything."
**Requires backend decision:** the `scope` discriminator mentioned above, and whether
a disabled tenant's static/marketing content (branding, footer) should still resolve
from a cached/last-known bootstrap so the takeover page can show the tenant's own
logo, or whether it's a fully generic (unbranded) page. Given
`BootstrapConfig.branding`/`theme` are only available *after* a successful bootstrap
load, a tenant-branded maintenance page is not achievable without a design decision
here (e.g. serving branding via a separate lightweight endpoint that stays up even
when the tenant is otherwise disabled).
**No frontend UI currently exists for this — requires a future frontend task.** Same
takeover component as §1 can likely serve both scopes once the backend supplies
`scope`, but nothing renders differently for tenant-vs-global today because nothing
renders a maintenance screen at all yet.
---
## 3. Per-module maintenance
E.g. payments down but catalog still browsable.
**Existing granularity concept:** `BootstrapConfig.featureFlags`
(`FeatureFlagsConfig`, `src/app/shared/models/config/feature-flags.model.ts`) —
a flat `Record<string, boolean>` with known keys `wishlist, compare, reviews,
questions, comments, recommendations, blog, chat, analytics, notifications, coupons,
loyalty, giftCards, invoices` and an index signature for tenant-specific extras. This
is a **static, bootstrap-time** on/off switch per feature — not a live "is this
service currently degraded" signal, and it has no `payments` or `catalog` key today.
It's read once at bootstrap load and doesn't change until the next bootstrap refresh.
There is no separate "module health" concept distinct from `featureFlags`. The admin
dashboard's `healthChecks()` / `homeHealthChecks()` (`AdminDashboardFacade`,
`src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts`) are **not**
module-availability checks — they validate the *local bootstrap document itself*
(schema version present, no missing translations, no invalid colors/widget refs/
layouts, draft-exists, etc.), entirely client-side, with no backend health probe
behind any row except product/category counts (which reflect load success/failure of
`ProductFacade`/`CategoryFacade`, not an explicit "payments module is down" signal).
`AdminMonitoringPageComponent` reuses the same boolean-shaped `healthChecks()` — it is
not a live service-status board either.
**What the backend should send:** each domain-specific endpoint (e.g. `POST /cart`,
`POST /orders`, `{qrApiUrl}/qr`) should independently return `503` with the structured
body (§7) with `scope: "module"` and a `module` field (e.g. `"payments"`) when that
subsystem specifically is down, while unrelated endpoints (`GET /category`,
`GET /items/{id}`) keep responding normally. This requires no new bootstrap field —
it's a per-request response behavior, consistent with REST conventions (the resource
itself is unavailable, not the whole API).
**What the frontend currently does:** nothing differentiates a per-module outage from
any other request failure. `ApiService` (`src/app/services/api.service.ts`) has no
per-endpoint error branching for 503; a failed `createCartPayment()`/`createOrder()`
call surfaces through whatever generic error handling the checkout components already
have for network failures (out of scope for this doc — see the sibling
`ERROR_CONTRACT.md` task for the general error-response shape).
**No frontend UI currently exists for this — requires a future frontend task.** The
checkout flow would need a "payments unavailable" inline state (banner or disabled
submit + tooltip, per §7) distinct from a generic error toast, and catalog browsing
would need to keep working untouched — which it structurally already would, since
`ProductFacade`/`CategoryFacade` and the payment calls are fully independent code
paths today (no shared failure state). That independence is a real asset: a payments
outage cannot accidentally break catalog browsing given the current facade
separation, but no UI exists yet to *tell the user* payments specifically are down
rather than "something went wrong."
---
## 4. Read-only mode
Writes disabled, reads still work.
**Does the frontend already assume this is possible?** Partially, structurally, but
not deliberately. Cart state is `LOCAL-ONLY` (`CartService`,
`src/app/services/cart.service.ts`, signal-based, persisted to `localStorage` key
`marketplace_cart`) — adding items to cart, changing quantities, and browsing the cart
UI works entirely client-side with **no backend call at all** until checkout. The
only writes that hit a backend are at the checkout boundary: `POST /cart`
(`createCartPayment`), `POST /orders` (`createOrder`), `POST /purchase-email`, and the
QR/card payment polling. So today, if the backend rejected writes only, catalog
browsing, search, wishlist/compare (also `LOCAL-ONLY`,
`LocalUserExperienceRepository`), and cart-building would all continue working simply
because they never touch the backend — but reviews (`POST /items/{id}/callback`) and
questions (`POST /items/{id}/questiion`) are also writes and would fail the same as
checkout, since both are LIVE endpoints via `ProductDataProvider`.
There is no code today that *checks for* a read-only flag and proactively disables
write UI (e.g. graying out "Add to cart" or the checkout button ahead of time). A
write attempt would only be discovered to be blocked when the write call itself
fails.
**What the backend should send:** `503` (or `403`, see note below) with the
structured body (§7), `scope: "readonly"`, on write endpoints specifically —
`POST /cart`, `POST /orders`, `POST /purchase-email`, `POST /items/{id}/callback`,
`POST /items/{id}/questiion`, `POST /websession/{sessionId}` (cart sync) — while GET
endpoints keep working. `403 Forbidden` is arguably more correct REST semantics for
"this resource forbids this method during a maintenance window" than `503`, but `503`
+ `Retry-After` communicates "temporary" more clearly to a client and is
recommended so the frontend can offer a countdown/retry consistent with §5's pattern.
**Requires backend decision:** which status code is authoritative — this should be
pinned down jointly with whatever `ERROR_CONTRACT.md` settles on for its 5xx
conventions, since read-only is really "a subset of write endpoints return
maintenance-503."
**No frontend UI currently exists for this — requires a future frontend task.** No
bootstrap flag exists to proactively disable checkout/review/question submission
ahead of a failed request (e.g. `featureFlags.readOnly` or a dedicated
`platformStatus.readOnly` field would need to be added to `BootstrapConfig` if the
product wants a proactive banner instead of a reactive failure). Reactive handling
(showing an error when the write call 503s) can reuse the same inline
error-state pattern as §3/§6 once `ERROR_CONTRACT.md` defines the generic error body
handling.
---
## 5. Scheduled maintenance
Advance notice pattern (banner / countdown) ahead of a maintenance window.
**What exists in the frontend today:** nothing. No banner component, no countdown
component, no bootstrap field for an upcoming maintenance window.
**Requires backend decision — proposed minimal contract:** add an optional field to
`BootstrapConfig` (loaded once per session/on refresh via `GET /bootstrap`), e.g.:
```ts
interface ScheduledMaintenanceNotice {
startsAt: string; // ISO 8601
endsAt?: string; // ISO 8601, optional if duration is unknown
scope: 'global' | 'tenant' | 'module';
module?: string; // present when scope === 'module'
messageKey?: string; // optional i18n key/translated string for custom copy
}
```
surfaced as `bootstrap.maintenanceNotice?: ScheduledMaintenanceNotice | null`. This
keeps the mechanism consistent with how the platform already declares other
runtime-configured, backend-authored state (feature flags, tenant config, API
endpoint records all live in the bootstrap document per
`docs/context/BACKEND-AUDIT.md` §6) rather than inventing a new polling endpoint. A
polling `GET /maintenance-notice` endpoint is an alternative if the notice needs to
appear/change without a full bootstrap refresh — that tradeoff is the backend
decision.
**No frontend UI currently exists for this — requires a future frontend task.** A
dismissible banner component reading `bootstrap.maintenanceNotice` and showing a
localized "maintenance starts in Xh Ym" countdown would need to be built and mounted
at a layout level (header or a global banner slot) — no such banner or countdown
component exists in `src/app/shared/ui/` today.
---
## 6. Temporary feature disable
Single feature toggled off without full maintenance — e.g. reviews temporarily
disabled while the rest of the product page works.
**This is the one scenario the frontend already has a real mechanism for**, via
`BootstrapConfig.featureFlags` (§3). Setting `featureFlags.reviews = false` in the
bootstrap document is exactly the existing, live mechanism for "reviews are off right
now" — it's read by whatever consumes `FeatureConfigService`
(`src/app/core/config/*`) and gates the relevant UI. This is a **deploy/config-time**
toggle (changes on next bootstrap load), not a live incident-response toggle, but
structurally it is the same shape a backend team would use to kill a misbehaving
feature quickly: update the bootstrap document (or whatever backend-side config
drives it), and the next bootstrap fetch picks it up.
**Recommendation:** reuse `featureFlags` for this scenario rather than introducing a
parallel mechanism — it already exists, is already wired through to the UI in the
relevant places, and matches the "temporary, single-feature, not a full outage"
framing exactly. No backend decision needed for the *mechanism*; only for *process*
(how fast a flag flip propagates — depends on bootstrap cache/refresh cadence, which
is outside this doc's scope).
**Gap:** `featureFlags` has no `payments` or `catalog` key and is a boolean only — it
can't express "reviews disabled with reason X, back at time Y" the way §5's proposed
`maintenanceNotice` can. If product wants a "reviews are temporarily unavailable —
back tomorrow" message rather than the feature silently disappearing, that needs the
richer shape from §5, scoped to `module`, not a plain `featureFlags` boolean.
---
## 7. Recommended API responses
All maintenance-scenario responses use HTTP `503 Service Unavailable` (except the
read-only debate in §4) with a `Retry-After` header (seconds, standard HTTP) and a
JSON body. This is written to be consistent with, not contradict, whatever
`ERROR_CONTRACT.md` (sibling task, in progress) settles on for its general
structured-error envelope — if that doc defines a different top-level error
shape (e.g. `{ error: { code, message, ... } }` vs. a flatter shape), this body
should be nested under that envelope rather than duplicating a competing shape.
Pending that reconciliation, the fields below are what the frontend needs regardless
of the outer envelope:
```json
{
"status": 503,
"code": "maintenance",
"scope": "global",
"module": null,
"reason": "scheduled",
"message": "The marketplace is temporarily unavailable for scheduled maintenance.",
"retryAfter": 1800,
"startedAt": "2026-07-26T02:00:00Z",
"expectedEndAt": "2026-07-26T03:00:00Z"
}
```
Field notes:
- `scope`: `"global" | "tenant" | "module" | "readonly"` — lets the frontend pick the
right UI (full takeover vs. inline banner vs. disabled control) without guessing
from status code alone.
- `module`: present only when `scope === "module"` (e.g. `"payments"`, `"reviews"`).
- `reason`: `"scheduled" | "incident" | "disabled"` — free-form enough for the
frontend to choose copy tone (planned vs. unplanned) without needing new fields
per scenario.
- `retryAfter`: mirrors the `Retry-After` header in the body too, so a client that
only reads JSON (not headers) still gets it — useful since some HttpClient error
paths surface the body more readily than headers depending on interceptor
structure.
- `startedAt` / `expectedEndAt`: optional, ISO 8601, for countdown/banner copy (§5).
Per-scenario summary:
| Scenario | Status | `scope` | Notes |
|---|---|---|---|
| Global | 503 | `"global"` | On every endpoint, especially `/bootstrap` |
| Per-tenant | 503 | `"tenant"` | On that tenant's `/bootstrap` and all its endpoints |
| Per-module | 503 | `"module"` | Only on that module's endpoints (e.g. `/cart`, `/orders`) |
| Read-only | 503 or 403 | `"readonly"` | Only on write endpoints; GETs unaffected — pin down with `ERROR_CONTRACT.md` |
| Scheduled (advance notice) | 200, via `bootstrap.maintenanceNotice` | n/a | Not an error response — a proactive field on the normal `/bootstrap` payload, see §5 |
| Temporary feature disable | 200, via `bootstrap.featureFlags.<key> = false` | n/a | Not an error response — existing bootstrap mechanism, see §6 |
---
## 8. Frontend behavior
Grounded in the UI patterns that already exist (`EmptyStateComponent`
(`src/app/shared/ui/empty-state/empty-state.component.ts`), the `errorTitle` /
`error` / `retry` i18n-key convention used across catalog, product details, and
generic list widgets (`src/app/i18n/en.ts`), and `AuthErrorPageComponent`'s
code-keyed full-page pattern). No new UI concepts are invented below beyond composing
these.
| Scenario | Recommended UI | Existing pattern reused | Status |
|---|---|---|---|
| Global maintenance | Full-page takeover, replaces the entire app shell (no header/footer, since branding may be unavailable — see §2) | `AuthErrorPageComponent` shape: `EmptyStateComponent` + `ButtonComponent`, code-keyed copy, `retry()` action | No frontend UI currently exists for this — requires a future frontend task |
| Per-tenant maintenance | Same full-page takeover as global, ideally tenant-branded if the backend decision in §2 allows branding to still resolve | Same as above | No frontend UI currently exists for this — requires a future frontend task |
| Per-module maintenance | Inline empty-state/banner scoped to the affected section only (e.g. checkout step shows `EmptyStateComponent` with `errorTitle`/`error`/`retry` copy; catalog pages untouched) | `EmptyStateComponent` + the `errorTitle`/`error`/`retry` i18n triple already used in `catalog`/`productDetails`/generic-list translations | No frontend UI currently exists for this — requires a future frontend task |
| Read-only mode | Disabled write control (e.g. "Add to cart" / "Submit review" button) + tooltip explaining why, OR a reactive error state on submit if no proactive flag exists (§4) | Disabled-button-plus-tooltip is a common pattern in the design system but not wired to any maintenance signal today | No frontend UI currently exists for this — requires a future frontend task |
| Scheduled maintenance | Dismissible banner at layout/header level with countdown copy | No banner/countdown component exists in `src/app/shared/ui/` today | No frontend UI currently exists for this — requires a future frontend task |
| Temporary feature disable | Feature's own UI simply doesn't render (existing `featureFlags` gating), optionally with a short "temporarily unavailable" note if `messageKey` (§5) is present | Existing `featureFlags` boolean gating (already live) | Existing mechanism works; richer messaging is the only gap |
---
## Summary: what's proposed/new vs. what already exists
**Already exists and can be reused as-is:**
- `BootstrapConfig.featureFlags` — static per-feature kill switch (§3, §6).
- `EmptyStateComponent` + `errorTitle`/`error`/`retry` i18n convention — the inline
error-state building block for any scenario.
- `AuthErrorPageComponent` — the full-page-takeover shape (code-keyed copy record,
`EmptyStateComponent` + `ButtonComponent`, `retry()` handler) to model a maintenance
page after.
- Cart's `LOCAL-ONLY` design already means most of "read-only browsing" works
incidentally, since browsing/cart-building never call the backend.
**Proposed/new (this document introduces):**
- The `scope`/`module`/`reason` structured 503 body (§7).
- `bootstrap.maintenanceNotice` (§5) for scheduled-maintenance advance notice.
- A dedicated `maintenance-page.component.ts` full-page takeover (§1/§2).
- Inline per-module/read-only error and disabled-control states wired to the new 503
shape (§3/§4).
---
## Requires backend decision (full list)
- §1: whether a dedicated `GET /status`/`GET /maintenance` probe should exist for
auto-recovery polling, beyond a plain 503 on `/bootstrap`.
- §2: the `scope` discriminator (`"global"` vs `"tenant"`) so the frontend can tell
the two apart from a single tenant's bootstrap response; and whether a
disabled tenant's branding can still resolve for a branded takeover page.
- §3: none beyond adopting the §7 response shape per-endpoint — this one is mostly
frontend-gap, not backend-undecided.
- §4: which status code is authoritative for read-only (`503` vs `403`) — to be
pinned down jointly with `ERROR_CONTRACT.md`.
- §5: whether scheduled-maintenance notice ships via a `bootstrap.maintenanceNotice`
field (proposed) or a separate polling endpoint.
- §7: how this document's 503 body nests inside whatever outer envelope
`ERROR_CONTRACT.md` defines.
## No frontend UI currently exists for this — requires a future frontend task (full list)
- Global maintenance full-page takeover component.
- Per-tenant maintenance takeover (branded or not, pending §2's backend decision).
- Per-module inline maintenance banner/empty-state wiring on checkout/payment flows.
- Proactive read-only disabling of write controls (Add to cart / Submit review /
Submit question / Checkout) ahead of a failed request.
- Scheduled-maintenance banner + countdown component at the layout/header level.
- Richer "temporarily unavailable, back at X" messaging for `featureFlags`-gated
features (today they just silently don't render — no explanatory copy).

View File

@@ -1,39 +1,23 @@
# Next Phase — Post-Backend-Integration Work # Next Phase — Roadmap
Everything here assumes a real backend implementing `docs/BACKEND_INTEGRATION.md` exists. None of this can start before that. Not a re-statement of the backend checklist itself (`BACKEND_INTEGRATION.md` §9 owns that) — this is what the frontend needs to do once a real API is reachable. The one roadmap. Everything after this point assumes the previous phase is done — don't start Phase 2 work before Phase 1 lands.
## Connect gateways ## Phase 1 — Backend integration
Swap every mock gateway for a real one behind its existing (or newly-added) DI token, per the migration checklist in `BACKEND_INTEGRATION.md` §8. Order matters — follow §8's dependency-ordered sequence (auth/tenant/bootstrap first, then read-heavy catalog domains, then write-heavy customer domains, then admin domains, then builder/CMS). Implement the backend per `BACKEND.md`, then swap every frontend mock gateway for a real one behind its DI token, in the dependency order `BACKEND.md` §8 specifies (auth/tenant/bootstrap first, then read-heavy catalog, then write-heavy customer domains, then admin, then builder/CMS). Wire the currently-dormant Ed25519 admin-auth interceptor/guard once the backend can issue/verify challenges. Enforce the admin role model in route guards once real roles exist server-side.
## Remove mocks ## Phase 2 — Production testing
Once every domain has a real gateway bound and verified, retire the `*LocalGateway`/mock repository implementations (or gate them behind an explicit dev-only flag if they're still useful for offline frontend development). Remove `useMockData`/mock-interceptor paths that are no longer reachable. Add the automated test suite that doesn't exist yet: facade-level integration tests against real endpoints (not mocks), and E2E coverage for the critical flows — storefront checkout, admin product/category CRUD, builder draft → publish → live storefront reflects the change, admin auth once Ed25519 is live.
## Wire dormant auth ## Phase 3 — Performance
Register the Ed25519 `authInterceptor` in `app.config.ts` and attach `ed25519AuthGuard` to admin routes once a real backend can issue/verify challenges — currently fully built but not activated. Fix the `toAuthErrorShape()` gap noted in `KNOWN-ISSUES.md` so `session-expired`/`invalid-signature` screens become reachable once the backend returns a body-level error code. Re-profile under real backend latency (mock responses are instant today, real ones won't be) — loading states, skeleton timing. Revisit the two known large lazy chunks (`project-editor`, `catalog-container`) with real data before committing to a bundle-splitting approach.
## Enforce the role model ## Phase 4 — Monitoring
Wire the existing (currently unenforced) `AdminRole` model into route guards and UI gates — right now anyone who passes admin auth has full access regardless of role. Wire real error tracking/APM and a real event source for the admin Monitoring page (currently mock activity data). Implement the maintenance-mode frontend UI gaps `BACKEND.md` §10 flags as not existing yet (full-page takeover, per-module banners, scheduled-maintenance countdown), once the backend maintenance contract is live.
## Integration testing ## Phase 5 — Version 2 ideas
No automated test suite exists yet for any of the touched areas. Once real endpoints exist, this is the point to add integration tests against them (not against mocks) — facade-level tests verifying real request/response shapes match `BACKEND_INTEGRATION.md`. Everything in `docs/PRODUCT_BACKLOG.md` (dark mode, brand-color contrast decision, advanced analytics, additional payment providers, Contacts page content) and `docs/FUTURE_FEATURES.md` (Angular 22 upgrade, cart-modal composition cleanup) — none of it scheduled, all of it deliberately deferred past initial launch.
## E2E tests
Critical flows worth covering first: storefront checkout (cart → order → payment), admin product/category CRUD, builder draft → publish → live storefront reflects the change, admin auth (once Ed25519 is live).
## Performance profiling
Real backend latency will differ from the instant mock responses today — re-profile loading states, skeleton timing, and the two known large lazy chunks (`project-editor`, `catalog-container`) under real network conditions before committing to a bundle-splitting approach.
## Production monitoring
Wire real error tracking/APM once real endpoints exist — the current admin Monitoring page is mock activity data with no real event source. Decide on the actual monitoring/observability stack as part of backend infra (out of frontend scope, but the frontend's Monitoring UI is ready to display real events once real events exist — see `BACKEND_INTEGRATION.md` §3 Monitoring).
## Maintenance mode
Build the frontend UI gaps explicitly flagged as not existing yet in `docs/MAINTENANCE_MODE.md` (full-page maintenance takeover, per-module inline banners, scheduled-maintenance countdown) once the backend maintenance-mode contract (also in that doc) is decided and implemented.

View File

@@ -28,7 +28,7 @@ Standards referenced below are enforced, not suggestions: `docs/architecture/fou
2. **Gateway interface**`services/admin-dashboard-metrics.gateway.interface.ts`. An abstract contract (`AdminDashboardMetricsGateway`) for "however we get category/product counts" — deliberately decoupled from *how* (local computation vs. real API) so the facade never knows which implementation is active. 2. **Gateway interface**`services/admin-dashboard-metrics.gateway.interface.ts`. An abstract contract (`AdminDashboardMetricsGateway`) for "however we get category/product counts" — deliberately decoupled from *how* (local computation vs. real API) so the facade never knows which implementation is active.
3. **Gateway implementation**`services/admin-dashboard-metrics.local.gateway.ts`. `AdminDashboardMetricsLocalGateway implements AdminDashboardMetricsGateway`, composing `BackofficeDataService.loadCategories()/loadProducts()` (already used elsewhere) into counts. A future `AdminDashboardMetricsApiGateway` would implement the same interface against a real endpoint (`docs/BACKEND_API.md#615-backoffice--dashboard-metrics--recent-activity-planned`) — nothing above this layer changes when that happens. 3. **Gateway implementation**`services/admin-dashboard-metrics.local.gateway.ts`. `AdminDashboardMetricsLocalGateway implements AdminDashboardMetricsGateway`, composing `BackofficeDataService.loadCategories()/loadProducts()` (already used elsewhere) into counts. A future `AdminDashboardMetricsApiGateway` would implement the same interface against a real endpoint (see `docs/BACKEND.md` §3 CRUD Contracts / §8 migration guide) — nothing above this layer changes when that happens.
4. **DI token**`services/admin-dashboard-metrics-gateway.token.ts`. `const ADMIN_DASHBOARD_METRICS_GATEWAY = new InjectionToken<AdminDashboardMetricsGateway>(...)`, bound to the local gateway by default in `app.config.ts`. This is the swap point: rebinding this token to a real API gateway is the *only* change needed to go from mock to real data. 4. **DI token**`services/admin-dashboard-metrics-gateway.token.ts`. `const ADMIN_DASHBOARD_METRICS_GATEWAY = new InjectionToken<AdminDashboardMetricsGateway>(...)`, bound to the local gateway by default in `app.config.ts`. This is the swap point: rebinding this token to a real API gateway is the *only* change needed to go from mock to real data.
@@ -42,7 +42,7 @@ Standards referenced below are enforced, not suggestions: `docs/architecture/fou
9. **Route wiring**`app.routes.ts`. `/:lang/backoffice/dashboard -> AdminDashboardPageComponent`, guarded by `adminAuthGuard`; `/:lang/backoffice` (empty path) redirects to `dashboard`. 9. **Route wiring**`app.routes.ts`. `/:lang/backoffice/dashboard -> AdminDashboardPageComponent`, guarded by `adminAuthGuard`; `/:lang/backoffice` (empty path) redirects to `dashboard`.
Full narrative and known gaps: `docs/ADMIN.md`. Full narrative and known gaps: `docs/archive/ADMIN.md` (historical build log) and `docs/BACKEND.md` (current contract).
## Steps to add a new feature (derived from the example above) ## Steps to add a new feature (derived from the example above)
@@ -56,5 +56,5 @@ Full narrative and known gaps: `docs/ADMIN.md`.
8. Build the container/page component that injects the facade and wires routing. 8. Build the container/page component that injects the facade and wires routing.
9. Add routes in `app.routes.ts`, with `adminAuthGuard` (or the relevant guard) if it's an admin surface. 9. Add routes in `app.routes.ts`, with `adminAuthGuard` (or the relevant guard) if it's an admin surface.
10. Add every new user-facing string to `i18n/translations.ts` (interface) then `en.ts`/`ru.ts`/`hy.ts` — never hardcode copy in a template. 10. Add every new user-facing string to `i18n/translations.ts` (interface) then `en.ts`/`ru.ts`/`hy.ts` — never hardcode copy in a template.
11. Document backend gaps (if any) in `docs/BACKEND_API.md` (§6, endpoints by domain) using the same CURRENT/PLANNED/FUTURE tagging as the existing entries. 11. Document backend gaps (if any) in `docs/BACKEND.md` §3 (CRUD Contracts, endpoints by domain), marking proposed/unimplemented endpoints as such.
12. Run `npm run arch:check` (import boundaries + circular dependencies) and `npx tsc -p tsconfig.app.json --noEmit` before committing. 12. Run `npm run arch:check` (import boundaries + circular dependencies) and `npx tsc -p tsconfig.app.json --noEmit` before committing.

View File

@@ -18,7 +18,7 @@ Every tenant has three surfaces on this one codebase:
- **Rendering**: Bootstrap JSON → Section Engine → Page Renderer → Widget Host → registered widget component (ADR-005). 100% lazy-loaded routes. - **Rendering**: Bootstrap JSON → Section Engine → Page Renderer → Widget Host → registered widget component (ADR-005). 100% lazy-loaded routes.
- **Theming**: CSS custom properties per tenant, 3 theme stylesheets, never hardcoded hex in a component (ADR-008). Design system spec: [`DESIGN.md`](../DESIGN.md) (root of repo). - **Theming**: CSS custom properties per tenant, 3 theme stylesheets, never hardcoded hex in a component (ADR-008). Design system spec: [`DESIGN.md`](../DESIGN.md) (root of repo).
- **i18n**: 3 locales (en/ru/hy), compile-time-enforced key parity across locale files. - **i18n**: 3 locales (en/ru/hy), compile-time-enforced key parity across locale files.
- **Backend**: mostly PLANNED (mock gateways behind swappable provider tokens) — see [BACKEND_INTEGRATION.md](BACKEND_INTEGRATION.md), the single canonical backend spec (endpoints, DTOs, auth, security, error model, uploads, migration guide, checklist). Categories is the one domain fully wired to a real HTTP gateway; everything else is local/mock. - **Backend**: mostly PLANNED (mock gateways behind swappable provider tokens) — see [BACKEND.md](BACKEND.md), the single canonical backend spec (architecture, bootstrap, auth, JWT, Ed25519, permissions, maintenance mode, error model, every endpoint, DTOs, uploads, pagination/filters/sorting, publish workflow, media, builder, implementation checklist). Categories is the one domain fully wired to a real HTTP gateway; everything else is local/mock.
## Doc index (living documents) ## Doc index (living documents)
@@ -27,23 +27,18 @@ Read these directly — they're the current source of truth, not one-off reports
| Doc | What it covers | | Doc | What it covers |
|---|---| |---|---|
| [ARCHITECTURE.md](ARCHITECTURE.md) | Layered architecture, container/facade/service pattern, bootstrap/theme/widget engines, links to the enforced ADRs | | [ARCHITECTURE.md](ARCHITECTURE.md) | Layered architecture, container/facade/service pattern, bootstrap/theme/widget engines, links to the enforced ADRs |
| [BACKEND_INTEGRATION.md](BACKEND_INTEGRATION.md) | **Canonical backend spec** — every endpoint, DTO, CRUD contract, auth, security, error model, uploads, migration guide, checklist | | [BACKEND.md](BACKEND.md) | **The one canonical backend spec** auth, JWT, Ed25519, permissions, maintenance mode, every endpoint, DTOs, uploads, error model, migration guide, checklist |
| [AUTHENTICATION.md](AUTHENTICATION.md) | Standalone auth deep-dive (also inlined in BACKEND_INTEGRATION.md §4) |
| [ERROR_CONTRACT.md](ERROR_CONTRACT.md) | Standalone error-contract deep-dive (also inlined in BACKEND_INTEGRATION.md §6) |
| [MAINTENANCE_MODE.md](MAINTENANCE_MODE.md) | Global/tenant/module maintenance-mode contract |
| [FRONTEND.md](FRONTEND.md) | App structure, routing, i18n, theming, state management, dynamic rendering | | [FRONTEND.md](FRONTEND.md) | App structure, routing, i18n, theming, state management, dynamic rendering |
| [EDITOR.md](EDITOR.md) | The Project Editor: every section, save/publish/draft/reset model | | [EDITOR.md](EDITOR.md) | The Project Editor: every section, save/publish/draft/reset model |
| [StaticPages.md](StaticPages.md) | The Static Pages CMS module (the thing that actually serves About/Contacts/etc. today) | | [StaticPages.md](StaticPages.md) | The Static Pages CMS module (the thing that actually serves About/Contacts/etc. today) |
| [PROJECT-STRUCTURE.md](PROJECT-STRUCTURE.md) | Folder-by-folder tour of `src/app/**` with a worked feature-add example | | [PROJECT-STRUCTURE.md](PROJECT-STRUCTURE.md) | Folder-by-folder tour of `src/app/**` with a worked feature-add example |
| [ADMIN.md](ADMIN.md) | Admin backoffice: routing, architecture, data sources | | [PROJECT_STATUS.md](PROJECT_STATUS.md) | **Current status** — completion %, readiness for demo/production/backend, honest limitations |
| [PROJECT_STATUS.md](PROJECT_STATUS.md) | **Final Release Candidate status** — frontend/backend/docs/auth/builder/storefront/admin readiness, honest limitations | | [NEXT_PHASE.md](NEXT_PHASE.md) | The one roadmap — backend integration → testing → performance → monitoring → v2 ideas |
| [FRONTEND-ROADMAP.md](FRONTEND-ROADMAP.md) | Status snapshot refreshed from recent commits — what shipped, what's open | | [TODO.md](TODO.md) | Release blockers only — currently empty |
| [KNOWN-ISSUES.md](KNOWN-ISSUES.md) | Real, reproducible, currently-open frontend bugs only | | [KNOWN-ISSUES.md](KNOWN-ISSUES.md) | Real, reproducible, currently-open frontend bugs only |
| [PRODUCT_BACKLOG.md](PRODUCT_BACKLOG.md) | Items needing a client/business decision (dark mode, brand colors, page content, etc.) | | [PRODUCT_BACKLOG.md](PRODUCT_BACKLOG.md) | Items needing a client/business decision (dark mode, brand colors, page content, etc.) |
| [FUTURE_FEATURES.md](FUTURE_FEATURES.md) | Nice-to-have, non-blocking future work (Angular 22, bundle splitting, etc.) | | [FUTURE_FEATURES.md](FUTURE_FEATURES.md) | Nice-to-have, non-blocking future work (Angular 22, bundle splitting, etc.) |
| [NEXT_PHASE.md](NEXT_PHASE.md) | What happens after backend integration lands | | [ANGULAR22_PLAN.md](ANGULAR22_PLAN.md) | Angular 22 upgrade feasibility (research only, not yet executed — tracked in FUTURE_FEATURES.md) |
| [TODO.md](TODO.md) | Release blockers only — currently empty |
| [ANGULAR22_PLAN.md](ANGULAR22_PLAN.md) | Angular 22 upgrade feasibility (research only, not yet executed) |
| [SALES-GUIDE.md](SALES-GUIDE.md) | Plain-language guide for the sales team — what to demo, what's not live yet | | [SALES-GUIDE.md](SALES-GUIDE.md) | Plain-language guide for the sales team — what to demo, what's not live yet |
| [`../DESIGN.md`](../DESIGN.md) | Visual design system: colors, typography, elevation, component specs | | [`../DESIGN.md`](../DESIGN.md) | Visual design system: colors, typography, elevation, component specs |
| [`../PRODUCT.md`](../PRODUCT.md) | Product positioning, users, brand personality, anti-references | | [`../PRODUCT.md`](../PRODUCT.md) | Product positioning, users, brand personality, anti-references |
@@ -52,7 +47,7 @@ Read these directly — they're the current source of truth, not one-off reports
| `docs/context/**` | Barry Cache's own source-backed memory system — infrastructure, not project documentation, do not edit by hand | | `docs/context/**` | Barry Cache's own source-backed memory system — infrastructure, not project documentation, do not edit by hand |
| `docs/archive/**` | Superseded docs, kept for history only — do not implement against these | | `docs/archive/**` | Superseded docs, kept for history only — do not implement against these |
**One topic, one place**: routing lives in FRONTEND.md, not repeated here. Backend contract lives in BACKEND_INTEGRATION.md, not repeated in ADMIN.md. Design tokens live in DESIGN.md, not repeated elsewhere. **One topic, one place**: routing lives in FRONTEND.md, not repeated here. Backend contract lives entirely in BACKEND.md nowhere else. Design tokens live in DESIGN.md, not repeated elsewhere.
## What's still open ## What's still open
@@ -60,7 +55,7 @@ Read these directly — they're the current source of truth, not one-off reports
## Historical reports ## Historical reports
19 one-off audit/sprint/review reports were archived, then deleted once every open finding worth keeping was confirmed merged into [KNOWN-ISSUES.md](KNOWN-ISSUES.md)/[FRONTEND-ROADMAP.md](FRONTEND-ROADMAP.md)/[TODO.md](TODO.md). Full original text recoverable via `git log --diff-filter=D -- docs/archive` if needed. 19 one-off audit/sprint/review reports were archived, then deleted once every open finding worth keeping was confirmed merged into [KNOWN-ISSUES.md](KNOWN-ISSUES.md)/[TODO.md](TODO.md). A 20th (`FRONTEND-ROADMAP.md`, despite its name a shipped-history changelog, not a forward roadmap) was archived to `docs/archive/` on 2026-07-26 for the same reason. Full original text recoverable via `git log --diff-filter=D -- docs/archive` or `docs/archive/FRONTEND-ROADMAP.md` itself.
## How to run it ## How to run it
@@ -84,11 +79,11 @@ See root `CLAUDE.md` for the full Barry Cache workflow and memory policy.
## Current status ## Current status
- **Frontend**: ~96% of planned UI built. Storefront/Builder/Backoffice all have working UI; several polish/audit passes complete (see below). Full detail (completion %, per-area readiness, known limitations): [PROJECT_STATUS.md](PROJECT_STATUS.md). Short version:
- **Backend**: in progress — mostly PLANNED/mock gateways, no confirmed live backend contract beyond auth/session and storefront reads. Categories is the one domain fully wired to a real gateway.
- **Storefront polish, performance audit, WCAG 2.1 AA accessibility audit, release-candidate walkthrough**: all done — see [FRONTEND-ROADMAP.md](FRONTEND-ROADMAP.md) for the summary of each.
- **Angular 22 upgrade**: not started, feasibility researched — see [ANGULAR22_PLAN.md](ANGULAR22_PLAN.md) (verdict: safe, ~2-3.5 days, 2 tooling blockers to clear first).
- **Documentation**: consolidated (this pass) — 19 one-off reports archived, 2 files renamed for clarity (`PROJECT.md``PROJECT_INDEX.md`, `backend/BACKEND-INTEGRATION.md``BACKEND_API.md`), 1 duplicate deleted (`RELEASE-NOTES.md` merged into `CHANGELOG.md`).
- **First client demo**: upcoming — blocked on nothing documentation can fix; see [KNOWN-ISSUES.md](KNOWN-ISSUES.md) for what's still open, starting with the dead-routes finding at the top of this document.
Draft/publish for the Project Editor is still **frontend-only** (localStorage), no backend persistence — the single largest backend gap, see [BACKEND_INTEGRATION.md §1 (Bootstrap: Draft vs Published)](BACKEND_INTEGRATION.md#1-bootstrap) and §8 (Real Backend Implementation Guide). - **Frontend**: Release Candidate, feature-complete. `TODO.md` has no blockers.
- **Backend**: not implemented, fully specified. Categories is the one domain wired to a real gateway; everything else is mock. See [BACKEND.md](BACKEND.md).
- **Documentation**: consolidated (Final Documentation Consolidation pass, 2026-07-26) — one canonical backend doc, one roadmap, one status doc, historical/sprint docs moved to `docs/archive/`.
- **First client demo**: ready, with one caveat — admin role enforcement doesn't exist yet, see `PROJECT_STATUS.md`.
Draft/publish for the Project Editor is still **frontend-only** (localStorage), no backend persistence — the single largest backend gap, see [BACKEND.md §1 (Bootstrap: Draft vs Published)](BACKEND.md#1-bootstrap) and §8 (Real Backend Implementation Guide).

View File

@@ -1,52 +1,44 @@
# Project Status — Final Closeout # Project Status
Date: 2026-07-26. Branch: `B2B`. Honest snapshot, verified against source — not aspirational. Date: 2026-07-26. Branch: `B2B`. Honest snapshot, verified against source — not aspirational.
## Frontend status ## Completion estimates
**Release Candidate, complete.** `docs/TODO.md` has no remaining blockers. `npx tsc --noEmit` and `ng build` are clean. Manual smoke testing (home, catalog, cart, dialogs) shows zero console errors. All native browser dialogs replaced with shared components, no known broken-image paths, no raw developer jargon in default admin views, no apology-toned empty states. One real (minor) bug remains open — see `docs/KNOWN-ISSUES.md` (Ed25519 admin-auth error codes `session-expired`/`invalid-signature` are currently unreachable; needs a backend body-error-code contract plus a small frontend fix). Frontend-engineering estimates only (not effort/story-point estimates) — how much of the intended surface is built and working against mock data.
## Backend status | Area | Completion | Basis |
|---|---|---|
| **Frontend (overall)** | **~95%** | `TODO.md` has zero release blockers; one known minor bug open (`KNOWN-ISSUES.md`); several items deliberately deferred as product decisions, not gaps. |
| **Backend** | **~10%** | Only Categories has a real HTTP implementation. Every other domain is a working mock. The *specification* is 100% done (`BACKEND.md`); the *implementation* is not started. |
| **UI (visual/component layer)** | **~95%** | No native browser dialogs, no known broken-image paths, no raw dev jargon in default admin views, no apology-toned empty states, consistent shared primitives across all three surfaces. |
| **Admin (backoffice)** | **~85%** | UI built and working for every domain (dashboard, products, categories, orders, customers, transactions, users, moderation, media, monitoring, analytics) against mock data. Missing: role enforcement (model exists, nothing checks it), real data everywhere except Categories. |
| **Storefront** | **~95%** | Feature-complete for the audited surfaces (home, catalog, product detail, cart, checkout UI, wishlist/compare, search, static/CMS pages). i18n complete (en/ru/hy near-parity). Runs against mock data. |
**Not started. Fully specified.** `docs/BACKEND_INTEGRATION.md` (4,371 lines) is the single canonical spec: every endpoint, DTO, CRUD contract, auth flow, security posture, error model, upload contract, migration guide, and a 34-item top-to-bottom checklist. Only one domain has a real HTTP implementation today — Categories (`AdminCategoriesApiGateway`). Every other admin domain (Products, Orders, Users, Transactions, Monitoring, Moderation) currently injects its mock gateway class directly and needs a DI token added before it's even swappable. Content-management/builder publish has zero backend call today (in-memory + localStorage only) — the single largest gap. ## Ready for first customer?
## Documentation status **Yes, for a demo. No, for production.** The storefront and builder demo end-to-end with no visible rough edges. Production readiness is blocked entirely on the backend not existing yet — see `BACKEND.md`.
Consolidated this closeout. One canonical backend doc (`BACKEND_INTEGRATION.md`) replaces three overlapping ones (archived to `docs/archive/`: `BACKEND_API.md`, `AUTH.md`, `BACKEND_API_REMAINING_WORK.md`). `AUTHENTICATION.md`, `ERROR_CONTRACT.md`, `MAINTENANCE_MODE.md` stand alone as deep-dive references and are also inlined/cross-referenced in the canonical doc. `TODO.md`, `KNOWN-ISSUES.md`, `PRODUCT_BACKLOG.md`, `FUTURE_FEATURES.md` are now cleanly separated by category instead of one mixed checklist. `PROJECT_INDEX.md` (the entry point) updated to reflect all of the above. Not fully swept: some deep architecture ADRs (`docs/architecture/foundation/adr/**`) and a few secondary docs (`FRONTEND.md`, `EDITOR.md`, `ARCHITECTURE.md`, `PROJECT-STRUCTURE.md`, `StaticPages.md`, `ADMIN.md`) still contain old `BACKEND_API.md`/`AUTH.md` references — low-traffic, historical-context docs, not the navigation entry point, left as a known gap rather than touched blindly.
## Authentication status
**Storefront: live.** Telegram/QR session login works end-to-end, is the only way customers authenticate today. **Admin: dormant.** Ed25519 challenge/response admin auth is fully wired client-side (keypair service, signing flow, guard, interceptor) but the interceptor is not registered in `app.config.ts` and the guard is not attached to any route — the flow does not run in production today. No token refresh is implemented for either flow. Full detail: `docs/AUTHENTICATION.md`.
## Builder status
Fully functional as an editor of in-memory/localStorage draft state — homepage sections, widgets, languages, navigation, footer, branding, theme, static pages. **No save/publish ever reaches a backend.** "Publish" today just promotes the local draft signal; nothing is sent over HTTP. This is the single biggest backend gap for going live with real tenant control.
## Storefront status
Feature-complete for the audited surfaces (home, catalog, product detail, cart, checkout UI, wishlist/compare, search, static/CMS pages). Runs entirely against mock data providers. i18n complete across en/ru/hy for customer-facing surfaces (near-parity key counts verified). No native browser dialogs, all dynamic images have a graceful placeholder fallback.
## Admin status
Backoffice UI is built for every domain (dashboard, products, categories, orders, customers, transactions, users, moderation, media, monitoring, analytics) and runs entirely against mock gateways except Categories. Admin route access is gated by `adminAuthGuard`, but that guard performs no role checks today — anyone who passes the (currently Telegram-based) auth gate has full admin access regardless of role; the role model exists in code but isn't enforced yet. Monitoring/Analytics reflect this: Monitoring shows merchant-friendly mock activity; Analytics has no real data source and several values are honestly `null`.
## Known limitations ## Known limitations
- One real frontend bug open (Ed25519 auth error codes unreachable see `KNOWN-ISSUES.md`). - One real frontend bug open: Ed25519 admin-auth error codes `session-expired`/`invalid-signature` are currently unreachable (see `KNOWN-ISSUES.md`).
- Admin role model exists but isn't enforced by any route guard or UI gate yet. - Admin role model exists in code but isn't enforced by any route guard or UI gate — anyone who passes admin auth has full access regardless of assigned role.
- No automated test suite exists for the components touched across recent RC passes (none existed before either). - No automated test suite exists for the components touched across recent RC passes (none existed before either).
- Bundle has two large lazy chunks (project-editor 320 kB, catalog-container 126 kB) — not release-blocking, tracked in `FUTURE_FEATURES.md`. - Two large lazy chunks (`project-editor` 320 kB, `catalog-container` 126 kB) — not release-blocking (`FUTURE_FEATURES.md`).
- 53 local `B2B` commits not yet pushed to `origin` (verified 2026-07-26) — pending explicit go-ahead, a process step not a code blocker. - 53 local `B2B` commits not yet pushed to `origin` (verified 2026-07-26) — pending explicit go-ahead, a process step not a code blocker.
- Several product-decision items (dark mode, brand-color contrast, Contacts page content, advanced analytics) are documented but not scheduled — see `PRODUCT_BACKLOG.md`. - Several product-decision items (dark mode, brand-color contrast, Contacts page content, advanced analytics, additional payment providers) documented but not scheduled — `PRODUCT_BACKLOG.md`.
## Ready for production? ## Backend waiting items
**No.** No real backend exists. The frontend is ready to be wired to one the moment it exists — see `BACKEND_INTEGRATION.md` and `NEXT_PHASE.md`. Everything in `BACKEND.md` §9 (Backend Checklist) — 34 items across 6 phases, from foundation (auth, tenant resolution, bootstrap, error envelope) through hardening (rate limiting, CSP, audit logging, maintenance mode). The single largest gap: the Project Editor (builder) has **no save/publish HTTP call at all** today — drafts live in-memory and in `localStorage` only.
## Ready for backend integration? ## Authentication status
**Yes.** This is the primary deliverable of this closeout. Every endpoint, DTO, auth flow, error contract, and migration step a backend engineer needs is documented in `BACKEND_INTEGRATION.md`, with every frontend-undefined decision explicitly flagged rather than guessed. **Storefront: live.** Telegram/QR session login is the only way customers authenticate today, and it works end-to-end. **Admin: dormant.** Ed25519 challenge/response admin auth is fully built client-side (keypair service, signing flow, guard, interceptor) but the interceptor isn't registered in `app.config.ts` and the guard isn't attached to any route — it doesn't run in production today. No token refresh exists for either flow. Full contract: `BACKEND.md` §4.
## Ready for first client demo? ## Builder status
**Yes, with one caveat.** The storefront and builder can be demoed end-to-end against mock data with no visible rough edges from the RC-02/closeout passes. The caveat: admin/backoffice has no role enforcement, so a demo giving anyone admin access effectively gives them full admin access — fine for a controlled demo, worth stating explicitly if the audience will poke at role-based permission claims. Fully functional editor of in-memory/`localStorage` draft state (homepage sections, widgets, languages, navigation, footer, branding, theme, static pages). "Publish" today only promotes the local draft signal — nothing reaches a backend.
## Documentation status
Consolidated in this closeout pass. One canonical backend doc (`BACKEND.md`, merges everything that used to be five overlapping files). One roadmap (`NEXT_PHASE.md`). One status doc (this file). Historical sprint/audit reports live in `docs/archive/`, not in root `docs/`. `PROJECT_INDEX.md` is the entry point and every remaining doc is reachable from it. Not fully swept: a handful of low-traffic architecture docs (`docs/architecture/foundation/adr/**`, `FRONTEND.md`, `EDITOR.md`, `ARCHITECTURE.md`, `PROJECT-STRUCTURE.md`, `StaticPages.md`) still contain a few old filename references from before this consolidation — historical-context docs, not the navigation entry point, left as a known gap rather than swept blindly.

View File

@@ -1,235 +0,0 @@
# BACKEND READY
Backend APIs are now available.
Read
docs/BACKEND-API.md
Do NOT redesign.
Do NOT change models.
Replace ONLY gateway implementations.
Keep
Facades
Signals
Components
Pages
State
unchanged.
Implement
Authentication
JWT
Refresh
CRUD
Uploads
Errors
Retry
Offline
Maintenance
Feature Flags
Run build.
Run typecheck.
Generate
docs/BACKEND_INTEGRATION.md
Commit.
***************
# RELEASE CANDIDATE
Pretend this application ships tomorrow.
Walk through EVERY route.
Storefront
Builder
Backoffice
Verify
Console
Network
Performance
Loading
Errors
404
Raw i18n
Icons
Images
Animations
Overflow
Responsive
Dialogs
Buttons
Forms
Search
Checkout
Builder
Admin
Fix every P0/P1 issue.
Do not redesign.
Commit every milestone.
Generate
docs/RELEASE_REPORT.md
*****************
# DOCUMENTATION SYNC
Do NOT modify application code.
Update only documentation.
Refresh
Graphify graph
Obsidian
PROJECT_INDEX.md
FRONTEND-ROADMAP.md
KNOWN-ISSUES.md
ADRs if required
Synchronize all project knowledge.
Commit documentation separately.
********************
# CLEANUP
Inspect only for
Dead Components
Dead Services
Dead Routes
Dead CSS
Duplicate Models
Duplicate Interfaces
Unused Imports
Unused Variables
Unused Assets
Unused Icons
Unused SCSS
Unused Directories
Unused Modules
Do NOT touch business logic.
Produce
docs/CLEANUP_REPORT.md
Delete only confirmed dead code.
Commit.
***************
# ANGULAR UPGRADE
Inspect
Angular
TypeScript
RxJS
Builder
Compiler
Dependencies
Determine whether upgrading to Angular 22 is safe.
Do NOT upgrade automatically.
Produce
docs/ANGULAR22_PLAN.md
Include
Benefits
Risks
Breaking changes
Migration steps
Estimated effort
Stop.

View File

@@ -1,6 +1,6 @@
# Static Pages (Project Editor module) # Static Pages (Project Editor module)
Sprint X+2. Full-featured CRUD editor for tenant static content (About, Privacy, Terms, Contacts, custom pages, etc.), living inside the Project Editor at `/edit/static-pages`. Edits `bootstrap.staticPages` directly — the same model the storefront renders from (`docs/BACKEND_API.md#46-staticpages`), no parallel content store. Sprint X+2. Full-featured CRUD editor for tenant static content (About, Privacy, Terms, Contacts, custom pages, etc.), living inside the Project Editor at `/edit/static-pages`. Edits `bootstrap.staticPages` directly — the same model the storefront renders from (`docs/BACKEND.md` §3 CRUD Contracts, CMS), no parallel content store.
`/backoffice/static-pages` (Admin dashboard) redirects here rather than hosting a second CRUD UI over the same data. `/backoffice/static-pages` (Admin dashboard) redirects here rather than hosting a second CRUD UI over the same data.

View File

@@ -1,3 +1,5 @@
> **ARCHIVED 2026-07-26.** Historical sprint log (Sprint 19-28 admin backoffice build-out). Living admin architecture reference now lives in [`docs/BACKEND.md`](../BACKEND.md) (backend contract) and [`docs/ARCHITECTURE.md`](../ARCHITECTURE.md) (frontend architecture). Kept for history only.
# Marketplace Admin Dashboard - Sprint 19 # Marketplace Admin Dashboard - Sprint 19
## Scope ## Scope

View File

@@ -1,3 +1,5 @@
> **ARCHIVED 2026-07-26.** Despite its filename this was a shipped-history changelog, not a forward roadmap — superseded by [`../NEXT_PHASE.md`](../NEXT_PHASE.md) (the one roadmap), [`../CHANGELOG.md`](../../CHANGELOG.md) (shipped history), and [`../PROJECT_STATUS.md`](../PROJECT_STATUS.md)/[`../KNOWN-ISSUES.md`](../KNOWN-ISSUES.md)/[`../PRODUCT_BACKLOG.md`](../PRODUCT_BACKLOG.md)/[`../FUTURE_FEATURES.md`](../FUTURE_FEATURES.md) (open items, now category-split). Kept for history only.
# Frontend Roadmap # Frontend Roadmap
Status snapshot, refreshed from recent commits only. Full sprint history: `SPRINT-PLAN.md` (removed, see git history). Open bugs: `docs/KNOWN-ISSUES.md`. Status snapshot, refreshed from recent commits only. Full sprint history: `SPRINT-PLAN.md` (removed, see git history). Open bugs: `docs/KNOWN-ISSUES.md`.
@@ -32,7 +34,7 @@ As of the 2026-07-26 Final Project Closeout, open items are split by category in
- Real, reproducible frontend bugs: `docs/KNOWN-ISSUES.md` (one open item). - Real, reproducible frontend bugs: `docs/KNOWN-ISSUES.md` (one open item).
- Items needing a client/business decision (dark mode, brand-color contrast, Contacts page content, advanced analytics, payment providers): `docs/PRODUCT_BACKLOG.md`. - Items needing a client/business decision (dark mode, brand-color contrast, Contacts page content, advanced analytics, payment providers): `docs/PRODUCT_BACKLOG.md`.
- Nice-to-have, non-blocking future work (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation): `docs/FUTURE_FEATURES.md`. - Nice-to-have, non-blocking future work (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation): `docs/FUTURE_FEATURES.md`.
- Backend integration: fully specified, not yet implemented — the single canonical spec is `docs/BACKEND_INTEGRATION.md`. - Backend integration: fully specified, not yet implemented — the single canonical spec is `docs/BACKEND.md`.
- Release blockers: `docs/TODO.md` — currently none. - Release blockers: `docs/TODO.md` — currently none.
Overall status: `docs/PROJECT_STATUS.md`. Overall status: `docs/PROJECT_STATUS.md`.