Files
marketplaces/docs/MAINTENANCE_MODE.md

399 lines
22 KiB
Markdown
Raw Normal View History

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