feat(bootstrap): fall back to built-in placeholder when marketplace unpublished
Adds published: boolean to the bootstrap wire contract. ConfigService swaps to a new DEFAULT_BOOTSTRAP constant (all feature flags on, generic branding/theme/pages) whenever the backend reports published: false, so an unpublished marketplace renders a working demo instead of a blank or broken page. Missing published field stays backward compatible (treated as true). Documents the brand bootstrap wire shape for backend/ops use. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# Frontend default bootstrap (unpublished-marketplace placeholder)
|
||||
|
||||
**Date:** 2026-08-22
|
||||
**Status:** approved (decided by project owner in-session, no further review requested)
|
||||
|
||||
## Problem
|
||||
|
||||
Production `/bootstrap` has no fallback today. A marketplace with no published revision either 404s or returns whatever partial row the backend has — frontend has nothing sane to render. Need a placeholder that shows immediately for any brand before its first publish, with every feature switched on so it doubles as a full product demo.
|
||||
|
||||
## Decision
|
||||
|
||||
Whole-object fallback, decided client-side from one explicit backend signal.
|
||||
|
||||
### 1. Backend contract change
|
||||
|
||||
Add one required top-level field to the `/bootstrap` response:
|
||||
|
||||
```ts
|
||||
interface BootstrapConfig {
|
||||
schemaVersion: string;
|
||||
generatedAt: string;
|
||||
published: boolean; // NEW — false until MarketplaceRevision.status = 'published'
|
||||
tenant: TenantConfig;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`published` mirrors whether the marketplace has a `publishedRevision` (see `MarketplaceRevision.status` in `BACKEND-INTEGRATION.md` §11) — not `lifecycleState` directly, since a marketplace can be `live` while a *new* draft revision sits unpublished. Backend still returns full real `tenant`/`branding`/etc when `published: true`; when `false` it may return anything or the last-known real data — frontend ignores every other field in that case (see §2).
|
||||
|
||||
### 2. Frontend: whole-object swap
|
||||
|
||||
New constant, colocated with the other `DEFAULT_*` config constants:
|
||||
|
||||
```ts
|
||||
// src/app/shared/models/config/default-bootstrap.const.ts
|
||||
export const DEFAULT_BOOTSTRAP: BootstrapConfig = {
|
||||
schemaVersion: '1.0.0',
|
||||
generatedAt: new Date(0).toISOString(),
|
||||
published: false,
|
||||
tenant: { /* generic placeholder — brandName 'Marketplace', no real domain */ },
|
||||
branding: { brandName: 'Marketplace', ... },
|
||||
theme: { /* the existing default-light palette from bootstrap.json */ },
|
||||
featureFlags: { wishlist: true, compare: true, reviews: true, blog: true, chat: true,
|
||||
analytics: true, notifications: true, coupons: true, loyalty: true,
|
||||
giftCards: true, invoices: true }, // everything ON
|
||||
features: DEFAULT_MARKETPLACE_FEATURES_CONFIG, // reused, already all-true
|
||||
header: DEFAULT_HEADER_CONFIG, // reused
|
||||
modules: DEFAULT_PLATFORM_MODULES_CONFIG, // reused (sellerManagement off — real module gate, not a feature flag)
|
||||
navigation: { /* hardcoded generic nav */ },
|
||||
pages: [ /* hardcoded generic home page, hero+categories+featured, same shape as bootstrap.json */ ],
|
||||
staticPages: { /* generic about/privacy/terms/contacts */ },
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
`ConfigService.loadBootstrap()` gains one check after the provider emits:
|
||||
|
||||
```ts
|
||||
tap(config => {
|
||||
const resolved = config.published ? config : DEFAULT_BOOTSTRAP;
|
||||
this.bootstrapSnapshot = resolved;
|
||||
this.revisionState.update(v => v + 1);
|
||||
}),
|
||||
```
|
||||
|
||||
No change to `ApiBootstrapProvider`, `MockBootstrapProvider`, or the `ConfigProvider` interface — the swap is a `ConfigService`-only concern, so it applies uniformly regardless of provider mode.
|
||||
|
||||
### 3. Error handling
|
||||
|
||||
- `published` missing/undefined from an old backend response → treat as `true` (backwards compatible: existing marketplaces that never send the field keep behaving exactly as today, same pattern already used for `modules`/ADR-011).
|
||||
- Actual HTTP failure (network error, 5xx) stays a hard error — `catchError` behavior unchanged, no fallback. Fallback is only for the *known* "not published yet" case, not for "backend unreachable." (Matches your earlier answer: explicit signal, not HTTP-status-driven.)
|
||||
|
||||
### 4. Testing
|
||||
|
||||
- `ConfigService` unit test: `published: false` response → snapshot equals `DEFAULT_BOOTSTRAP`.
|
||||
- `ConfigService` unit test: `published: true` → snapshot equals the real response, untouched.
|
||||
- `ConfigService` unit test: `published` absent → snapshot equals the real response (back-compat).
|
||||
- `DEFAULT_BOOTSTRAP` itself: a schema-shape test (it must satisfy `BootstrapConfig` — TypeScript already enforces this at compile time, so this is really just "does it compile").
|
||||
- One E2E smoke: a marketplace with no revision renders the placeholder home page without erroring.
|
||||
|
||||
## Out of scope (explicitly deferred)
|
||||
|
||||
- Field-level merge (real brand name + placeholder theme) — rejected in favor of simpler whole-object swap.
|
||||
- Any admin-panel UI for previewing/editing the default — not asked for.
|
||||
- Backend implementation of `published` resolution logic — backend team's own call once they build the service; this spec only fixes the wire contract.
|
||||
Reference in New Issue
Block a user