2026-07-14 09:50:03 +04:00
|
|
|
# Marketplace Project Editor - Sprint 13 (updated Sprint 18)
|
2026-07-10 13:43:53 +04:00
|
|
|
|
|
|
|
|
## 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
|
2026-07-13 15:29:21 +04:00
|
|
|
- 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
|
2026-07-10 13:43:53 +04:00
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
2026-07-14 09:50:03 +04:00
|
|
|
## Draft / Publish (Sprint 16, autosave added Sprint 18)
|
2026-07-10 13:43:53 +04:00
|
|
|
|
2026-07-13 15:29:21 +04:00
|
|
|
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.
|
2026-07-14 09:50:03 +04:00
|
|
|
- `save()` snapshots the current in-memory bootstrap as "last saved" and
|
|
|
|
|
timestamps it (`lastSavedAt`).
|
2026-07-13 15:29:21 +04:00
|
|
|
- `publish()` runs `ProjectValidator`, and if there are no issues, applies
|
2026-07-14 09:50:03 +04:00
|
|
|
the bootstrap via `PlatformRuntimeService.reloadFromBootstrap`, marks
|
|
|
|
|
status `published`, and becomes the new `originalBootstrap` snapshot used
|
|
|
|
|
by reset.
|
2026-07-13 15:29:21 +04:00
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
|
2026-07-14 09:50:03 +04:00
|
|
|
### 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.
|
|
|
|
|
|
2026-07-13 15:29:21 +04:00
|
|
|
## 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.
|
2026-07-10 13:43:53 +04:00
|
|
|
|
2026-07-14 10:13:59 +04:00
|
|
|
## QR Login Reuse (Sprint 18, corrected)
|
|
|
|
|
|
|
|
|
|
There is exactly **one** Telegram QR/session backend
|
|
|
|
|
(`{authApiUrl}/users/sessions`) and exactly **one** QR login component/UI.
|
|
|
|
|
Nothing about the QR flow is duplicated for admin:
|
|
|
|
|
|
|
|
|
|
- `TelegramSessionApiService` (`services/telegram-session-api.service.ts`) is
|
|
|
|
|
the single place that calls `POST/GET/DELETE {authApiUrl}/users/sessions...`
|
|
|
|
|
and normalizes the response into `AuthSession`. It holds no state and
|
|
|
|
|
writes no cookies - it's a pure API wrapper.
|
|
|
|
|
- `QrLoginEngine<TSession>` (`shared/qr-login/qr-login.engine.ts`) is the
|
|
|
|
|
QR/polling/expiry/"return from Telegram app" state machine (extracted from
|
|
|
|
|
the original `TelegramLoginComponent`), driven by a small
|
|
|
|
|
`QrLoginAdapter<TSession>` (`shared/qr-login/qr-login.model.ts`).
|
|
|
|
|
- `TelegramLoginComponent` (`components/telegram-login/`) is **the same
|
|
|
|
|
component for both customer and admin login** - not two components. It
|
|
|
|
|
takes a `mode: 'customer' | 'admin'` input; `ngOnInit` picks
|
|
|
|
|
`AuthService` or `AdminAuthService` accordingly and builds the
|
|
|
|
|
`QrLoginAdapter` from whichever one, but the QR image, polling loop,
|
|
|
|
|
timeouts, and dialog markup are identical either way. Customer usage is
|
|
|
|
|
unchanged (`<app-telegram-login />` on the cart page, `mode` defaults to
|
|
|
|
|
`'customer'`); admin usage is `<app-telegram-login mode="admin" />`,
|
|
|
|
|
mounted once globally in `app.html`.
|
|
|
|
|
|
|
|
|
|
An earlier version of this sprint's work built a separate
|
|
|
|
|
`AdminAuthService`/`AdminLoginComponent` pair that called its own
|
|
|
|
|
`adminAuthApiUrl` placeholder endpoint. That was wrong: there is no separate
|
|
|
|
|
admin backend, and inventing one client-side would have meant testing against
|
|
|
|
|
an endpoint that doesn't exist. It was replaced with the shared-API approach
|
|
|
|
|
described above.
|
|
|
|
|
|
|
|
|
|
## Admin Authentication (Sprint 18, corrected)
|
|
|
|
|
|
|
|
|
|
Only the **storage** is separate between customer and admin - the QR/session
|
|
|
|
|
API and UI component are shared (see above), by design, since one Telegram
|
|
|
|
|
QR/session backend serves both. What stays separate is everything needed so
|
|
|
|
|
that scanning the admin QR can never authenticate the customer session (or
|
|
|
|
|
vice versa):
|
2026-07-14 09:50:03 +04:00
|
|
|
|
|
|
|
|
| | Customer (`AuthService`) | Admin (`AdminAuthService`, `core/admin-auth/`) |
|
|
|
|
|
|---|---|---|
|
2026-07-14 10:13:59 +04:00
|
|
|
| Cookie | `webSessionID` (`SameSite=Lax`) | `adminSessionID` (`SameSite=Strict`) |
|
|
|
|
|
| Token storage | `web_session_id` (localStorage, anonymous API attribution only, unrelated to auth) | `adminToken` / `adminRefreshToken` (localStorage, reserved for a future JWT pair - unused today) |
|
|
|
|
|
| Signals | `session`, `status`, `showLoginDialog` on `AuthService` | `session`, `status`, `showLoginDialog` on `AdminAuthService` |
|
2026-07-14 09:50:03 +04:00
|
|
|
| Guard | none yet for customer routes | `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`) |
|
2026-07-14 10:13:59 +04:00
|
|
|
| Interceptor | `apiHeadersInterceptor` | `adminAuthHeadersInterceptor` (`core/admin-auth/admin-auth-headers.interceptor.ts`), self-guards on `/admin/` in the request URL, sets `AdminWebSessionID` + `Authorization: Bearer <adminToken>` when present |
|
|
|
|
|
| Session/QR API | `TelegramSessionApiService` | same `TelegramSessionApiService` instance/endpoint |
|
|
|
|
|
| Login UI | `TelegramLoginComponent` (`mode="customer"`, default) | same `TelegramLoginComponent` (`mode="admin"`) |
|
|
|
|
|
|
|
|
|
|
**Backend gap this creates, and why it matters:** because admin login goes
|
|
|
|
|
through the exact same Telegram session API as customer login, the backend
|
|
|
|
|
has **no concept of "this is an admin session"** at the point the QR is
|
|
|
|
|
scanned - it's just a regular Telegram user session, identical in shape to a
|
|
|
|
|
customer's. The frontend only decides *where to store* the resulting session
|
|
|
|
|
id (admin cookie vs. customer cookie); it cannot and does not decide whether
|
|
|
|
|
that Telegram user is actually allowed to act as an admin. **Real admin
|
|
|
|
|
authorization must be enforced server-side**, at the point admin API calls
|
|
|
|
|
are made with the `AdminWebSessionID` header - the backend must check the
|
|
|
|
|
authenticated user against an admin/role list and reject non-admins, since
|
|
|
|
|
nothing on the frontend prevents any Telegram user from completing the QR
|
|
|
|
|
flow while `mode="admin"` is showing. This needs a backend decision (role
|
|
|
|
|
check keyed off the session id, or a dedicated admin-scoped token issuance)
|
|
|
|
|
before admin login can be considered secure, not just "separate storage."
|
2026-07-14 09:50:03 +04:00
|
|
|
|
|
|
|
|
### 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
|
2026-07-14 10:13:59 +04:00
|
|
|
behavior, so it is safe in all environments. `TelegramLoginComponent` in
|
|
|
|
|
customer mode is currently mounted only on the cart page, so `?login=true`
|
|
|
|
|
only shows a dialog there; the admin-mode instance is mounted globally so
|
|
|
|
|
`?adminLogin=true` works from any route.
|
2026-07-14 09:50:03 +04:00
|
|
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
2026-07-10 13:43:53 +04:00
|
|
|
## Constraints
|
|
|
|
|
|
|
|
|
|
- runtime bootstrap engine not replaced
|
|
|
|
|
- configuration stays source of truth
|
|
|
|
|
- no hardcoded marketplace values
|
|
|
|
|
- no business domain management mixed into editor
|