# Marketplace Project Editor - Sprint 13 (updated Sprint 18) ## Scope Sprint 13 introduces first version of Marketplace Project Editor. Editor only manages bootstrap configuration. Out of scope: - products - categories - orders - analytics management ## Architecture ```text src/app/features/project-editor/ pages/ components/ sections/ models/ services/ facade/ ``` Editor uses strongly typed bootstrap models and isolated feature state. Components do not call APIs directly. ## Facade API `ProjectEditorFacade` exposes: - `loadBootstrap()` - `updateBootstrap()` - `exportBootstrap()` - `importBootstrap()` - `preview()` Current source of truth: - existing bootstrap provider via `ConfigService` Future backend integration: - replace local import/export/save flow with builder endpoints - keep component contracts unchanged ## Supported Sections - General - marketplace name - domain - description - default language - supported languages - Branding - logo - small logo - favicon - marketplace title - Theme - palette tokens via color pickers - Header - logo/search/categories/languages/cart/profile/wishlist/compare/region toggles - Footer - company information - address - phone - email - copyright - payment icons - social links - static pages - Homepage - home page sections list - visibility - order - layout - drag-and-drop ordering - Widgets - homepage widget configuration - typed shortcuts for hero/categories/product collection widgets - JSON fallback for other widget props - Marketplace Features - feature flags - search suggestions/history - recently viewed - reviews/questions/recommendations - Preview - export JSON - import JSON - runtime preview without full browser refresh - Languages (Sprint 16) - add/remove supported locale - set default locale - generically syncs translation keys across static pages and navigation labels (`LocaleSyncService`) - Navigation (Sprint 16) - header navigation: add/remove/reorder/edit label/URL/visibility - flat footer navigation: same actions - grouped footer navigation (column-based) is read-only in this tab for now ## Preview Strategy Preview updates bootstrap snapshot in memory and re-applies: - theme - branding - runtime state - route-based page rendering on next Angular navigation This keeps bootstrap engine intact while enabling fast local preview. ## Widget Configuration Current widget editor supports explicit fields for: - Hero - layout - height - overlay - autoplay - Categories - layout - columns - Product Collection - layout - cards per row - filters - badges - rating - price Other widgets use JSON props fallback until dedicated editors are added. ## Draft / Publish (Sprint 16, autosave added Sprint 18) There is still no backend draft/publish API. This sprint models it client-side in `ProjectEditorFacade`: - `status: 'draft' | 'published'` and `dirty` (diffed against the last-saved snapshot) live in facade state. - `save()` snapshots the current in-memory bootstrap as "last saved" and timestamps it (`lastSavedAt`). - `publish()` runs `ProjectValidator`, and if there are no issues, applies the bootstrap via `PlatformRuntimeService.reloadFromBootstrap`, marks status `published`, and becomes the new `originalBootstrap` snapshot used by reset. **Backend gap, not yet implemented:** real persistence needs `PUT /builder/bootstrap/draft` and `POST /builder/bootstrap/publish` endpoints so drafts/publishes survive a reload and are shared across editors. ### Autosave (Sprint 18) `ProjectEditorDraftStorageService` (`services/project-editor-draft-storage.service.ts`) persists the full bootstrap draft to `localStorage` (key `projectEditor.draftBootstrap.v1`, scoped by `tenant.id`) on every `updateBootstrap()` call, `save()`, and `publish()`. On `loadBootstrap()`, if a stored draft exists for the same tenant it is loaded instead of the freshly-fetched bootstrap and `draftRestored` is set true (surfaced in the save bar as a dismissible notice). The published/loaded bootstrap is never overwritten automatically — only explicit `publish()` calls change what the runtime actually serves; the localStorage draft is a separate, purely local concern that survives refreshes and browser restarts. Status indicators in `ProjectEditorSaveBarComponent`: - **Unsaved changes** - shown while `dirty()` is true. - **Last saved: HH:MM:SS** - shown once not dirty and `lastSavedAt` is set. - **Draft restored** banner - shown once after a local draft is loaded from a previous session, dismissible. ### Reset (Sprint 18) - **Reset section** - button above the active section (shown only for sections with a bootstrap-key mapping in `EDITOR_SECTION_BOOTSTRAP_KEYS`, `models/project-editor.model.ts`). Reverts that section's bootstrap keys to `originalBootstrap` (the last loaded/published snapshot). Confirmation required. - **Reset draft** - button in the save bar. Reverts the entire bootstrap to `originalBootstrap` and clears the persisted local draft. Confirmation required. - Per-field reset is **not implemented** - the bootstrap schema has no registry of per-field defaults, so only section- and project-level reset exist. Adding field-level reset would require either a default-value registry per field or storing per-field undo history; deferred. ## Validation `ProjectValidator` (`services/project-validator.service.ts`) runs on every render of the save bar: missing logo, no languages, invalid marketplace URL, duplicate static-page slugs, empty homepage, a homepage widget with no `type`, duplicate header navigation links, invalid theme colors. Publish is blocked while any issue is present; Save is not. ## Rich HTML editing Static page HTML is edited via `MarketplaceHtmlEditorComponent` (`components/html-editor/`), a `contentEditable` + toolbar component with no external dependency. It emits raw HTML on every change and never sanitizes — sanitization remains a storefront-render concern. ## QR Login Reuse (Sprint 18) The Telegram QR-login flow (QR image, polling, expiry, "return from app" recovery via visibilitychange/focus/pageshow) was extracted from `TelegramLoginComponent` into `shared/qr-login/qr-login.engine.ts` (`QrLoginEngine`) plus an adapter interface (`shared/qr-login/qr-login.model.ts`, `QrLoginAdapter`). The engine is not a DI singleton - each login surface instantiates its own `new QrLoginEngine(adapter)` and drives it from an `effect()` watching its own dialog-visibility signal. `TelegramLoginComponent` was refactored onto this engine with no behavior change. `AdminLoginComponent` (`core/admin-auth/admin-login.component.ts`) reuses the same engine against a separate adapter backed by `AdminAuthService`, so QR/polling/timeout logic is not duplicated between customer and admin login. ## Admin Authentication (Sprint 18) Admin authentication is completely separate from the customer/storefront session (`AuthService`), by design - one must never authenticate the other: | | Customer (`AuthService`) | Admin (`AdminAuthService`, `core/admin-auth/`) | |---|---|---| | Cookie | `webSessionID` | `adminSessionID` (`SameSite=Strict`) | | Anonymous/local id | `web_session_id` (localStorage, API attribution only) | `adminToken` / `adminRefreshToken` (localStorage, reserved for future JWT pair) | | Signals | `session`, `status`, `showLoginDialog` on `AuthService` | `session`, `status`, `showLoginDialog`, `role` on `AdminAuthService` | | Guard | none yet for customer routes | `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`) | | Interceptor | `apiHeadersInterceptor` | `adminAuthHeadersInterceptor` (`core/admin-auth/admin-auth-headers.interceptor.ts`), self-guards on `/admin/` in the URL, sets `AdminWebSessionID` + `Authorization: Bearer ` when present | | Login UI | `TelegramLoginComponent` (mounted per-page, e.g. cart) | `AdminLoginComponent` (mounted once, globally, in `app.html`) | **Backend gap:** `environment.adminAuthApiUrl` (`https://users.vitanova.network:456/admin`) is a placeholder path under the existing auth host - there is no real admin session/login backend yet. `AdminAuthService.createWebSession()` / `checkSessionOnce()` / `logout()` call `POST|GET|DELETE {adminAuthApiUrl}/sessions...` following the same shape as the customer session API; confirm/repoint this once the backend ships dedicated admin endpoints. ### Login test mode `?login=true` and `?adminLogin=true` query params (handled once in `App.ngOnInit` via `openLoginDialogsFromTestModeQueryParams()`, `app.ts`) call `AuthService.requestLogin()` / `AdminAuthService.requestLogin()` respectively, for manual testing. This only sets the same signal a normal "please log in" action would set - it does not bypass authentication or change any other behavior, so it is safe in all environments. Note `TelegramLoginComponent` is currently mounted only on the cart page, so `?login=true` only shows a dialog there; `AdminLoginComponent` is mounted globally so `?adminLogin=true` works from any route. ### Ed25519 prep `core/admin-auth/ed25519-verification.model.ts` defines `Ed25519VerificationService` (abstract, injectable) with `requestChallenge()` / `verify(signedResponse)` and the `Ed25519Challenge` / `Ed25519SignedResponse` / `Ed25519VerificationResult` shapes (nonce, timestamp, payload, public key, signature). No crypto is implemented. The current DI binding, `NoopEd25519VerificationService` (registered in `app.config.ts`), fails closed (throws) rather than silently accepting anything, so it's safe to wire into a real login path today - it will error loudly instead of pretending to verify a signature. Swap the DI binding for a real implementation once the backend ships challenge/verify endpoints; nothing else needs to change. ## Known gaps / deferred (Sprint 18) Full field-by-field coverage of every supported bootstrap property (with bilingual EN/RU labels, description, and validation state per field) was not completed in this pass - the bootstrap schema is large (theme typography/ spacing/shadows, full company address, per-locale footer copyright, grouped footer navigation editing, sidebar navigation, per-page SEO map, catalog/ product-page/user-experience sub-fields, permissions, API endpoints) and several concepts named in the sprint brief (payments, delivery/shipping, checkout, unified search config) have **no corresponding model in `shared/models/config` at all** - they would need new bootstrap schema before an editor could expose them. See the section-by-section gap list gathered during Sprint 18 investigation for the full inventory; treat as a follow-up sprint rather than something silently skipped. ## Constraints - runtime bootstrap engine not replaced - configuration stays source of truth - no hardcoded marketplace values - no business domain management mixed into editor