# 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` 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` 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. = 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).