notifyMe() just called toggleWishlist() - no actual subscription
mechanism existed. Now calls a new subscribeToRestock() API method
(POST /items/{id}/notify-me, not yet built server-side - see
BACKEND-API-REFERENCE.md §12.5) and falls back to a local-only record
in localStorage on failure, so the request isn't silently dropped
while the backend catches up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
JSON-LD was absent entirely (sitemap generation is separate backend
work, out of scope here). Added Product schema (name/description/
image/offers with price+availability) on item pages via setItemMeta(),
and a site-wide Organization schema via resetToDefaults(), both
injected as a single #seo-json-ld <script type=application/ld+json>
tag that gets replaced on navigation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bootstrap.layout.type (the 'Site Layout' selector in the theme editor)
was only ever consulted by a validator checking it against the known
list - nothing used it to actually pick a layout. SectionEngineService
already resolves a per-page layout.type with a hardcoded 'default'
fallback; that fallback now reads the site-wide setting first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getPaymentDescription()'s final fallback ('Покупка на Маркетплейсе')
ignored the active language. Moved to a translated key.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CartService.addItem() fired an async dynamic import + API call for
new items but returned void immediately - buyNow() navigated to /cart
before the item was actually added, landing the user on an empty or
stale cart. addItem() now returns a Promise that resolves once the
cart signal actually contains the item; buyNow() awaits it before
navigating.
Also wired SeoService.setItemMeta()/resetToDefaults() into the product
detail page - built and working, but never called anywhere, so every
product page rendered the site-wide default OG/Twitter tags instead of
per-product ones.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both setItemMeta() and resetToDefaults() hardcoded 'ru_RU'. Added a
LanguageService-driven mapping (ru/en/hy -> ru_RU/en_US/hy_AM).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Known bug per BACKEND-API-REFERENCE.md §5: the client has dedicated
session-expired/invalid-signature recovery screens fully built, but
the mapper only ever derived the error code from HTTP status, never
the response body - so a real 401 with error.code: 'TOKEN_EXPIRED'
rendered the generic 'Unauthorized' screen instead.
Frontend half of the fix: prefer error.code from the body when present
(mapped via authErrorCodeFromBackendCode), fall back to status-derived
code otherwise. Stays dormant until the backend actually sends the
code, per the doc.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
resolveByKey/resolveByRoute subscribed with only a next callback -
a resolver failure left loading=true forever with no error branch to
recover from. Added an error signal, error subscribe handler, and a
distinct error state UI (separate from the existing 404 not-found
state) with a way back home.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A category/product facade error propagated through switchMap
uncaught, erroring the shared widget stream (shareReplay) in
WidgetHostService with no fallback - the widget just silently failed
to render, and the error stayed cached for every later subscriber.
Added catchError falling back to an empty { section, settings } shape,
same pattern as the widget-manifest fetch (falls back to { widgets: [] }
on any error, never throws to the UI).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Products/Categories saveDraft() and deleteOne(), and Users
setRole()/setStatus()/invite(), had no error handler at all - a
failed mutation was completely silent.
Also found and fixed the same premature-navigation bug as the Phase 1
cart fix: both product and category editor pages called
router.navigate() immediately after facade.saveDraft(), before the
save had resolved - so even after adding error feedback, the user
would already be gone from the page before it could show. saveDraft()
now takes an onSuccess callback and only the page navigates on actual
success; on failure it stays put and shows a themed error dialog.
Added a mutationError signal to all three facades and a themed
app-dialog error alert on the products/categories editor + list pages
and the users page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AdminAnalyticsFacade.error() already existed but the reports page
template never checked it - a load failure just rendered the summary
cards with default/zero values, looking like a legitimate empty
report instead of a failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Orders/Products/Categories/Customers/Transactions loadList() caught
errors by silently clearing the list to [] with no error state - an
API failure looked identical to a genuine 'no results' empty state.
Added an error signal to each facade (set on failure, cleared on
retry) and an error branch in each list page/component, distinct from
both loading and the real empty state.
Order and Customer detail (loadDetail) had it worse: no error handler
at all, so a failure just left the page on 'Loading...' forever with
nothing to retry or navigate away with. Added selectedLoading/
selectedError to both facades and an error screen with a back button
to both detail pages.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Order cancel/refund and user suspend used window.confirm(). Migrated
all three to app-confirm-dialog, matching products/categories/orders
delete gates from earlier in this phase.
Remaining window.confirm() usages are the three canDeactivate dirty
guards (categories/products/project-editor) - left as-is, since
CanDeactivate needs a synchronous or Observable/Promise return and
browser navigation guards conventionally use the native dialog there;
converting those is a separate, larger refactor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The bulk button read 'Archive' but called applyBulkDelete() ->
gateway.deleteReview() - a real hard delete, and reviews have no
archived status in the model at all, so 'archive' was never a real
concept here. Relabeled to 'Delete' and added an app-confirm-dialog
gate, same pattern as products/categories/orders.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bulk-delete button called facade.applyBulkDelete() with no gate.
Added app-confirm-dialog, same pattern as products/categories.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(delete) and (bulkDelete) called facade.deleteOne()/applyBulkDelete()
straight from the click event. Added app-confirm-dialog gates for
both, same pattern as categories.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bulk-delete called facade.applyBulkDelete() straight from the button
with zero confirmation. Single-delete had a confirm gate but via raw
window.confirm/alert instead of the themed dialog used elsewhere.
Migrated both to app-confirm-dialog (single + new bulk), and the
delete-blocked message to a themed app-dialog instead of window.alert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The status <select> on the order detail page let an admin jump
straight to 'cancelled'/'refunded' with no confirmation, bypassing the
dedicated cancel()/requestRefund() buttons that do confirm. It also
stayed editable after an order reached a terminal status, so it could
be moved backward out of cancelled/refunded.
- Dropdown options now exclude terminal statuses; reaching them
requires the confirm-gated buttons.
- setStatus() guards against a terminal status slipping through
regardless.
- Once an order is terminal (isTerminal(), already computed but
unused), the dropdown and both action buttons are disabled.
- Same fix applied to the orders list page's bulk status dropdown,
which had the identical gap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
createPayment() and recordOrder() hardcoded currency: 'RUB' regardless
of LanguageService.currentCurrency() (app supports RUB/USD/EUR/AMD).
Widened CartPaymentRequest.currency from a 'RUB' literal to string and
use the actual selected currency in both calls.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
recordOrder() and autoSubmitPurchase() read userEmail()/userPhone()
signals and submitEmail() was fully implemented (validation, error
handling), but the success screen's template never rendered the
inputs - so the form was unreachable and the fallback auto-submit
always sent blank email/phone.
- Added the email/phone form to the payment-success screen, wired to
the existing signals/handlers.
- autoSubmitPurchase() (the 5s fallback if the user doesn't submit
manually) no longer navigates home via an unconditional setTimeout(0)
fired before the submission result is known - it now waits for
submitPurchaseEmail() to settle, same as the manual path, and skips
entirely if the user already submitted (new purchaseSubmitted flag).
- It also now sends whatever the user has typed instead of
hardcoded-blank fields, and shows a toast instead of only logging to
console when no Telegram user id is available.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AdminOrderTimelineEntry had no actor field at all - order status
changes and refund requests were unattributed. Added actor: string,
populated from the signed-in admin's displayName (same pattern as
Users/Transactions), surfaced in the order detail timeline UI.
Real backend-issued orders still need a server-side audit trail;
this only covers the local mock gateway pending backend work
(BACKEND-API-REFERENCE.md §12).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
createOrder() sent a discount-applied price per line item that the
client computed itself, with no server revalidation. Items now only
carry productId/name/quantity - the backend must price from its own
catalog. createPayment()'s amount (required to actually charge the
payment gateway) is unchanged; backend must revalidate it instead,
tracked in BACKEND-API-REFERENCE.md §12.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
adminAuthGuard only checked isAuthenticated() - any signed-in admin
could reach any route. The live Telegram/QR auth (Mechanism A) carries
no role claim, so a real gate needs a backend change (tracked in
BACKEND-API-REFERENCE.md).
Added AdminPermissionsService + requireAdminPermission() guard factory
that derive a permission set locally by matching the Telegram username
against the mock Users domain's roleId - the same local-only stand-in
already used for the rest of that domain. Wired onto /backoffice/users
requiring 'users.manage'. Explicitly cosmetic: backend must
independently authorize every mutation regardless of what this guard
decides.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
audit entries hardcoded actor: 'admin' regardless of who performed the
action. Both local gateways now pull the signed-in admin's displayName
from AdminAuthService, falling back to 'admin' only when unavailable.
Moderation's actor field is a role classifier ('admin' | 'customer'),
not an identity string, and is left unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
isSlugTaken previously caught network/API errors and returned false,
letting the save proceed as if the slug were free. Now the error
propagates and blocks save via a distinct slugCheckError state,
surfaced in the category form with its own hint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Third attempt, done properly this time - first two were reverted
(one stopped cleanly on real conflicts, one botched sequencing and
deleted the old focus-trap before finishing the swap).
DialogComponent gains closeOnEscape/closeOnBackdropClick (default true,
backward-compatible with its 13 other call sites) and ariaLabel (for
dialogs with no visible title header). FOCUSABLE_SELECTOR now includes
iframe for the bank-payment panel's focus trap.
Cart wires closeOnBackdropClick=false on both dialogs (in-flight payment
shouldn't cancel on a stray click) and closeOnEscape tied to the bank
popup's open state, so Escape closes the nested bank iframe first and
falls back to the QR view - matches the original priority exactly.
Original geometry (500px QR modal/40px padding, 960x760 bank modal/
56-16-16 padding, both mobile breakpoints) preserved via :host ::ng-deep
overrides scoped per dialog instance - same pattern already used by
product-carousel-widget.component.ts.
cart.component.ts loses ~90 lines of hand-rolled ViewChild/HostListener/
focus-trap code - app-dialog owns all of it now.
Verified live in browser: dialog sizing/padding/aria-label correct at
mobile+desktop, backdrop-click confirmed inert, Escape-priority confirmed
(bank closes first, then QR), initial focus lands on close button.
83/83 tests pass, tsc/build clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lucide/angular shipped its entire ~1500-icon set in the initial bundle
despite the app only using 85 named-imported icons - confirmed upstream
tree-shaking failure (sideEffects:false, clean named imports, single
non-splittable fesm file). Replaced icon-registry.ts/icon.component.ts
with hand-rolled inline SVG rendering of just the 85 used icons,
transcribed from lucide's own node data for pixel-identical output.
Zero call-site changes - AppIconName and app-icon's public API unchanged.
Also: karma-coverage wired (npm run test:coverage), baseline captured
in docs/SPRINT-PLAN-NEXT.md (32% statements / 18.5% branches).
Cart-modal -> app-dialog migration was attempted and reverted - real
conflicts (backdrop-close, nested-modal escape priority, iframe sizing),
documented in docs/FUTURE_FEATURES.md for a properly scoped follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sprint G: audited every BootstrapConfig field for a real runtime consumer
(docs/DEAD-CONFIG-AUDIT.md). Wired 3 previously-dead editable fields:
footer.logoUrl, company.address.street/contacts.phone, catalog.suggestionsEnabled.
Remaining dead fields needing a business/design decision tracked in
PRODUCT_BACKLOG.md/KNOWN-ISSUES.md, not silently left.
Sprint H: 6 new spec files (test count 57 -> 83), covering ProjectEditorFacade
(undo/redo, draft persistence, publish gating), AdminAnalyticsFacade
(never-fabricate-a-number contract), and regression coverage for this
session's carousel/hero/profile-toggle fixes.
Sprint I: widget settingsSchema (declared in widget-manifest.json, never
validated) now enforced via a new lightweight schema check in
ProjectValidator, surfaced through the existing issuesByField pipeline.
Same check reused in diagnostics so editor and diagnostics can't disagree.
Verification: tsc clean, ng build clean, 83/83 tests pass, barry-cache
validate clean (2 pre-existing unrelated warnings only).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sprint E: homepage section editor now filters the layout-strategy picker
to each widget's widget-manifest.json supportedLayouts instead of always
showing all 5 strategies. columns field gated to widgets that read it
(hero, product-collection carousel).
Sprint F: closes client bug report (no items-per-page control, hero
carousel not manually/automatically scrollable, no 1-2 slide big-carousel
option). Product carousel item width now driven by layout.columns
(reused, was already editable but dead). Hero widget gains prev/next
arrows, touch swipe, and 1-2 panel mode via the same field.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sprint A: storefront header profile control (login/logout only, no menu),
wired to existing customer Telegram auth (AuthService).
Sprint B: backoffice/reports page, reuses AdminAnalyticsFacade (Sales,
Top Products, Marketplace Health cards + CSV export).
Sprint C: backoffice/settings page, admin UI density preference
(comfortable/compact), localStorage-persisted, applied to app-table
across all admin list pages.
Sprint D: admin bottom-nav Help -> mailto using existing supportEmail,
Documentation -> external link via new TenantConfig.documentationUrl.
AdminNavLink gains externalHref for non-routerLink nav entries.
Docs: docs/GLOBAL-SPRINT-PLAN.md tracks the full sprint breakdown.
docs/COMING-SOON-AUDIT.md removed, folded into docs/KNOWN-ISSUES.md.
docs/BACKEND.md updated with the new documentationUrl bootstrap field.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ProjectEditorFacade.resetField(key): field-level revert-to-original, reusing getByPath + a new immutable setByPath (write-side counterpart, scalar/object dot-paths only).
- app-form-field gains showReset/resetLabel/(resetClicked) - a reusable per-field reset affordance, wired on branding logo/title and theme primary/background color.
- Save bar: unsaved-changes count is now clickable, expanding a field-level diff list (reuses facade.changeSummary(), already built for the Preview tab) with jump-to-section links.
- BuilderPropertySearchComponent: filters EditorSchemaService.all() by translated label/hint, jumps to the owning section - no new registry, reuses the existing schema.
- navigation-section: app-empty-state (existing component) added for empty header/footer link lists.
- Reset Section and Draft/Published badge were already implemented; not touched.
- New copy added as translation keys (en/ru/hy).
- PreviewHighlightService + appHighlightSource directive: shared hover/focus bridge between editor fields and the schematic live preview.
- BuilderLivePreviewComponent: in-page schematic homepage render (header/hero/blocks/footer) reading the same bootstrap the sections mutate, highlighting the area matching the active field.
- VisualLayoutPickerComponent: card-based layout picker (ControlValueAccessor, same shape as app-select) replacing the raw layout <select> in homepage-section.
- app-form-field gains optional usedBy/usedByLabel inputs for the "Where is this used?" helper text, wired into aria-describedby.
- Wired homepage/branding/theme sections with highlight sources + usedBy hints; live preview panel shown in project-editor-page for those three sections.
- All new copy added as translation keys (en/ru/hy).
Principal-architect-level review of the entire Seller Management body
of work (7 prior docs + all touched code), verified against fresh
tsc --noEmit and arch:check runs, not recalled from memory.
Real bug found and fixed (in scope per this mission's "unless
absolutely required" carve-out - a one-line correctness fix to
already-committed code, not new feature work):
AdminSellerManagementPageComponent.sellerManagementEnabled read the
bootstrap snapshot once via a plain signal() at construction, not
reactively via bootstrapRevision() the way UiRuntimeFacade/SeoService
both correctly do elsewhere in this codebase. Fixed to computed() keyed
on bootstrapRevision(). Currently invisible (flag is always false,
signal was never even read in the template) but would have gone stale
the moment bootstrap ever reloaded with the flag true. tsc clean after
the fix.
Findings documented in Seller-Management-Final-Design-Review.md (no
Critical/High severity found anywhere):
- Medium: SellerConfig (bootstrap wire shape) and Seller/SellerBranding
(domain entity) are two unreconciled type hierarchies for the same
concept - self-flagged already in BACKEND.md SS11.6, restated here as
an independently-confirmed finding rather than letting it drift.
- Medium: no reusable capability-guard abstraction exists anywhere in
the codebase, despite ADR-009/ADR-011 both prescribing "check the
flag in one place" - ADR-009's own described FeatureFlagService was
never built. Fine with one consumer, a real drift risk the moment a
second one needs the same check.
- Medium: the flag's true branch has never been exercised, even
manually - every verification claim in this whole body of work was
tested at the flag's real value (false).
- Low/nice-to-have: sellerId typed as bare string instead of the UUID
alias used everywhere else in the new sellers domain; MarketplaceRef
vs TenantConfig overlap (deliberate, documented, but worth watching);
documentation-to-code ratio (8 docs, zero backend bytes) carries a
consolidation-burden risk, especially the Unified/Split-Orders
question restated independently in 4 different docs.
- Explicitly checked for and did NOT find: circular dependencies,
scattered tenant/seller conditionals, over-engineering relative to
the typed-models-only mandate, or any auth/payment code touched.
Verdict: not an unqualified "ready for implementation" - two Medium
findings should be resolved by decision/small build before real
backend work starts, not because they block anything today but
because both compound in cost the longer they're left unresolved.
Everything actually built (typed foundation, disabled-by-default flag,
Phase 1 UI, plus the bug this review fixed) is solid and ready to
stay exactly as-is. No Critical or High-severity issue found anywhere.
Reviewed the Phase 1 UI against every other Backoffice page. Found and
fixed 2 real issues; everything else verified already consistent
(built entirely from shared components, so hover/focus/dialog-a11y/
dark-readiness/contrast come from those components, not reinvented).
Fixed:
- Message textarea had no id/aria-describedby wiring (app-input
self-wires this via injected FormFieldContext; the raw textarea -
no dedicated textarea component exists yet - never got it, so the
visible label's `for` pointed nowhere). Added explicit aria-label
bound to the same translation key as the visible label.
- Learn More dialog's feature list would render native browser
bullets (no global list-style reset exists outside details>summary
in styles.scss). Replaced with checkCircle icon + text rows,
consistent with how the rest of the app pairs icons with list/status
meaning.
Added docs/architecture/foundation/Seller-Management-UX-Review.md
documenting both fixes plus everything checked and confirmed already
consistent (empty-state usage, icon reuse, translations completeness
across en/ru/hy, responsive at 1280px/375px, dialog a11y verified via
accessibility tree not assumed).
tsc --noEmit clean, arch:check (boundaries + cycles) clean. Live-
verified: Learn More dialog shows all 6 items each with an icon
(confirmed via DOM query), textarea aria-label confirmed
"Сообщение", no console errors.
Typed models only, per mission. Nothing outside the new files reads
or writes any of this yet.
New core/sellers/models/ (mirrors core/products/models,
core/auth/models convention):
- MarketplaceRef - minimal {id,slug,name} reference from a seller
back to its marketplace, distinct from bootstrap's TenantConfig.
- SellerStatus - 'pending'|'active'|'suspended'|'disabled', no
transition logic.
- SellerScope - {sellerId, marketplaceId}, domain-level counterpart
to BootstrapConfig.seller (SellerConfig from the ADR-011 pass).
- SellerBranding (+SellerContact/SellerAddress/SellerThemeOverrides)
- logo/banner/description/contacts/address/theme overrides, every
field optional. Marketplace branding/theme remain default;
nothing consumes this yet.
- SellerPermissionRole/SellerPermissions - marketplaceOwner/seller/
sellerStaff/platformAdmin. Separate vocabulary from the existing
AdminRole (core/auth/models/permission.model.ts) - not merged, not
wired into any guard, zero auth behavior change.
- Seller - the eventual entity, composed from the above.
Changed (optional-only, verified backward compatible):
- Item (models/item.model.ts) gained sellerId?: string
- AdminProduct (features/admin/products/models/) gained
sellerId?: string
- AdminOrder (features/admin/orders/models/) gained sellerId?: string
Absent means marketplace-owned in every case, exactly like every
existing product/order today. No consumer of any of these three
models needed updating. AdminOrderItem (per-line-item ownership) and
the existing PermissionsConfig/AdminRole system were deliberately not
touched - out of scope for this pass.
Added docs/architecture/foundation/Seller-Management-Domain-Models.md
documenting every new type, every changed field, and the explicit
non-goals list. Linked from the foundation README alongside ADR-011
and the diagrams doc.
tsc --noEmit clean, arch:check (boundaries + cycles) clean.
No backend, no CRUD, no API, no business logic - production-quality
UI only, built entirely from existing shared components (app-dialog,
app-empty-state, app-button, app-form-field, app-input, app-icon,
app-badge). Gated per ADR-011: reads
modules.sellerManagement.enabled from bootstrap (always false today,
no backend sets it) rather than hardcoding disabled state.
New:
- AdminSellerManagementPageComponent (features/admin/seller-management/
pages/) - renders the specified empty state (title/description/
Request Access + Learn More buttons) using existing shared/ui
primitives only, no new UI infrastructure.
- Request Access dialog: Company/Email/Message form via
app-form-field + app-input + a plain textarea (no dedicated
textarea component exists yet, styled to match app-input's own
tokens exactly). Submission is mocked (setTimeout), no API call.
On submit: closes and opens a success dialog ("Thank you...").
- Learn More dialog: 6 capability bullets (seller dashboards,
storefronts, permissions, analytics, product ownership, marketplace
administration) under a "Coming Soon" badge.
- New admin nav group "Partners" > "Seller Management" link
(admin-nav.model.ts), new route /backoffice/partners/seller-management
(app.routes.ts), using the same loadComponent/breadcrumb pattern as
every other admin route.
Translations: full en/ru/hy coverage, zero hardcoded strings - new
adminShell.nav.{partnersGroup,sellerManagement},
adminShell.pages.sellerManagement, and a new adminSellerManagement.*
namespace (emptyState/requestDialog/requestSuccessDialog/
learnMoreDialog) added to translations.ts (types) and all three
locale files.
Accessibility: inherited from app-dialog (role="dialog",
aria-modal, focus trap on Tab/Shift+Tab, Escape to close, focus
restored to trigger on close) - no new a11y code needed, reused as-is.
Responsive: existing --space-*/--font-size-* tokens throughout,
flex-wrap on button row, mobile breakpoint stacks actions full-width.
Verified live (ru locale, devBypassAdmin): nav group/link render
correctly, breadcrumb shows "Управление продавцами", empty state
copy matches spec exactly, Request Access dialog opens with all 3
fields + Cancel/Send Request, filled + submitted -> success dialog
with exact spec copy, Learn More dialog shows all 6 bullets + Coming
Soon badge, no console errors, verified again at 375px mobile
viewport. tsc --noEmit clean, ng build clean (pre-existing bundle-
budget warning only), arch:check (boundaries + cycles) clean.
Architectural foundation only - no UI, no backend, no business logic.
Per ADR-001 (Platform -> Marketplace -> Seller hierarchy) and ADR-009
(feature flags / capability guards): Seller is an optional child scope
beneath a marketplace, not another tenant.
New:
- PlatformModulesConfig / SellerManagementModuleConfig
(shared/models/config/platform-modules.model.ts) - the
modules.sellerManagement.enabled contract, defaults to disabled
(DEFAULT_PLATFORM_MODULES_CONFIG).
- SellerConfig (shared/models/config/seller.model.ts) - typed shape for
the resolved seller scope, mirroring TenantConfig's fields at the
subset a seller needs. Frontend never resolves this itself; it only
reads what the backend already decided (same convention as tenant
resolution, ADR-001).
Changed:
- BootstrapConfig gained two optional fields: modules?, seller?. Both
absent by default - every existing marketplace's bootstrap response
is untouched, TypeScript-checked backward compatible (all new fields
optional, no existing field types changed).
tsc --noEmit clean. No component, facade, service, or route touched -
this commit is pure type contracts.
Ran via ng update @angular/core@22 @angular/cli@22 (schematics applied
automatically). TypeScript bumped 5.9.3 -> 6.0.3 as a required peer.
Migrations applied:
- provideHttpClient() calls gained withXhr() where HttpXhrBackend is used
(app.config.ts)
- optional-chaining expressions wrapped in $safeNavigationMigration()
(language-selector.component.html)
- nullishCoalescingNotNullable/optionalChainNotNullable extended
diagnostics disabled in tsconfig.app.json/tsconfig.spec.json (matches
the new stricter default the migration works around)
Next: ng update @angular/cdk@22, then verify tsc/build/tests.
Phase 3 (Performance). Bundle-stats analysis (esbuild metafile) found
AdminLayoutComponent statically imported and used as component: in
app.routes.ts, the only route in the file not using loadComponent -
pulled the whole backoffice shell into the initial bundle even for
storefront-only visitors, even though every child route under it was
already lazy.
Fixed: component: AdminLayoutComponent -> loadComponent(). Verified
live at /backoffice/dashboard - admin-layout-component now its own
21.86kB lazy chunk, no console errors, dashboard renders correctly.
Initial bundle over-budget shrank from 438.68kB to 417.53kB.
Investigated and deliberately left as-is (not bugs, documented/
legitimate):
- src/app/i18n/ru.ts (272kB) eagerly bundled - explicit, commented
tradeoff in translate.service.ts (ru is platform default language,
avoids extra round-trip for majority of users; en/hy already
code-split). Changing this trades bundle size for default-language
UX regression - a product call, not a cleanup item.
- @lucide/angular (182kB) - verified tree-shaking works correctly
(1750 icons in the package, ~85 actually imported by name in
icon-registry.ts, sideEffects:false). Cost is genuine icon usage,
not dead weight. A further win exists (splitting the icon registry
into storefront-critical vs admin-only sets so backoffice-only
icons don't ride the eager header/footer import chain) but touches
every icon consumer across the app - flagging as a scoped follow-up
rather than attempting blind in this pass.
Remaining bundle-budget warning after this fix: 1.12MB vs 700kB
budget. Given ~350kB is unavoidable Angular framework/router/zone.js
floor, +272kB deliberate ru.ts, +182kB legitimate icon usage, the
700kB budget itself looks stale/unrealistic for this app's actual
floor - flagging for a business decision on raising it rather than
chasing further cuts.
tsc --noEmit clean, ng build clean (warning only, no errors), live
browser-verified.
Ran knip to find unused exports/dependencies (deps already clean, no
unused packages/files found).
Removed genuinely dead code (verified zero references anywhere,
including templates):
- 4 unused constants in config/constants.ts (scroll/pagination/search
thresholds never consumed)
- isAdminRole(), toSearchResult(), createInitialSearchState(),
getTranslatedCategoryName() - unused utility functions
- DEFAULT_EDITOR_HEADER_CONFIG - unused constant
- TelegramService - entire file deleted; cart.component.ts/
cart.service.ts already implement the same window.Telegram.WebApp
access directly, this was an unused duplicate
Real bug fix found during the sweep: SeoService has providedIn:'root'
with a live effect() meant to sync <title>/OG/canonical tags to
tenant bootstrap config, but nothing in the app ever injected it, so
Angular never instantiated it and the effect never ran - the SEO sync
a prior sprint reported as "done and verified" was actually dead on
arrival. Fixed by injecting SeoService in the root App component.
Left alone: ~125 knip-flagged "unused exported types" - overwhelmingly
config/schema interfaces for the widget/theme/admin domain models,
high false-positive rate for this kind of interface-heavy Angular app,
deleting blind risks breaking structural type contracts. Also left
locally-used-but-over-exported helpers (toCssColor/toBackendColor,
HTML_EDITOR_TOOLBAR*, HISTORY_LIMIT, DEFAULT_CATALOG_PAGE_SIZE) - real
code, not dead, just exported wider than needed.
tsc --noEmit and ng build --configuration=production both clean (only
pre-existing bundle-budget warning, unrelated).
Files changed: src/app/app.ts, src/app/config/constants.ts,
src/app/core/auth/models/permission.model.ts,
src/app/core/products/models/catalog-experience.model.ts,
src/app/core/search/models/search-state.model.ts,
src/app/features/project-editor/models/project-editor.model.ts,
src/app/services/index.ts, src/app/utils/item.utils.ts,
src/app/services/telegram.service.ts (deleted)
bootstrap.json's footer.paymentIcons referenced /assets/images/
mir-logo.svg, visa-logo.svg, mastercard-logo.svg - none of that
directory's files existed until the RC-02 placeholder fix, and these
three were still missing. Site-wide broken-image icons in every page
footer. Added neutral labeled-badge SVGs (not reproductions of the
actual trademarked logo artwork) at the exact referenced paths, plus
an onerror fallback on the footer <img> for defense in depth.
Analytics, Reports, and Diagnostics were already clean (no raw
HTTP/queue-worker strings found on audit). Monitoring had three spots
speaking developer language by default:
- Background queue names ("order-notifications") -> friendly labels
("Order notifications").
- Webhook event keys ("order.created") -> friendly labels ("New order
placed").
- Activity log's "api" category showed the raw HTTP line
("GET /api/products responded 200 in 84ms") as the primary message.
Now shows a plain-language summary by default ("Product data
refreshed successfully"), with the raw string moved to a collapsed
"Technical details" <details> per event (api/error/warning rows).
getMainImage() referenced /assets/images/placeholder.svg as the no-image
fallback, but src/assets/images/ never existed - any item with zero
photos rendered a browser broken-image icon instead of a placeholder.
Added the asset.
Also added an (error) handler (onImageError) on every dynamic <img> that
renders a user/admin-supplied URL (product card, cart line item, cart
payment QR code, product gallery main + thumbnails) so a 404'd/broken
image URL swaps to the shared placeholder instead of shipping broken.
pages/category, pages/search, pages/item-detail, pages/info/**,
pages/legal/** (40+ files) were entirely unrouted dead code:
- category/:id, category/:id/items, search all redirect/route to
CatalogContainerComponent
- product/:id routes to ProductDetailsContainerComponent, not item-detail
- cmsContentRoutes (meant to route info/legal) is a literal empty array;
static/legal content is served by the CMS-driven :staticPath ->
StaticPageComponent route instead
dynamic-renderer/ is unrelated and stays - confirmed active, it's the
live homepage rendering pipeline (HomeComponent -> WebsiteRuntimeFacade
-> PageRendererService/PageResolverService -> DynamicPageLayoutComponent).
Updated docs/TODO.md, docs/KNOWN-ISSUES.md, docs/FRONTEND-ROADMAP.md,
docs/PROJECT_INDEX.md to reflect the resolution.
- Route aria-label/alt/title strings (rating, discount, carousel arrows,
hero slides, dialog close, toast dismiss, QR code, bank payment iframe,
guest checkout fallback) through the translate pipe/service instead of
literal English.
- Drop the "Oops!"/"Упс!" apology framing from category/subcategory empty
states (en/ru/hy) - zero results is not an error.
Phase 8 (RC-01): these 4 list/dashboard pages had no error-state handling
on their primary data-load subscriptions — on a gateway error, `loading`
was either never reset (Users, Monitoring, Analytics: genuine infinite-
spinner risk, nested subscribe chain in Analytics never resolved on
failure) or there was no loading/empty/error handling at all (Reports
queue: raw table with zero skeleton or fallback).
- admin-users.facade.ts, admin-monitoring.facade.ts: add `error` signal,
error callback on the primary load subscribe so `loading` always
resolves.
- admin-analytics.facade.ts: add `error` signal; every level of the
4-deep nested gateway subscribe chain (orders -> products ->
categories -> reviews) now has an error handler that resolves loading
instead of leaving it stuck true.
- admin-moderation.facade.ts: add `reportsLoading`/`reportsError` signals
(reports list had none previously).
- Templates: reuse existing `app-skeleton`/`app-empty-state`/`app-button`
primitives for the new error branch, `common.retry` label, two new
generic `common.errorTitle`/`common.errorDescription` i18n keys added
to en/ru/hy (reused across all 4 fixes instead of one-off per-page
copy).
Verified: tsc --noEmit clean, `npm run build` green (pre-existing bundle-
budget warning only, unrelated). Live-checked Home (375px) and Backoffice
Products (1024px) — no console errors, tables/cards render without
overflow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Replaced hardcoded English audit/timeline text (order status changes,
review moderation events, user role/status changes) with proper
adminXxx.timelineEvent.*/adminUsers.audit.* i18n keys, so Recent
Activity/Timeline/Audit panels no longer mix English into ru/hy UI.
- Translated raw internal codes rendered directly to users: transaction
payment method ('card'/'qr'/'cash_on_delivery' -> adminTransactions.methodValue.*)
and user roles/permissions ('products.manage' etc -> adminUsers.roleValue.*/
adminUsers.permission.*), replacing developer-facing enum leakage with
real copy.
- Fixed wrong-noun list-footer counts: Orders/Transactions/Moderation
list pages all reused adminProducts.items ("N товаров"/"N products")
regardless of what was actually listed; each now has its own itemsCount
key ("N заказов", "N транзакций", "N отзывов").
- Fixed customer detail page's "Back" button reusing adminOrders.back
("Back to orders") instead of a customers-specific label.
- Added translation keys to en/ru/hy + translations.ts interface for all
of the above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- KNOWN-ISSUES item 11: the WYSIWYG page content editor was buried inside
a collapsed Advanced <details>, labeled 'Raw HTML (advanced)'. Moved it
to the top of the Content tab, unwrapped, relabeled 'Page Content' /
'Содержимое страницы' / 'Эջի բովանդակություն' with a plain-language
description. Advanced tab keeps genuinely technical fields (id, slug,
route, custom template).
- Publish (save-bar) and Remove language (languages-section) had no
confirmation despite being destructive/high-impact — added
window.confirm guards using the existing builder.confirm* i18n pattern
(matches resetDraft/resetSection/dirty-guard precedent), all 3 locales.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- product-carousel-widget: implicit CSS grid track had no min-width:0,
so the flex scroller's intrinsic content width (fixed 220px product
cards) overflowed the grid item and pushed body width to ~2187px on
a 1440px viewport, squeezing the entire homepage into a ~340px column.
Added min-width:0 to the track and scroller (standard grid/flex
overflow fix).
- bootstrap.json mock fixture: about-us/privacy-policy/terms-of-service
static pages (the real CMS pages served via bootstrap.staticPages,
per docs/PROJECT_INDEX.md) used a 'content' field, but
content-page.service.ts's normalizePage() only reads 'html' -
ContentPageBootstrapInput has no 'content' field. Title rendered,
body was always empty. Renamed the 3 fixture entries' field from
content to html to match the schema; content now renders.
- compare-table: colour row rendered raw hex/name values as plain text
(e.g. '#fCfCfC') with no swatch, inconsistent with variant-selector's
established colour-swatch pattern used on the product page. Added a
small circular swatch (reusing the same border-radius:50% pattern)
next to the value.
Verified live via ng serve: overflow gone (body/viewport width match
at 1440/1280/375), static pages render real content, compare swatch
displays. tsc --noEmit and ng build both clean (pre-existing bundle-
budget warning only, already tracked in KNOWN-ISSUES item 12).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>