docs: create ERROR_CONTRACT.md and MAINTENANCE_MODE.md
Unified API error envelope + full HTTP status catalogue (401/403/404/ 409/422/429/500/503, maintenance, validation, tenant-disabled, rate-limit, expired-token, invalid-signature) with JSON examples and current frontend reaction behavior, including two flagged pre-existing frontend bugs (expired-token/invalid-signature body-code handling is currently dead code - toAuthErrorShape() ignores fallbackCode for real HTTP errors). Maintenance-mode contract (global/per-tenant/per-module/read-only/ scheduled/feature-disable) with proposed 503 response shapes and an explicit split between "requires backend decision" and "no frontend UI exists yet, requires a future frontend task." These two agents wrote their files before hitting a session usage limit that killed the process before final report-back; content verified complete on disk before committing.
This commit is contained in:
490
docs/ERROR_CONTRACT.md
Normal file
490
docs/ERROR_CONTRACT.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# 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.
|
||||
398
docs/MAINTENANCE_MODE.md
Normal file
398
docs/MAINTENANCE_MODE.md
Normal file
@@ -0,0 +1,398 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user