skills/vendix-panel-ui/SKILL.md
Panel UI module visibility system: backend defaults, NgRx selectors, MenuFilterService, module flows, and sidebar filtering. Trigger: When adding sidebar modules, configuring panel_ui visibility, debugging menu filtering, or distinguishing visibility from permissions.
npx skillsauth add rzyfront/vendix vendix-panel-uiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill for sidebar/menu visibility. panel_ui controls what the user sees in admin navigation; it is not backend authorization.
Any plan that introduces a new module or submodule MUST explicitly answer these two questions in writing. If the plan does not declare these decisions, block execution and ask the human.
default_visible_for_privileged_users (true / false)
true (recommended for features useful to everyone): owner / admin / super_admin see the item automatically on next login. Set the fallback value to true.false (experimental or super_admin-only): hidden by default; admin must enable it per-user from the settings UI. Set the fallback value to false.show_new_badge (yes / no, default yes)
yes (recommended whenever the item is meant to be discovered): the user dropdown banner ("Tienes N módulos nuevos disponibles") and the Settings → General → "Módulos del Panel" section surface the key as new until the user toggles/activates it.no: silent rollout. Only for admin-only utilities that do not need discoverability.The plan must include a one- or two-line justification for each decision. PRs that add a key to PANEL_UI_FALLBACK without these decisions documented must be rejected in review.
| Source | Role |
| --- | --- |
| user_settings.config.panel_ui | Main per-user module visibility map (nested by app_type) |
| user_settings.config.panel_ui_seen_keys | Per-user record of which module keys the user has interacted with (drives "Nuevo" badge) |
| user_settings.config.new_keys | Computed at read time: defaults keys not yet seen by the user (only for privileged roles) |
| user_settings.app_type | Selects the active app panel map |
| DefaultPanelUIService | Defaults from default_templates.user_settings_default deep-merged with hardcoded PANEL_UI_FALLBACK, with 5-minute cache |
| mergePanelUiSoft (backend util) | Lazy merge of defaults into panel_ui for privileged roles at read time (login/refresh/env-switch/getSettings) |
| store_settings.module_flows | Force-hides operational modules (accounting/payroll/invoicing) when flows disabled |
| MenuFilterService | Applies panel keys, store type rules, subscription feature requirements |
| APP_MODULES constant | Editable module catalog for settings UI |
The only supported shape is nested by app_type:
{
"ORG_ADMIN": { "dashboard": true, "stores": true, ... },
"STORE_ADMIN": { "dashboard": true, "products": true, "products_list": true, ... },
"STORE_ECOMMERCE": { ... },
"VENDIX_LANDING": {}
}
Flat legacy shape ({ products: true, dashboard: true } at top level) is not supported. The backend mergePanelUiSoft detects it (all top-level values are booleans) and discards it as if the user had no panel_ui. Privileged users then get defaults filled in; non-privileged users get {}.
panel_ui is UI visibility only. Protect APIs with permissions/guards.DefaultPanelUIService, not from the sidebar components.owner, admin, super_admin) receive panel_ui with defaults merged in at read time (lazy soft merge). They see new modules without needing DB backfill.manager, cashier, employee, etc.) get panel_ui exactly as stored. Admin must explicitly curate their access.false values are always respected. Defaults only fill undefined keys.MenuFilterService.moduleKeyMap; label mismatches hide items unexpectedly.moduleKeyMap mean OR logic across multiple keys.module_flows, and subscription gates may hide a module even when panel_ui[key] === true.PANEL_UI_FALLBACK appears automatically for privileged users with no DB write and no seed run. This is guaranteed by the auto-merge in getUnifiedTemplate() (see Real Backend Flow).user_settings.config.panel_ui_seen_keys[app_type] are surfaced via new_keys on every read. Discovery is shown only in the user dropdown banner and the Settings → General → "Módulos del Panel" section — never in the sidebar (intrusive).manager, cashier, employee), the badge does not apply: they only see the key after an admin toggles it in the settings UI or super_admin runs POST /superadmin/users/sync-panel-ui.DefaultPanelUIService.getUnifiedTemplate()This method now performs a deep merge between the DB template and the hardcoded PANEL_UI_FALLBACK:
default_templates.user_settings_default).app_type in PANEL_UI_FALLBACK:
key: value:
false).Consequence: adding a key to PANEL_UI_FALLBACK is sufficient for it to appear in production for privileged users. No seed run, no migration, no DB write required. The (optional) seed update remains good hygiene for fresh user creation but is no longer load-bearing for visibility.
Every endpoint that ships user_settings to the frontend runs mergeUserConfigPanelUi(config, defaults, roles):
user_settings + user roles + defaults (DefaultPanelUIService.generatePanelUI(''), which already includes the auto-merge above).PRIVILEGED_ROLE_NAMES → fill undefined keys from defaults per app_type. Otherwise return user's panel_ui as-is.new_keys (see "Nuevo" Badge System).config (in-memory only — no DB write).Read-path call sites:
apps/backend/src/domains/auth/auth.service.ts
getSettings() — explicit settings refresh endpointregisterOwner() responseregisterCustomer() responselogin() responseapps/backend/src/domains/auth/environment-switch.service.ts — userSettingsForResponse in env switch payloadBackend helpers:
apps/backend/src/common/utils/privileged-roles.util.ts — PRIVILEGED_ROLE_NAMES = { owner, admin, super_admin }, hasPrivilegedRole(roles)apps/backend/src/common/utils/panel-ui-merge.util.ts — mergePanelUiSoft, mergeUserConfigPanelUi, internal isLegacyFlatPanelUiapps/backend/src/common/services/default-panel-ui.service.ts — defaults source (template + fallback auto-merge)PRIVILEGED_ROLE_NAMES is the single source of truth for "who sees everything automatically". Anywhere else that filters by role (e.g. ELIGIBLE_ROLES in superadmin/users/users.service.ts) reuses this constant via Array.from(PRIVILEGED_ROLE_NAMES).
On every read path (login, refresh, env switch, getSettings), the backend computes:
new_keys[app_type] = defaults[app_type].keys.filter(
k => !user.panel_ui_seen_keys[app_type]?.includes(k)
);
Only computed for privileged roles. Returned on user_settings.config.new_keys.
POST /api/auth/panel-ui/mark-seen{ key: string, app_type: string }key to user_settings.config.panel_ui_seen_keys[app_type].new_keys.length.N next to the "Configuración" entry.new_keys shows the "Nuevo" indicator. Toggling/saving consumes the key via authFacade.markPanelUiSeen(key, app_type).MenuFilterService.isNewModule(label) and getNewKeyForLabel(label) remain exported so other UI surfaces can opt-in to render discovery hints — but the sidebar must not (decision after rev1 UX feedback).
Clients without panel_ui_seen_keys (first request after deploy) see all existing module keys as "Nuevo" in the dropdown banner and Settings list. The keys are then consumed gradually as the user activates them in Settings. This is intentional and self-healing.
The lazy soft merge does not write to DB. To persist the merge for users (e.g. before changing defaults again, or to capture a snapshot of current state), super_admin uses:
| Endpoint | Purpose |
| --- | --- |
| GET /superadmin/users/panel-ui-preview | Dry-run: counts eligible users, lists missing keys per app_type. No writes. |
| POST /superadmin/users/sync-panel-ui | Persists merge for eligible users. Body: { user_ids?: number[], app_types?: string[], strategy?: 'merge' \| 'replace' }. Default merge (fills missing keys, keeps false). replace overwrites entire app_type map. |
@Roles(UserRole.SUPER_ADMIN). No additional permission row required.PRIVILEGED_ROLE_NAMES (i.e. only owner, admin, super_admin are touched by default; other roles are not auto-backfilled).apps/backend/src/domains/superadmin/users/users.service.ts:syncPanelUI.user_settings from login/session restore (already merged by backend if user is privileged; includes new_keys).user_settings.config.panel_ui.user_settings.app_type.selectVisibleModules returns keys whose value is true, after module-flow adjustments.MenuFilterService.filterMenuItems(...) filters layout menu items recursively.isNewModule(label) for badges anymore. Discovery hints are rendered by the user-dropdown banner and the Settings → General "Módulos del Panel" section.authFacade.markPanelUiSeen(key, app_type) is dispatched from those surfaces (e.g. when the user toggles a module ON in Settings), never from a sidebar click.Frontend key files:
apps/frontend/src/app/core/store/auth/auth.selectors.tsapps/frontend/src/app/core/store/auth/auth.facade.tsapps/frontend/src/app/core/services/menu-filter.service.tsapps/frontend/src/app/shared/constants/app-modules.constant.tsapps/frontend/src/app/private/layouts/store-admin/store-admin-layout.component.tsapps/frontend/src/app/private/layouts/organization-admin/organization-admin-layout.component.tsBefore any code edit, the plan must record:
default_visible_for_privileged_users (true / false) — mandatory.show_new_badge (yes / no, default yes) — mandatory.Implementation steps:
DefaultPanelUIService.PANEL_UI_FALLBACK under the correct app_type, with the value chosen in decision #1.default_templates.user_settings_default as well — not required, since auto-merge in getUnifiedTemplate() covers visibility.APP_MODULES (frontend constant apps/frontend/src/app/shared/constants/app-modules.constant.ts) under the right app_type and parent (e.g. submodules of "Configuración" go inside settings.children[]). This is the pillar of admin curation: without this entry the module cannot be toggled from Settings → "Módulos del Panel" nor from Settings → Users → "Editar usuario", even if it shows up correctly in the sidebar. Forgetting this is the #1 cause of "I added a module but admin cannot enable/disable it per user".MenuFilterService.moduleKeyMap (single string or array for OR logic).Submodule keys should follow parent_child, for example orders_sales or settings_domains, unless existing code already uses a different key.
No DB backfill needed for privileged users. Owners/admins/super_admins see the new key automatically on next request (auto-merge in template + lazy soft merge per user). For non-privileged users the admin must either toggle the key in the settings UI per user, or super_admin can run POST /superadmin/users/sync-panel-ui after temporarily widening the eligible role set if a bulk operation is needed.
PANEL_UI_FALLBACK without the two decisions documented in the plan must be rejected in review.panel_ui for security: it's UI only. Always pair with PermissionsGuard / @Permissions on the backend.panel_ui in DB: the read-path utility discards it. Don't write flat shape from any handler or seed. If you find one, sync it via the super_admin endpoint (strategy: 'replace') to overwrite cleanly.ELIGIBLE_ROLES drift: any role list outside PRIVILEGED_ROLE_NAMES will silently mis-match the seed and act as a no-op. Always import from privileged-roles.util.ts.MenuFilterService.moduleKeyMap falls into the "no mapping" branch and is only shown if it has visible children. Easy to miss in tests.APP_MODULES entry: the module appears in the sidebar but admin cannot toggle it from Settings → "Módulos del Panel" nor from the per-user edit modal. Symptom: "the module is visible to me but I cannot enable/disable it for other users". Fix: add an entry under the correct app_type and parent in apps/frontend/src/app/shared/constants/app-modules.constant.ts.generatePanelUI(_app_type) ignores its argument — the underscore is a hint. It always returns the full nested map.getUnifiedTemplate() actually includes the new key in the resolved template, (b) the user has a privileged role, (c) the template cache is not stale beyond the 5-minute TTL after a redeploy.default_templates rows. Auto-merge in getUnifiedTemplate() is what covers the "key added after the initial seed" case — no re-seed required, but keep the seed in sync for hygiene.| Concern | System |
| --- | --- |
| Hide/show sidebar item | panel_ui, module_flows, store type, subscription filtering |
| Allow/deny API operation | PermissionsGuard, roles, auth guards, subscription guards |
| Show a module but block writes | Feature gate/subscription guard |
| Grant permission but hide menu | Possible; visibility and authorization are separate |
vendix-permissions - Backend authorizationvendix-settings-system - Settings persistence and defaultsvendix-subscription-gate - Subscription-based feature accessvendix-frontend-routing - Lazy routes for moduleshow-to-plan - Must record the two Critical Plan Decisions whenever a plan introduces a module or submoduledevelopment
Mobile app development rules for Vendix Expo/React Native project. Trigger: When editing, creating, or modifying any file under apps/mobile, or when developing mobile-specific features.
development
Feature gating by store subscription state: global store write guard, AI feature gate, Redis feature resolution, quota consumption, frontend paywall interceptor, banner, and subscription UI states. Trigger: When adding feature gates, paywalls, subscription-based access control, protecting store write operations, AI feature gates, or rollout flags.
testing
SaaS subscription billing for Vendix stores: plan pricing, invoices, Wompi platform payments, manual payments, partner commissions, payouts, proration, and dunning. Trigger: When creating SaaS invoices, working with partner rev-share, margin/surcharge pricing, invoice sequence allocation, partner payout batches, subscription payments, manual payments, or dunning flows.
development
Periodic quota counters with Redis, UTC period keys, Lua-based idempotent AI quota consumption, request-id deduplication, and post-success consumption. Trigger: When building quota counters, enforcing monthly/daily feature caps, or reusing AI quota patterns for uploads, emails, exports, or rate-limited features.