(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>
Removes all tracked repo documentation (root status docs, docs/,
docs/architecture/foundation/**, docs/archive/**, docs/context/BACKEND-AUDIT.md
+ adrs, src/assets/mock/README.md) and replaces it with:
- GAPS-AND-IMPROVEMENTS.md — role-based findings (user, PO, QA, backend,
accessibility, engineering) plus automated code-review passes over the
storefront and backoffice, each with file:line references. Findings only,
no fixes applied.
- BACKEND-API-REFERENCE.md — single consolidated backend contract: auth
(both mechanisms), bootstrap, pagination/sorting/filtering conventions,
error model, every live/mock-only endpoint with JSON examples, and the
admin-domain DI-token seam gaps.
Open items and unresolved decisions from the deleted docs (KNOWN-ISSUES,
PRODUCT_BACKLOG, SPRINT-PLAN-NEXT, Seller-Management audits, etc.) were
harvested into the two new files before deletion, not lost.
CLAUDE.md/AGENTS.md/GEMINI.md/.claude/ and docs/context/{INDEX,LOG,
MAINTENANCE,README}.md are untouched — confirmed gitignored, never part of
git history, outside this cleanup's scope.
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>
/search routes to CatalogContainerComponent + CatalogSearchResultsComponent
(app.routes.ts:45-47) - confirmed the live search surface (pages/search/* is
unrouted dead code per KNOWN-ISSUES.md item 13, not touched here).
- search-results.component: hand-rolled `.skeleton-card` shimmer (hardcoded
hex gradient colors, duplicate keyframes) replaced with the shared
app-skeleton primitive the dead pages/search copy already used, but the
live component never got. Bare `<div class="empty-state"><h3>/<p></div>`
replaced with app-empty-state + app-icon, matching CatalogEmptyStateComponent's
established pattern elsewhere in the same feature.
- Added distinct empty-state messaging: a too-short query (<3 chars, mirrors
the existing minSearchLength/isQueryTooShort convention from the dead
pages/search/search.component.ts) now shows "Enter at least N characters"
instead of being indistinguishable from a genuine no-results-for-X state,
which now shows the query and a retry hint (search.noResults/noResultsFor/
noResultsHint/minLength i18n keys already existed, just unused on this path).
- catalog-container.component.html: icon-only close/remove buttons (filter
drawer, sort sheet, grid sheet, saved-search chip) rendered a literal "x"
text character with no app-icon - now use app-icon name="x".
Debounce (220ms, search.facade.ts), URL query-param sync, keyboard
arrow-key suggestion navigation (role=combobox/aria-activedescendant), and
filter/sort discoverability were all verified already correct on this path,
no changes needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Search suggestions/popular-searches (search-autocomplete.service.ts,
search.facade.ts) set icon values like 'inventory_2', 'category', 'sell',
'auto_awesome', 'trending_up' - Material Symbols ligature names rendered as
raw {{ item.icon }} text in search-bar.component.html. No Material Icons
font is loaded anywhere in this Lucide/app-icon-based app, so these
rendered as literal garbled text ("inventory_2", etc.) instead of icons.
- SearchSuggestion.icon retyped from string to AppIconName (search.model.ts)
- Suggestion icon values mapped to registered app-icon names: product->package,
category->folder, brand->tag, ai->zap, popular/trending->trendingUp (new
registry entry, LucideTrendingUp)
- search-bar.component now renders <app-icon [name]="item.icon" /> instead of
the raw ligature string, and its icon-only clear ("x") button now renders
app-icon name="x" instead of a bare literal "x" character glyph
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RC-01 Phase 1 mechanical fixes (verified against current repo state,
not blindly reapplied from TODO.md):
- admin/products create/edit/duplicate now protected by an unsaved-
changes guard (adminProductDirtyGuard), mirroring the existing
categories pattern. AdminProductsFacade had zero dirty-tracking
before this - added a dirty signal, set true on updateDraft(),
cleared on load/create/successful save. Added confirmLeaveUnsaved
to the adminProducts i18n section (en/ru/hy) - categories already
had its own copy of this key, products didn't.
- barry-cache bumped ^0.1.0 -> ^0.9.3 (the pinned range no longer
resolved on the registry - ETARGET - which had been silently
blocking every npm install/uninstall all cycle).
- Removed primeng/primeicons (npm uninstall, now unblocked) - the
only consumer (items-carousel) was already deleted in RC PERF-01.
- Removed core/search/services/search-history.service.ts, a dead
1-line re-export with zero importers (verified: the real
implementation is features/search/services/search-history.service.ts,
used by search.facade.ts). Left core/search/models/* alone - those
ARE live, imported by catalog components.
Verified before touching: HeaderConfig.showProfile toggle is already
removed from the header-section editor template (TODO.md was stale on
this one) - no change needed, will correct the tracking doc separately.
tsc --noEmit clean, npm run build green (bundle unchanged, primeng
was already tree-shaken out, this just removes the dead dependency
declaration itself).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dead code sweep verified manually against app.routes.ts, DI registries, and
cross-repo grep for every candidate (per prior false-positive incident with
knip on pages/**). Deleted only what has zero reachable reference:
Auth (unregistered, comment-only mention):
- core/auth/guards/ed25519-auth.guard.ts - ed25519AuthGuard never imported;
only mentioned inside a doc-comment in admin-login-page.component.ts.
- core/auth/guards/permission.guard.ts - permissionGuard never imported.
- core/auth/interceptors/auth.interceptor.ts - authInterceptor not present
in app.config.ts's withInterceptors([...]) list; not imported elsewhere.
Search feature:
- features/search/services/search-analytics.service.ts - SearchAnalyticsService
never imported outside its own file.
- features/search/components/empty-results/* - app-search-empty-results
selector never used in any template; search-bar.component.html implements
its own inline @if (noResults) empty state instead.
Content management:
- features/content-management/pages/content-management-page.component.ts -
thin wrapper around StaticPagesEditorComponent with zero route pointing at
it in app.routes.ts. The rest of features/content-management/* (facade,
static-pages-editor, page-editor, etc.) remains: it is used by
project-editor and stays.
Backoffice CRUD scaffolding (re-verified the UI-COMPOSITION-REVIEW.md claim
independently): app.routes.ts backoffice section only loads
features/admin/{dashboard,products,categories,transactions,orders,customers,
moderation,users,monitoring,analytics} and features/backoffice/media. Grepped
every other backoffice/* folder for cross-references - none found.
- features/backoffice/{categories,customers,inventory,orders,products,settings}
- each contained only a placeholder .gitkeep from the original scaffold
commit (b957112); no real components were ever added, so this is not the
"duplicate implementation" the prior doc described, just unused scaffold
dirs. Removing corrects that doc's premise.
- features/backoffice/shared/backoffice-coming-soon-page.component.* - only
consumer would have been those scaffold dirs; unreferenced elsewhere.
- assets/mock/backoffice/{customers,orders}/list.json - mock data with no
corresponding fetch call; BackofficeDataProvider only exposes
loadProducts()/loadCategories(), backed by the products/categories mock
files, which are kept.
Dead shared barrels/models (no importer anywhere in src/app):
- shared/index.ts, shared/models/index.ts, shared/types/index.ts - unused
re-export barrels.
- shared/models/domain/index.ts + user-preferences.model.ts (whole domain/
subfolder) - UserPreferences interface has zero consumers.
Storefront pages (pages/public/platform-home.component.ts) - PlatformHomeComponent
has no route in app.routes.ts and is not imported anywhere; distinct from the
pages/category, pages/search, pages/info/**, pages/legal/**, pages/item-detail
components which ARE routed and were correctly left untouched.
Verification: npx tsc --noEmit -p tsconfig.app.json clean after each batch;
npm run build succeeded (pre-existing initial-bundle-budget warning only,
unrelated to this change).
- Admin Categories CRUD (create/edit/delete/reorder) silently failed
end-to-end in local dev: ADMIN_CATEGORIES_GATEWAY resolved
strategy.getBackofficeProviderMode(), which (unlike
getBootstrapProviderMode()) has no localhost fallback, so it always
picked AdminCategoriesApiGateway (real HTTP, 404s here) over the
purpose-built AdminCategoriesLocalGateway mock. saveDraft()'s
subscribe() has no error branch, so a create/publish click gave zero
feedback: the category never saved, dirty stayed true forever, and
the unsaved-changes guard then blocked navigation with no
explanation. Live-verified end-to-end: created 3 categories, edited,
reordered via the keyboard move-up/move-down buttons - all persist
correctly now. Fixed by wiring the token to the category-specific
strategy.getCategoryProviderMode() (was already defined, just never
called) and giving it the same isLocalhost() mock fallback
getBootstrapProviderMode() already uses. Production behavior
(non-localhost) is unchanged - still resolves to the real API
gateway.
- Categories list (tree/table/grid views) mislabeled its Edit button
'Edit product' (adminProducts.edit) instead of 'Edit category' -
copy-pasted the wrong existing i18n key; adminCategories.edit
already exists with the correct translation in en/ru/hy. Not part
of the tracked ~178-key missing-translation gap (docs/KNOWN-ISSUES.md) -
this key exists and is simply wrong, not missing.
Verified live via browser walkthrough of every Backoffice route
(dashboard, products list/create/edit, categories list/create/edit/
reorder, orders list/detail, transactions list/detail+audit dialog,
customers list/detail, moderation list+reports queue, users, monitoring,
analytics, media library) at desktop and mobile widths. Console/network
noise from the mock backoffice API 404ing locally is pre-existing and
already documented (docs/ADMIN.md's prior bug-hunt audit pass) - not
re-reported. Product create/edit CRUD already worked end-to-end
(AdminProductsFacade injects its local gateway unconditionally, no
swappable-provider mistake there).
npx tsc --noEmit and npm run build both green (only the pre-existing
700kB initial-bundle budget warning, already tracked as out of scope).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Legacy no-lang-prefix URLs with a query string (e.g. the dev
?devBypassAdmin=true bypass itself, or any bookmarked/shared deep link
into the Builder) got their query string percent-encoded into the path
instead of preserved (language.guard.ts) - router.createUrlTree([...])
treats a single array element as a literal path segment, so
`/edit/branding?devBypassAdmin=true` became
`/ru/edit/branding%3FdevBypassAdmin%3Dtrue`, a 0-result route. Switched
to router.parseUrl() on the full redirect string so path, query params,
and fragment are parsed and preserved correctly. This guard runs on
every top-level route in the app (not just Builder), so this was
silently breaking any legacy URL with a query string app-wide.
- "Reset draft" (Sbrosit' chernovik) left the save-bar showing "unsaved
changes" immediately after the reset, even though the reset already
discarded everything and cleared the persisted localStorage draft
(project-editor.facade.ts resetDraft()) - it updated the in-memory
bootstrap and cleared the draft but never resynced lastSavedBootstrap,
which the dirty computed diffs against. Now resetDraft() also resets
lastSavedBootstrap to match, so the status bar correctly reads as clean
right after a full discard.
Verified live via browser walkthrough of every Builder section (General,
Branding, Theme, Header, Footer, Homepage, Widgets, Static Pages,
Languages, Features, Navigation, Preview) plus save/publish/undo/redo/
reset-section/reset-draft/draft-restore flows, the media picker dialog,
and the Homepage block / Footer column keyboard-fallback reorder buttons
(WCAG 2.1.1 fallback added in the prior a11y pass) - all functioned
correctly end-to-end, no console errors, no untranslated i18n keys, no
unexpected 4xx/5xx, no layout overflow at desktop or mobile widths.
Investigated and flagged, not fixed (needs a design decision, not a bug
fix): the static page's actual body content editor
(app-marketplace-html-editor, per-locale) is not on the page editor's
"Content" tab at all - it only has title/hero-image/thumbnail fields.
The real WYSIWYG/HTML editor is nested inside a collapsed <details>
disclosure under the "Advanced" tab, labeled "Source HTML (advanced)" as
if it were a raw-HTML power-user fallback, when it is in fact the only
way to edit a static page's body content. Functions correctly once
found/expanded; the placement/labeling just doesn't match the "Content"
tab a merchant would expect it under, and moving it is a navigation
change beyond this pass's fix-what's-broken scope.
npx tsc --noEmit and npm run build both green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Cart item description showed a stray literal "..." when the item had no
description text (cart.component.html) — now only renders the trailing
ellipsis when a description is present.
- Compare table showed raw internal stock enum values ("high"/"low"/etc.)
instead of localized labels (compare-table.component.ts) — now reuses the
same stock-label mapping used by product cards.
- Search with zero results incorrectly showed the empty-category messaging
("browse categories" / "go to parent category") stacked on top of the
search's own "nothing found" message (catalog-container.component.ts) —
isEmptyCategoryState now excludes active search queries so only the
search-appropriate empty state renders.
- Footer "About" link pointed to /about, which 404s; the actual CMS page
route is /about-us (bootstrap.json mock nav data) — corrected the route.
- Added missing public/assets/images/placeholder.svg, the fallback image
referenced by getMainImage() for items without photos (previously 404s
if that fallback path is ever hit).
Investigated and left as-is (not code bugs): /images/*.webp 404s on
product cards are references to a real backend/CDN not present in local
dev (confirmed via mock-data.interceptor.ts and api.service.ts image-URL
resolution) — expected dev-only gap. Footer "Contacts" link (/contacts)
has no corresponding static page content at all in mock data; flagging
for a content decision rather than fabricating copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RC A11Y-01 audit pass, Backoffice (admin/*) area. Builds on 712a7b4/63c9cee's
composition pass (scope="col", skeletons, empty-states) without redoing that
work.
- Sidebar nav landmark: admin-layout's <nav aria-label> reused the
"Dashboard" nav-item translation key as its landmark label, misleadingly
announcing the whole sidebar as "Dashboard" - added a dedicated
adminShell.sidebarLabel key ("Admin sidebar navigation") in en/ru/hy.
Skip link, #admin-content main landmark, Escape handling, and mobile-drawer
focus management were already correct - verified, not touched.
- Categories tree drag-and-drop keyboard fallback (WCAG 2.1.1): the category
tree's native HTML5 DnD (draggable/dragstart/drop) reorders siblings with
no keyboard equivalent - existing arrow-key tree navigation only expands/
collapses/selects, never reorders. Added per-row move-up/move-down icon
buttons (disabled at sibling boundaries), reusing the existing `reorder`
output so the facade's reorder logic is untouched; new
adminCategories.moveUp/moveDown keys in en/ru/hy.
- Screen-reader loading announcements: skeleton-row loading states across
Products, Categories, Customers, Orders, Transactions, Users, Reviews,
Monitoring (webhooks/events), and Analytics (summary cards + top products)
were purely visual (app-skeleton is aria-hidden by design) with no
accessible "loading" text, unlike the storefront/product-details pattern -
added role="status"/aria-live="polite"/aria-busy + sr-only text using the
existing common.loading key.
- Table row headers: added scope="row" to the primary identifying cell
(product/category/customer name, order number, transaction order number,
user name, review customer, report target, top-products/low-stock product
name, webhook endpoint) on 9 tables that only had scope="col". Added
matching `tbody th[scope='row'] { font-weight/color/text-align/
vertical-align }` + last-row border resets in each component's own scss so
the semantic change doesn't alter visuals (the shared app-table stylesheet
styles all <th> as bold/muted by default).
Verified via `git show --stat` of fb1afb7/a03260e and `docs/UI-COMPOSITION-
REVIEW.md`'s Backoffice sections first, per instructions - confirmed
scope="col" coverage already complete, all admin modals already route
through the shared app-dialog (focus-trap/Escape/return-focus already
correct, nothing to fix), and the bare-<select> filters still carry
aria-label per the accepted Sprint 28 decision (not re-migrated to
app-select).
Flagged, not fixed:
- No toast/notification system exists anywhere in this codebase (product/
category save and delete call the gateway with no success/error UI at
all, not even a subscribe error handler) - there is nothing to wire
aria-live onto without adding a new UI mechanism, which is out of scope
for an a11y-only pass. A prerequisite feature-level fix, not an a11y
regression.
- Dashboard's per-card metric/status-row/timeline skeletons (dashboard-
metric, dashboard-status-row, dashboard-timeline) were left without
aria-live wiring - wrapping each of the ~10 simultaneous mini-widgets in
its own live region would fire a burst of redundant announcements; needs
a single page-level "loading dashboard" region instead, a larger change
than this surgical pass.
- Monitoring's events table and the notifications dropdown (role="menu"
with a static empty-state message, aria-haspopup="true") were left as-is -
matches the same partial-widget-pattern precedent already accepted for
locale-tabs/product-tabs in the storefront and builder passes.
- Analytics `lowStockProducts` table's missing loading-skeleton branch
(already flagged, not fixed, in the RC-Visual-02 pass) - untouched again
here for the same reason.
Verified: npx tsc --noEmit clean; npm run build green (only the pre-existing
768.57 kB vs 700 kB initial-bundle budget warning, unrelated to this pass).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RC A11Y-01 audit pass, Builder (project-editor) + content-management static
pages editor. Builds on 4ebc15f's composition pass without redoing it.
- Skip link: Builder's /edit and /edit/:section routes render outside the
storefront app-shell (isAdminRoute() branch has no skip link/landmark, only
a bare router-outlet, unlike the storefront's app.html) - added a
"skip to content" link targeting a new #builder-main-content landmark on
project-editor-page.component, reusing the existing adminShell.skipToContent
key and global .skip-link style. Nav landmark already had aria-label
(builder.appName) from the prior pass, verified correct.
- Save bar: added role="status"/aria-live="polite" to the save/publish status
block and role="status" to the draft-restored notice so save/publish state
changes and draft recovery are announced to screen readers (previously
silent DOM updates).
- Media picker (shared, used by both Builder and static-pages editor): upload
error message had no aria-live wiring - added role="alert".
- HTML editor (marketplace-html-editor): the contenteditable rich-text surface
had no accessible role/name - added role="textbox", aria-multiline="true",
aria-label.
- Drag-and-drop keyboard fallback (WCAG 2.1.1): homepage-section's block list
and footer-section's column list + per-column link list use Angular CDK
drag-drop (cdkDrag/cdkDropList), which has no built-in keyboard reordering.
Added move-up/move-down icon buttons (disabled at the first/last boundary),
matching the existing pattern already used by widgets-section and
navigation-section.
- Color picker: the swatch <input type="color"> had no accessible name (only
the paired text input was labelled via app-form-field) - added explicit
ariaLabel bindings to all 8 color-picker instances in theme-section.
- Languages: the new-locale code input relied on a placeholder ("de") as its
only accessible name - added ariaLabel + new builder.newLanguageLabel i18n
key (en/ru/hy).
- Undefined CSS var --color-primary (never defined anywhere, silently used its
hardcoded hex fallback and never responded to tenant theming - same
recurring bug class as 4ebc15f) - remapped to the real --primary-color token
in marketplace-html-editor, homepage-section, and section.shared (7 usages).
- role="alert" added to all 11 validation-error <p class="editor-error">
occurrences across footer/homepage/languages/navigation/preview/widgets
sections and the static-pages editor, so field/section validation messages
are announced.
- scope="col" added to preview-section's change-summary table headers.
Flagged, not fixed (design-system decisions, matching the storefront pass's
precedent):
- Save bar's --warning-color/--error-color/--info-color text fail WCAG AA
4.5:1 in some themes - same genuine brand semantic colors flagged (not
fixed) in fb1afb7's storefront pass; needs a deliberate token decision,
not a Builder-specific issue.
- locale-tabs (app-locale-tabs, shared) has role="tablist"/"tab" and
aria-selected but no roving-tabindex/arrow-key navigation - matches the
same partial-tablist pattern already accepted for product-tabs in the
storefront pass; all tabs remain natively Tab-focusable, so this meets
4.1.2/2.1.1 without the full ARIA authoring-practice pattern.
Verified: npx tsc --noEmit clean; npm run build green (only the pre-existing
bundle-budget warning, unrelated to this pass).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RC A11Y-01 audit pass, storefront + shared app-shell chrome only. Builds
on RC-Visual-02/RC-Premium-01/RC STORE-01 without redoing that work.
- Skip link: added first-focusable "skip to main content" link (app.html,
styles.scss .skip-link/.sr-only), targeting new #main-content landmark.
New app.skipToContent i18n key in en/ru/hy.
- Header: mobile menu items stayed keyboard-focusable and screen-reader
reachable while visually collapsed (max-height:0 with no visibility
toggle) - fixed with visibility:hidden + matched transition-delay.
Desktop search input (readonly, click-to-navigate) had no keyboard
activation - added aria-label + (keydown.enter).
- Cart payment/bank-payment modals: custom (non-app-dialog) UI had no
focus trap, no Escape handling, and never returned focus to the
triggering element - ported app-dialog's confirmed-correct
focus-trap/Escape/return-focus pattern directly onto cart.component.ts.
Added role="dialog"/aria-modal/aria-label to both panels and
role="status"|"alert"/aria-live to every payment-status screen so
screen readers announce state changes (creating/waiting/success/
error/timeout).
- Search combobox: suggestion listbox had no role="combobox" wiring on
the input and suggestion buttons weren't role="option" - added
aria-autocomplete, aria-controls, aria-activedescendant, aria-selected
so the existing arrow-key navigation is announced to screen readers.
- Product tabs: tablist/tab pattern was incomplete (no role="tablist",
no tabpanel) - added role="tablist" + ids to product-tabs.component,
role="tabpanel"/aria-labelledby to the content panel in
product-details-container.
- Review form: rating/text validation errors weren't associated with
their controls (no aria-describedby, no role="alert") - fixed; added
aria-required to the review textarea.
- delivery-selector: added aria-required to the delivery <select> when
a selection is mandatory.
- Shared app-icon component: doc comment claimed "decorative by default
(aria-hidden)" but no aria-hidden was ever applied - fixed to actually
set aria-hidden="true" when undecorated, and role="img"/aria-label
when ariaLabel is passed. Shared component, affects every icon-only
usage app-wide, no visual change.
- Color contrast: --text-light fails WCAG AA 4.5:1 for normal text in
every theme (dexar 3.39:1, lavero/novo 2.54:1 against white). The two
in-scope usages (company-details org-short/basis, review-form
upload-placeholder) switched to --text-secondary (4.55:1-7.56:1,
passes), same visual family, no layout change.
Flagged, not fixed (design-system decisions, not polish):
- --border-color fails WCAG 1.4.11 3:1 for UI-component boundaries in
every theme (dexar 1.42:1, lavero/novo 1.24:1 vs white) - pervasive
token used by hundreds of borders app-wide; needs theme-owner sign-off.
- --success-color/--warning-color/--error-color/--info-color used as
plain text-on-white in several places (product-information,
question-card, review-form, compare-page) fail 4.5:1 (2.15-3.76:1) -
genuine brand semantic colors, changing them to pass would visibly
shift the palette; needs a deliberate token decision.
- Header mobile-menu max-height/padding transition (pre-existing,
unrelated to this fix) flagged by design lint as layout-thrashing;
left as-is per the "no layout/business-logic changes" constraint.
Verified: npx tsc --noEmit clean; npm run build green (only the
pre-existing bundle-budget warning, unrelated to this pass).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RC PERF-01 asset audit (images/fonts/SVG/CSS), follow-up to 61a5714/4bf0666.
Removed from src/styles.scss - zero references (literal and hyphen-safe)
in any .html/.ts across the repo, and no dynamic class-string construction
found for the utility classes:
- .btn-primary / .btn-secondary (+ :hover) - unused button variants
- .catalog-product-card - unused selector in a shared comma group with
.product-card/.item-card, which stay
- .item-badges-overlay, .item-simple-desc - unused component helpers
- .text-center, .mt-1..4, .mb-1..4, .p-1..4 - fully unused spacing utilities
styles-*.css: 9.60 kB -> 8.41 kB raw (2.22 kB -> 1.99 kB transfer), -12%.
No other category needed a code change:
- Images: <img> tags already have loading="lazy"/decoding="async" on
storefront grids/galleries (product-card, catalog, category-grid,
cart, item-detail) from prior polish passes; CLS is already handled
via CSS aspect-ratio on those containers rather than width/height
attrs, so none were added. Payment-logo <img>s already have explicit
width/height. Flagged, not fixed: a handful of single-image admin/
editor previews (page-editor, brand-overview, asset-details-drawer)
lack loading="lazy" - low traffic, negligible impact, left alone to
avoid unnecessary diff.
- Fonts: index.html preconnects to fonts.gstatic.com/googleapis.com and
loads DM Sans 400/500/600/700 via Google Fonts CSS2 (display=swap
already in the URL). All 4 loaded weights are used in app CSS - no
dead weight to drop. Flagged, not fixed: 800/900 are used in several
component styles but never loaded, so the browser faux-bolds those -
a pre-existing rendering quirk, out of scope (changing loaded weights
risks visible text changes).
- SVG: icon-registry.ts centralizes all icons via @lucide/angular (no
inline SVG path duplication). Checked SVGs under public/ for editor
cruft (metadata/inkscape/sodipodi comments) - found none, already
clean. Flagged, not fixed: mastercard-logo.min.svg, dexar-logo*.svg,
dexar-favicon.svg, novo-logo.svg, novo-favicon.svg appear unreferenced
in src/public manifests - left in place since deletion is out of this
task's scope and they may be used by backend-driven tenant branding.
Verified: npx tsc --noEmit clean, npm run build green (initial bundle
unchanged at 1.12 MB, this pass only touched global CSS).
RC PERF-01 bundle audit follow-up on 61a5714.
- i18n: ru/en/hy translation packs (346 KB raw combined) were all
statically imported in TranslateService and shipped in the initial
bundle regardless of the visitor's language. Now only 'ru' (platform
default) is bundled eagerly; 'en'/'hy' are dynamic import()s. The
language route guard (languageGuard) awaits preloadLanguage() before
activating the route, so translations are always fully loaded before
any component renders - no flash of untranslated/fallback content.
- widget-host.service.ts: import UnknownWidgetComponent directly instead
of via the widgets/ui barrel (index.ts re-exports 6 widgets).
- Deleted src/app/components/items-carousel/* - confirmed dead (zero
references anywhere, verified via knip and grep), the only consumer
of primeng/primeicons in the app. Removed the now-unused
`@import 'primeicons/primeicons.css'` from styles.scss (no primeicons
CSS classes used elsewhere). primeng/primeicons remain listed in
package.json/package-lock.json - npm CLI in this environment is
blocked by an unrelated, pre-existing broken `barry-cache` devDependency
(ETARGET on `npm install`/`npm uninstall`), so the lockfile could not be
safely regenerated. Flagged, not fixed.
Routes audit (app.routes.ts): all storefront/builder/backoffice feature
routes already use loadComponent/loadChildren; nothing eagerly imported.
No route changes needed.
Lucide icons (icon-registry.ts): already named/tree-shakeable imports
from @lucide/angular, not a full-library import. No change needed.
Before/after (npm run build, production):
- Initial bundle raw: 1.47 MB -> 1.12 MB (-350 KB / -24%)
- Initial bundle transfer (est.): 263.59 kB -> 221.51 kB (-42 kB / -16%)
- Budget overage: 769.22 kB over -> 416.84 kB over (still exceeds the
700 KB budget; project-editor-page-component (320 kB),
catalog-container-component (126 kB), product-details-container
(88 kB), cart-component (61 kB) lazy chunks unchanged - no safe
mechanical split identified within scope, see PERF-01 report for
detail).
Verified: npx tsc --noEmit clean, npm run build green (warning only,
no errors).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RC PERF-01 platform-wide performance audit. App was the only one of 191
@Component decorators still on default change detection; all state
mutation flows through signal.set()/router-event handlers, so OnPush is
safe (no direct DOM mutation, no non-signal mutable bindings read in the
template).
Audit findings (see report):
- RxJS subscription leaks: 149 .subscribe() calls across 44 files
reviewed; all either use takeUntilDestroyed, manual Subscription +
ngOnDestroy, or self-completing HTTP/shareReplay observables in
providedIn:'root' singletons. No leaks found, no changes needed.
- OnPush coverage: 190/191 components already OnPush; app.ts fixed here.
- @for/*ngFor tracking: 0 legacy *ngFor found; @for requires track at
compile time in this Angular version. Already fully compliant.
- Duplicate HTTP calls: CategoryFacade and ConfigService already use
shareReplay({bufferSize:1, refCount:true}) caching consistently.
- Signals/BehaviorSubject boilerplate: only 2 combineLatest usages
app-wide, both narrow and already minimal; left as-is (no safe,
isolated leaf case to convert without touching facade state shape).
- Template method calls: mostly cheap signal reads or small pure
per-item formatters; none warranted extraction given OnPush is
already in place everywhere they're used.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Post-payment email/phone-capture form in cart.component.html was
commented-out markup (never rendered), with a matching ~90-line dead
.email-form CSS block still shipping in the bundle. Removed both.
Closes KNOWN-ISSUES.md item 10.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>