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:
93
README.md
93
README.md
@@ -1,65 +1,78 @@
|
||||
# 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
|
||||
- Category browsing and product detail pages
|
||||
- Search and shopping cart flows
|
||||
- Telegram login integration
|
||||
- Payment handoff through the existing backend contract
|
||||
- Runtime branding and configuration loading
|
||||
- PWA manifest and service worker configuration
|
||||
## Architecture
|
||||
|
||||
`Component (container) → Facade → Domain Service → Repository/Provider (DI token, swappable mock↔API) → Mock | API`
|
||||
|
||||
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/**`.
|
||||
|
||||
## 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
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```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:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Build for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The production build is written to `dist/dexarmarket/`.
|
||||
|
||||
## Project Structure
|
||||
## Folder structure
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/
|
||||
│ ├── components/ # Shared storefront components
|
||||
│ ├── core/ # Runtime, config, guards, providers, interceptors
|
||||
│ ├── dynamic-renderer/ # Page, section, and widget rendering pipeline
|
||||
│ ├── components/ # Shared storefront components (header, footer, product-card, etc.)
|
||||
│ ├── core/ # Auth, admin-auth, config/tenant resolution, DI providers, interceptors
|
||||
│ ├── dynamic-renderer/ # Bootstrap JSON -> section/widget rendering pipeline (live homepage engine)
|
||||
│ ├── 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/*
|
||||
│ ├── guards/ # Route guards (language, admin-auth, dirty-state, etc.)
|
||||
│ ├── i18n/ # Translation service, pipe, and locale packs (en/ru/hy)
|
||||
│ ├── pages/ # Top-level routed pages: home, cart, static-page
|
||||
│ ├── services/ # API, cart, auth, SEO, Telegram, and language services
|
||||
│ └── widgets/ # Dynamic renderer widgets
|
||||
│ ├── 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
|
||||
├── environments/ # Development and production environment settings (incl. useMockData)
|
||||
└── styles/ # Shared global styles and themes
|
||||
```
|
||||
|
||||
## Useful Checks
|
||||
## Documentation map
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run arch:check
|
||||
```
|
||||
Full index: [`docs/PROJECT_INDEX.md`](docs/PROJECT_INDEX.md). Key entry points:
|
||||
|
||||
| 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
|
||||
|
||||
- Authentication and payment integrations are intentionally left on their existing contracts.
|
||||
- Renderer and runtime architecture should remain stable during demo preparation.
|
||||
- Authentication and payment integrations are on their existing contracts — see `docs/BACKEND.md` for the auth/security contract a real backend must satisfy.
|
||||
- Client-facing content should avoid placeholder names, mock labels, and temporary routes.
|
||||
|
||||
@@ -41,7 +41,7 @@ Component (container) --> Facade --> Domain Service --> Repository/Provide
|
||||
|
||||
## 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 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)
|
||||
|
||||
- `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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
@@ -17,6 +17,7 @@ Everything here is derived from the actual current frontend source code (branch
|
||||
7. [Uploads](#7-uploads)
|
||||
8. [Real Backend Implementation Guide](#8-real-backend-implementation-guide)
|
||||
9. [Backend Checklist](#9-backend-checklist)
|
||||
10. [Maintenance Mode](#10-maintenance-mode)
|
||||
|
||||
---
|
||||
## 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)
|
||||
|
||||
Final Project Closeout moved every backend-shaped item out of `docs/TODO.md` into this
|
||||
@@ -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).
|
||||
|
||||
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/
|
||||
@@ -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 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.
|
||||
- **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)
|
||||
|
||||
@@ -88,7 +88,7 @@ No changes to `ProjectEditorIoService` (export/import), `ProjectEditorDraftStora
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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+)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -25,7 +25,7 @@ src/app/
|
||||
- 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`.
|
||||
- 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).
|
||||
|
||||
## i18n system
|
||||
@@ -51,4 +51,4 @@ src/app/
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
unreachable from any real backend response today.
|
||||
- **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
|
||||
blanket status-based fallback.
|
||||
- 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)
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -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`.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
- **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.
|
||||
- **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)
|
||||
|
||||
@@ -27,23 +27,18 @@ Read these directly — they're the current source of truth, not one-off reports
|
||||
| Doc | What it covers |
|
||||
|---|---|
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [BACKEND.md](BACKEND.md) | **The one canonical backend spec** — auth, JWT, Ed25519, permissions, maintenance mode, every endpoint, DTOs, uploads, error model, migration guide, checklist |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [ADMIN.md](ADMIN.md) | Admin backoffice: routing, architecture, data sources |
|
||||
| [PROJECT_STATUS.md](PROJECT_STATUS.md) | **Final Release Candidate status** — frontend/backend/docs/auth/builder/storefront/admin readiness, honest limitations |
|
||||
| [FRONTEND-ROADMAP.md](FRONTEND-ROADMAP.md) | Status snapshot refreshed from recent commits — what shipped, what's open |
|
||||
| [PROJECT_STATUS.md](PROJECT_STATUS.md) | **Current status** — completion %, readiness for demo/production/backend, honest limitations |
|
||||
| [NEXT_PHASE.md](NEXT_PHASE.md) | The one roadmap — backend integration → testing → performance → monitoring → v2 ideas |
|
||||
| [TODO.md](TODO.md) | Release blockers only — currently empty |
|
||||
| [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.) |
|
||||
| [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 |
|
||||
| [TODO.md](TODO.md) | Release blockers only — currently empty |
|
||||
| [ANGULAR22_PLAN.md](ANGULAR22_PLAN.md) | Angular 22 upgrade feasibility (research only, not yet executed) |
|
||||
| [ANGULAR22_PLAN.md](ANGULAR22_PLAN.md) | Angular 22 upgrade feasibility (research only, not yet executed — tracked in FUTURE_FEATURES.md) |
|
||||
| [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 |
|
||||
| [`../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/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
|
||||
|
||||
@@ -60,7 +55,7 @@ Read these directly — they're the current source of truth, not one-off 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
|
||||
|
||||
@@ -84,11 +79,11 @@ See root `CLAUDE.md` for the full Barry Cache workflow and memory policy.
|
||||
|
||||
## Current status
|
||||
|
||||
- **Frontend**: ~96% of planned UI built. Storefront/Builder/Backoffice all have working UI; several polish/audit passes complete (see below).
|
||||
- **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.
|
||||
Full detail (completion %, per-area readiness, known limitations): [PROJECT_STATUS.md](PROJECT_STATUS.md). Short version:
|
||||
|
||||
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).
|
||||
|
||||
@@ -1,52 +1,44 @@
|
||||
# Project Status — Final Closeout
|
||||
# Project Status
|
||||
|
||||
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
|
||||
|
||||
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`.
|
||||
**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`.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- One real frontend bug open (Ed25519 auth error codes unreachable — see `KNOWN-ISSUES.md`).
|
||||
- Admin role model exists but isn't enforced by any route guard or UI gate yet.
|
||||
- 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 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).
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
235
docs/SPRINTS.md
235
docs/SPRINTS.md
@@ -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.
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
## Scope
|
||||
@@ -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
|
||||
|
||||
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).
|
||||
- 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`.
|
||||
- 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.
|
||||
|
||||
Overall status: `docs/PROJECT_STATUS.md`.
|
||||
Reference in New Issue
Block a user