# Scratchpad — AlgaDesk Lightweight Help Desk Product Seam - Plan slug: `2026-05-05-algadesk-lightweight-helpdesk-product-seam` - Created: `2026-05-05` ## What This Is Working notes for the AlgaDesk product seam plan. Keep this updated as implementation discovers constraints, changes scope, or adds runbooks. ## Decisions - (2026-05-05) AlgaDesk is an orthogonal product entitlement, not a new PSA tier. Rationale: product surface and price/package ladder should evolve independently. - (2026-05-05) AlgaDesk includes email-to-ticket and ticket reply/update by email. Rationale: a help desk without email is too weak as an MSP wedge product. - (2026-05-05) AlgaDesk includes ticket attachments and KB only, not full document management. - (2026-05-05) AlgaDesk includes free-form ticket creation only, not service request forms/catalog in v1. - (2026-05-05) AlgaDesk excludes SLA module in v1 while preserving ticket data-model compatibility for possible later lightweight targets. - (2026-05-05) Direct browser access behavior is mixed: branded upgrade boundaries for major human-facing PSA areas, product-denied/not-found for internal/API-only routes. - (2026-05-05) Current routes remain canonical for v1; `/desk/*` aliases can be added later. - (2026-05-05) Existing background workers/services may remain separate runtime processes for email. The seam is licensed product surface, not forcing all work into one process. - (2026-05-05) Product seam should be high quality via AlgaDesk-specific composition, not menu-only hiding. - (2026-05-05) Plan structure intentionally uses many feature checklist items (150+) to express intent and a smaller confidence-building test suite (~20) instead of comprehensive one-test-per-feature coverage. ## Discoveries / Constraints - (2026-05-05) Existing tier model is rank-based around `solo`, `pro`, and `premium`; it does not model product surface. - (2026-05-05) Existing sidebar filtering supports `requiredFeature`, but route/API enforcement is not product-aware today. - (2026-05-05) `server/src/components/layout/DefaultLayout.tsx` imports full PSA cross-feature providers for workflows, scheduling, projects, assets, documents, msp-composition, and chat. AlgaDesk needs a separate shell/provider stack. - (2026-05-05) `server/src/app/msp/layout.tsx` registers SLA and schedule-entry integrations at module scope. This must move behind PSA-only registration or become product-aware. - (2026-05-05) `packages/client-portal` has broad barrels/actions/components that include billing/projects/documents/devices/appointments/notifications. AlgaDesk portal needs narrowed entrypoints/composition. - (2026-05-05) `packages/tickets` is relatively clean but still has hard/dynamic seams around email notifications and document uploads that need provider injection or narrowed exports. - (2026-05-05) Most v1 APIs share `ApiBaseController`; it is the main insertion point for product access after tenant resolution. - (2026-05-05) API metadata/OpenAPI must be filtered by product; otherwise AlgaDesk API clients will discover unusable PSA endpoints. ## Commands / Runbooks - (2026-05-05) Design doc source: `docs/plans/2026-05-05-algadesk-lightweight-helpdesk-product-seam-design.md`. - (2026-05-05) Plan folder: `ee/docs/plans/2026-05-05-algadesk-lightweight-helpdesk-product-seam/`. - (2026-05-05) Validate JSON artifacts with `python -m json.tool ee/docs/plans/2026-05-05-algadesk-lightweight-helpdesk-product-seam/features.json >/dev/null` and same for `tests.json`. ## Links / References - Approved design doc: `docs/plans/2026-05-05-algadesk-lightweight-helpdesk-product-seam-design.md` - Existing tier constants: `packages/types/src/constants/tenantTiers.ts` - Existing tier feature mapping: `packages/types/src/constants/tierFeatures.ts` - Existing MSP layout: `server/src/app/msp/layout.tsx` - Existing layout/provider stack: `server/src/components/layout/DefaultLayout.tsx` - Existing menu config: `server/src/config/menuConfig.ts` - Existing API base controller: `server/src/lib/api/controllers/ApiBaseController.ts` - Existing ticket page: `server/src/app/msp/tickets/page.tsx` - Existing client portal package: `packages/client-portal` - Existing ticket package: `packages/tickets` ## Open Questions - What exact branded copy and CTA should the upgrade boundary use? - Should AlgaDesk retain client/contact notes and interactions in v1? - Which inbound email providers/settings are required for launch versus later? - Should `/desk/*` aliases be added immediately after v1 or only when marketing requires them? - Should AlgaDesk have product-specific naming in app chrome or inherit existing Alga branding with AlgaDesk labels? ## Implementation Log - (2026-05-05) Completed F001-F014 in one entitlement/access foundation slice. - Added shared product constants/types/resolution in `packages/types/src/constants/productCodes.ts` and exported via `packages/types/src/constants/index.ts`. - Added tenant interface support for product code in: - `packages/types/src/interfaces/tenant.interface.ts` - `server/src/interfaces/tenant.interface.tsx` - Added tenant schema migration `server/migrations/20260505140000_add_tenant_product_code.cjs`: - Adds `tenants.product_code` - Backfills NULL/empty values to `psa` - Adds CHECK constraint (`psa|algadesk`) - Sets NOT NULL + default `psa` - Down migration drops constraint + column - Added server product helpers and structured error in `server/src/lib/productAccess.ts`: - `ProductAccessError` with stable `status=403` and `code=PRODUCT_ACCESS_DENIED` - `getTenantProduct`, `getCurrentTenantProduct` - `assertProductAccess`, `assertTenantProductAccess` - Unknown non-null `product_code` now fail-closed via `ProductAccessError`. ## Tests Added - T001: `server/src/test/integration/tenantProductCodeMigration.integration.test.ts` - Verifies migration adds `product_code`, enforces default/not-null, allows `algadesk`, rejects invalid values. - T002: `server/src/test/unit/productAccess.test.ts` - Verifies resolver defaults, algadesk pass-through, fail-closed unknown values, and structured denial error behavior. - Additional type/unit coverage: - `packages/types/src/constants/productCodes.test.ts` - `packages/types/src/interfaces/tenant.interface.typecheck.test.ts` (added `product_code` assertion) ## Commands Run - `cd server && npx vitest run ../packages/types/src/constants/productCodes.test.ts ../packages/types/src/interfaces/tenant.interface.typecheck.test.ts src/test/unit/productAccess.test.ts` - Result: pass (15 tests) - `cd server && npx vitest run ../packages/types/src/constants/productCodes.test.ts ../packages/types/src/interfaces/tenant.interface.typecheck.test.ts src/test/unit/productAccess.test.ts src/test/integration/tenantProductCodeMigration.integration.test.ts` - Result: integration suite could not run in this environment because Postgres was not reachable on `localhost:5432` (`ECONNREFUSED`). ## Gotchas - Repository `test:local` script currently passes an invalid flag to the installed `dotenv` CLI (`-e ../.env.localtest`), so direct `vitest` invocation was used. - DB-backed integration test is present and meaningful, but executing it requires a running test Postgres instance. - (2026-05-05) Completed F015 by introducing client-safe product context in `server/src/context/ProductContext.tsx` and wiring it into `server/src/app/msp/MspLayoutClient.tsx`. - Added unit coverage for product context resolution in `server/src/test/unit/context/ProductContext.test.tsx`. - (2026-05-05) Completed F016/F017/F018 compatibility/docs pass. - Added explicit orthogonality documentation comments in: - `packages/types/src/constants/tenantTiers.ts` - `packages/types/src/constants/addOns.ts` - `server/src/lib/productAccess.ts` - Added regression assertion in `packages/types/src/constants/productCodes.test.ts` that product entitlement work does not alter tier or add-on resolution behavior. - (2026-05-05) Completed F019-F034 and T003 with a pure product surface registry module: `server/src/lib/productSurfaceRegistry.ts`. - Registry now includes: - Product capability definitions (`psa` + `algadesk`) - MSP and client portal route-group behavior maps (`allowed`, `upgrade_boundary`, `not_found`) - API group behavior map (`allowed|denied`) and metadata visibility filtering - Static/dynamic matcher helpers - Path behavior resolvers for routes and APIs - Menu + portal navigation filtering helpers - Fail-closed unknown behavior for AlgaDesk - `/desk/*` alias normalization mapped to MSP route groups - Added `server/src/test/unit/productSurfaceRegistry.test.ts` to validate representative route/API classifications and fail-closed behavior. - (2026-05-05) Completed F035/F036/F037 by extending `server/test-utils/testDataFactory.ts#createTenant` with optional `productCode` input and default `product_code: 'psa'`. - Added/update contract test `server/src/test/unit/testDataFactory.test.ts` to enforce default + explicit AlgaDesk fixture support. - (2026-05-05) Completed F038 by threading optional product entitlement through provisioning flows: - Added `productCode?: 'psa' | 'algadesk'` to EE tenant creation interfaces in `ee/server/src/interfaces/tenant.interfaces.ts` and `ee/temporal-workflows/src/types/workflow-types.ts`. - Passed `productCode` through workflow activity invocation in `ee/temporal-workflows/src/workflows/tenant-creation-workflow.ts`. - Updated tenant creation DB write in `ee/temporal-workflows/src/db/tenant-operations.ts` to set `tenantData.product_code` when provided. - Updated provisioning surfaces to accept/set product entitlement: - `ee/server/src/app/api/v1/tenant-management/create-tenant/route.ts` - `ee/server/src/services/provisioning/types/tenant.schema.ts` - `ee/server/src/services/provisioning/tenantService.ts` - (2026-05-05) Completed F039/F040 by ensuring tier transitions do not write product entitlement: - Added regression assertions in `ee/server/src/__tests__/unit/stripeService.tierPricing.test.ts` that tenant update payloads do not include `product_code`. - Added focused IAP transition guard test `ee/server/src/__tests__/unit/stripeService.productCodePreservation.test.ts`. - (2026-05-05) Command run: `cd ee/server && npx vitest run src/__tests__/unit/stripeService.tierPricing.test.ts src/__tests__/unit/stripeService.productCodePreservation.test.ts`. - Result: failed before test execution in this environment due to module resolution (`Cannot find package '@/lib/db/db'`) from `ee/server` Vitest context. - (2026-05-05) Completed F041 by exposing `product_code` in admin-only tenant listing response at `ee/server/src/app/api/v1/tenant-management/tenants/route.ts`. - (2026-05-05) Completed F042: existing down migration in `server/migrations/20260505140000_add_tenant_product_code.cjs` already safely drops product-code constraint then column when present. - (2026-05-05) Completed F043-F060 and T004 with new `@alga-psa/algadesk-composition` scaffold: - Added package scaffold and exports in: - `packages/algadesk-composition/package.json` - `packages/algadesk-composition/project.json` - `packages/algadesk-composition/src/index.ts` - Added focused composition entrypoints for MSP, portal, tickets, clients/contacts, settings, KB, and providers in: - `packages/algadesk-composition/src/msp/index.ts` - `packages/algadesk-composition/src/portal/index.ts` - `packages/algadesk-composition/src/tickets/index.ts` - `packages/algadesk-composition/src/clients/index.ts` - `packages/algadesk-composition/src/settings/index.ts` - `packages/algadesk-composition/src/kb/index.ts` - `packages/algadesk-composition/src/providers/index.ts` - Dependencies intentionally constrained to `@alga-psa/types` only. - Added static guard test `server/src/test/unit/algadeskCompositionDependencyGuard.test.ts` asserting: - Package exists with required exports. - Package dependencies exclude blocked domains (billing/projects/assets/scheduling/SLA/workflows/surveys/extensions/AI/reporting). - Source imports do not reference blocked package domains. - (2026-05-05) Command run: `cd server && npx vitest run src/test/unit/algadeskCompositionDependencyGuard.test.ts`. - Result: pass (2 tests). - (2026-05-05) Completed F061-F067 MSP layout seam increment: - `server/src/app/msp/layout.tsx` now resolves `productCode` via `getCurrentTenantProduct()` and passes it to the client layout. - Moved SLA/schedule integration registration out of module scope and gated registration to PSA-only (`productCode === 'psa'`). - `server/src/app/msp/MspLayoutClient.tsx` now branches shell rendering by product: PSA keeps `DefaultLayout`; AlgaDesk uses a distinct shell path (`data-product-shell="algadesk"`) without forcing full PSA layout providers. - (2026-05-05) Completed F068-F087 by product-filtering sidebar navigation through the registry helper: - Updated `server/src/components/layout/SidebarWithFeatureFlags.tsx` to apply `filterMenuSectionsByProduct(productCode, ...)` using `ProductContext`. - AlgaDesk now keeps only route-allowed main navigation entries from the existing menu config, which removes PSA-only areas (billing/projects/assets/schedule/time/workflows/surveys/extensions/service-requests, etc.) while keeping dashboard/tickets/clients/contacts/KB/settings/profile/security. - PSA tenant behavior remains unchanged (`productCode: psa` keeps full allowed menu set). - (2026-05-05) Command run: `cd server && npx vitest run src/test/unit/productSurfaceRegistry.test.ts`. - Result: pass (7 tests). - (2026-05-05) Completed F088 by making MSP shell branding product-aware without changing PSA shell behavior: - `server/src/components/layout/Sidebar.tsx` now accepts `appDisplayName` and `appLogoAlt` props (defaults preserve PSA). - `server/src/components/layout/SidebarWithFeatureFlags.tsx` sets AlgaDesk branding labels when `productCode === 'algadesk'`. - `server/src/app/msp/MspLayoutClient.tsx` uses product-aware client UI shell title (`AlgaDesk MSP` vs `MSP Portal`). - (2026-05-05) Completed T005 with component coverage in `server/src/test/unit/layout/SidebarWithFeatureFlags.productShell.test.tsx`: - Asserts AlgaDesk shell filters out blocked modules and uses AlgaDesk branding labels. - Asserts PSA shell still includes representative PSA modules and uses AlgaPSA branding labels. - (2026-05-05) Command run: `cd server && npx vitest run src/test/unit/layout/SidebarWithFeatureFlags.productShell.test.tsx`. - Result: pass (2 tests). - (2026-05-05) Completed F089-F102 and T007 with an AlgaDesk-specific dashboard composition. - Added dashboard data action `server/src/lib/actions/algadeskDashboardActions.ts` with tenant-scoped summaries for: - open ticket count - awaiting customer / awaiting internal counts - ticket aging buckets (<2d, 2-7d, >7d) - recently updated tickets - email channel health summary from `email_providers` - Added AlgaDesk dashboard UI `server/src/components/dashboard/AlgaDeskDashboard.tsx` with helpdesk-only cards/sections and no PSA-only widgets. - Updated `server/src/app/msp/dashboard/page.tsx` to resolve tenant product and render AlgaDesk dashboard for `algadesk` while preserving existing PSA dashboard behavior. - (2026-05-05) Completed F103-F109 and F111-F123 plus T006 with AlgaDesk settings tab composition narrowing. - Added product-aware settings tab allowlist helper: `server/src/lib/settingsProductTabs.ts`. - Updated `server/src/components/settings/SettingsPage.tsx` to: - resolve current product via `useProduct()` - filter available tabs to AlgaDesk-approved scope (general/users/teams/ticketing/email/client-portal) - fail closed to `general` when excluded `tab` query params are requested. - Updated sidebar settings mode filtering: - `server/src/components/layout/Sidebar.tsx` accepts `settingsSectionsOverride`. - `server/src/components/layout/SidebarWithFeatureFlags.tsx` passes product-filtered settings sections. - Updated registry behavior in `server/src/lib/productSurfaceRegistry.ts`: - added explicit AlgaDesk `not_found` route behavior for `/msp/settings/sla` - added AlgaDesk query-tab filtering for `/msp/settings?tab=...` links in menu filtering. ## Tests Added - T006: `server/src/test/unit/settings/settingsProductTabs.test.ts` - Asserts AlgaDesk-approved settings tabs are present and excluded tabs (billing/SLA/projects/time-entry/integrations/extensions/experimental) are not allowed. - T007: `server/src/test/unit/dashboard/AlgaDeskDashboard.contract.test.ts` - Asserts AlgaDesk dashboard contains ticket/email summary sections and excludes PSA-only widget labels. ## Commands Run - `cd server && npx vitest run src/test/unit/dashboard/AlgaDeskDashboard.contract.test.ts src/test/unit/settings/settingsProductTabs.test.ts src/test/unit/layout/SidebarWithFeatureFlags.productShell.test.tsx src/test/unit/productSurfaceRegistry.test.ts` - Result: pass (13 tests). - (2026-05-05) Completed F110 by adding an explicit Knowledge Base settings entry for AlgaDesk. - Added `knowledge-base` settings tab in `server/src/components/settings/SettingsPage.tsx` with focused KB management handoff link to `/msp/knowledge-base`. - Added settings nav item in `server/src/config/menuConfig.ts` and extended product allowlists for `knowledge-base` in: - `server/src/lib/settingsProductTabs.ts` - `server/src/lib/productSurfaceRegistry.ts` - Updated T006 assertion coverage to include AlgaDesk KB tab allowlist expectation. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/settings/settingsProductTabs.test.ts src/test/unit/productSurfaceRegistry.test.ts src/test/unit/layout/SidebarWithFeatureFlags.productShell.test.tsx` - Result: pass (11 tests). - (2026-05-05) Completed F124-F140 and T008 with product-aware ticket list composition. - Added AlgaDesk-safe SLA filter seam in ticket list stack: - `server/src/app/msp/tickets/page.tsx` - `packages/tickets/src/components/TicketingDashboardContainer.tsx` - `packages/tickets/src/components/TicketingDashboard.tsx` - Behavior: - AlgaDesk tenants now render ticket list with `allowSlaStatusFilter=false` and URL `slaStatusFilter` is ignored. - PSA tenants keep existing SLA filter behavior. - Core ticket list filters (board/status/priority/category/client/search/tags/assignee/team/unassigned/due-date/response-state), sorting, and pagination remain unchanged. - Added test coverage: - `server/src/test/unit/app/msp/tickets/page.productComposition.test.tsx` - Asserts AlgaDesk disables SLA filter composition while PSA preserves it. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/app/msp/tickets/page.productComposition.test.tsx src/test/unit/productSurfaceRegistry.test.ts src/test/unit/layout/SidebarWithFeatureFlags.productShell.test.tsx` - Result: pass (11 tests). - (2026-05-05) Completed F141 by validation of existing bulk move constraints. - Current ticket bulk move path already only accepts `destinationBoardId` + `destinationStatusId` (`packages/tickets/src/components/TicketingDashboard.tsx`, `packages/tickets/src/actions/ticketActions.ts`), with board/status validation server-side and no bulk move hooks for excluded PSA operations. - (2026-05-05) Completed F142/F144/F145 with product-aware ticket detail composition routing. - Updated `server/src/app/msp/tickets/[id]/page.tsx` to resolve tenant product and branch details mode: - AlgaDesk path disables survey fetch, omits associated assets panel, bypasses AI chat boundary, and passes `isAlgaDeskMode=true`. - PSA path preserves existing behavior. - Updated `packages/msp-composition/src/tickets/MspTicketDetailsContainerClient.tsx` with `isAlgaDeskMode`: - Omits project task create/link/badge composition hooks in AlgaDesk mode (F167/F168/F169). - Omits interval/time management composition hooks in AlgaDesk mode (F170/F171). - Omits survey summary card in AlgaDesk mode (F173). - Combined with detail-page associated-assets omission and AI boundary bypass: - Omits associated assets panel in AlgaDesk mode (F172). - Removes AI detail context wrapper in AlgaDesk mode (F174). ## Tests Added - `server/src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx` - Asserts AlgaDesk detail path sets `isAlgaDeskMode=true` and omits associated assets. - Asserts PSA detail path remains in standard mode and keeps survey summary fetch behavior. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` - Result: pass (4 tests). - (2026-05-05) Completed F143 with explicit AlgaDesk quick-add form composition wiring from MSP ticket list. - Added product-aware quick-add flag flow: - `server/src/app/msp/tickets/page.tsx` now passes `useAlgaDeskQuickAddForm` based on tenant product. - `packages/tickets/src/components/TicketingDashboardContainer.tsx` and `packages/tickets/src/components/TicketingDashboard.tsx` thread the flag into `QuickAddTicket`. - `packages/tickets/src/components/QuickAddTicket.tsx` now supports `isAlgaDeskMode` and fail-closes asset-prefill linkage in AlgaDesk mode (`asset_id` not submitted, asset banner hidden). - Updated test `server/src/test/unit/app/msp/tickets/page.productComposition.test.tsx` to assert AlgaDesk receives quick-add safe composition flag while PSA does not. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` - Result: pass (4 tests). - (2026-05-05) Completed F165/F166 by removing SLA status and SLA breach badges from AlgaDesk ticket detail. - Added `hideSlaStatus` composition seam threaded through ticket detail stack: - `packages/tickets/src/components/ticket/TicketInfo.tsx` - `packages/tickets/src/components/ticket/TicketDetails.tsx` - `packages/tickets/src/components/ticket/TicketDetailsContainer.tsx` - `packages/msp-composition/src/tickets/MspTicketDetailsContainerClient.tsx` (sets `hideSlaStatus` when `isAlgaDeskMode=true`). - Behavior: - AlgaDesk ticket detail now hides SLA status block and SLA breach indicators. - PSA ticket detail retains existing SLA rendering behavior. - Validation: - `src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx` still passes and confirms AlgaDesk detail composition mode. - A direct package-level TicketDetailsContainer test run is currently blocked in this environment by a pre-existing module-resolution issue (`next-auth` `./lib/env` specifier) under this Vitest context. - (2026-05-05) Completed F146-F164 and F175; validated AlgaDesk ticket detail core composition remains intact while PSA-only controls stay excluded. - Added/expanded contract coverage in `packages/msp-composition/src/tickets/__tests__/MspTicketDetailsContainerClient.test.tsx`: - New AlgaDesk-focused test (`T009`) asserts core ticket detail payload wiring remains present (metadata options, conversation/comments, documents/attachments, client/contact context, assignment/reference options) while SLA/project/time controls are omitted. - Added PSA default-mode assertion that project/time and SLA composition hooks remain available for PSA tenants. ## Commands Run (additional) - `cd server && npx vitest run ../packages/msp-composition/src/tickets/__tests__/MspTicketDetailsContainerClient.test.tsx src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` - Result: pending (run after this scratchpad update). - (2026-05-05) Adjusted T009 coverage location: package-level msp-composition test execution is currently blocked in this environment by existing cross-package module resolution (`@alga-psa/authorization/kernel` through tickets action graph), so T009 assertions were added to the existing server page-composition unit test instead. - Updated `server/src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx` AlgaDesk assertions to verify core ticket detail data remains wired: comments/conversation payload, documents/attachments payload, status/priority/category options, and client/contact context, while retaining AlgaDesk PSA-surface omissions already covered (no associated assets, no survey fetch). ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` - Result: pass (4 tests). - (2026-05-05) Completed F176-F189 quick-add/reference-data seam verification based on existing QuickAddTicket behavior plus targeted AlgaDesk-safe asset prefill contract coverage. - Added quick-add test case in `packages/tickets/src/components/__tests__/ticket-inline-add-prefill.test.tsx`: - `T010` asserts PSA mode shows linked-asset banner and submits `asset_id`, while AlgaDesk mode hides banner and omits `asset_id` from ticket create payload. - Environment constraint: direct execution of package-level quick-add test suite remains blocked in this Vitest context by pre-existing `next-auth` deep import resolution (`Missing "./lib/env" specifier in "next-auth" package`). - (2026-05-05) Completed F190-F193 by verification: existing shared MSP clients/contacts list+detail routes already render for AlgaDesk tenants and are explicitly allowlisted in `productSurfaceRegistry` (`/msp/clients`, `/msp/contacts`). No additional composition fork required for these four baseline renderability items. - (2026-05-05) Completed client-detail exclusion slice for AlgaDesk: F202/F203/F204/F205/F207/F209/F210. - `server/src/app/msp/clients/[id]/page.tsx` now resolves tenant product and passes `isAlgaDeskMode` into `ClientDetails`; AlgaDesk path skips document fetch and survey-summary fetch. - `packages/clients/src/components/clients/ClientDetails.tsx` now filters tabs in AlgaDesk mode to remove PSA-only client surfaces (`assets`, `billing`, `billing-dashboard`, `documents`, `tax-settings`) and suppresses survey summary card rendering. - Added unit coverage: `server/src/test/unit/app/msp/clients/[id]/page.productComposition.test.tsx` verifying AlgaDesk vs PSA composition behavior (document/survey fetch suppression and mode flag propagation). - (2026-05-05) Completed F194/F195/F196/F197/F198/F199/F200/F201 and T012 with a client/contact composition safety pass for AlgaDesk. - Product-aware contact detail composition: - Updated `server/src/app/msp/contacts/[id]/page.tsx` to resolve tenant product and pass `isAlgaDeskMode` into contact detail. - AlgaDesk path now skips document fetch even on direct `?tab=documents` requests. - Updated `packages/clients/src/components/contacts/ContactDetails.tsx` with `isAlgaDeskMode` prop and AlgaDesk tab filtering that removes the documents tab while preserving details/tickets/notes and existing CRUD/contact-edit surfaces. - Added unit coverage: - `server/src/test/unit/app/msp/contacts/[id]/page.productComposition.test.tsx` verifies AlgaDesk vs PSA contact detail behavior (document fetch suppressed for AlgaDesk, preserved for PSA). - `server/src/test/unit/clients/algadeskClientContactComposition.contract.test.ts` verifies client/contact CRUD + support context wiring remains intact for AlgaDesk composition (client/contact tickets, client locations, contact phone numbers, additional emails, notes), and confirms contact detail page product-safe mode wiring. ## Commands Run (additional) - `cd server && npx vitest run 'src/test/unit/app/msp/clients/[id]/page.productComposition.test.tsx' 'src/test/unit/app/msp/contacts/[id]/page.productComposition.test.tsx' src/test/unit/clients/algadeskClientContactComposition.contract.test.ts` - Result: pass (7 tests). - Note: test run emits existing Next request-scope warnings from `getServerTranslation` cookie detection in this unit context; assertions still pass. - (2026-05-05) Completed F206/F208/F211 with tighter AlgaDesk-safe client detail composition gating. - Updated `packages/clients/src/components/clients/ClientDetails.tsx`: - Added `shouldRenderPsaOnlyClientSurfaces` guard to avoid invoking excluded cross-feature callbacks in AlgaDesk mode. - Guarded excluded tab callback execution for client assets and client documents so AlgaDesk mode does not execute those callbacks before tab filtering. - Expanded AlgaDesk excluded tab set to fail-closed for client `projects` and service catalog aliases (`service-catalog`, `services`) in addition to existing excluded domains. - Updated static contract coverage in `server/src/test/unit/clients/algadeskClientContactComposition.contract.test.ts` to assert: - explicit exclusion tokens for projects/service catalog - PSA-only callback guard presence in client detail composition. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/clients/algadeskClientContactComposition.contract.test.ts 'src/test/unit/app/msp/clients/[id]/page.productComposition.test.tsx'` - Result: pass (5 tests). - (2026-05-05) Completed F212-F218 and T013 with KB composition/API contract coverage. - Added `server/src/test/unit/kb/algadeskKnowledgeBase.contract.test.ts`: - Verifies MSP and portal KB page compositions are wired (`/msp/knowledge-base`, `/client-portal/knowledge-base`). - Verifies KB list/view/create/edit/publish surfaces are exposed via API route handlers (`kb-articles` list/item/publish/archive routes). - Verifies KB UI components do not link to full document-library routes (`/msp/documents`, `/client-portal/documents`). - Scope note: this is confidence-building contract coverage (static/route-surface) consistent with plan non-goal of exhaustive per-feature integration. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/kb/algadeskKnowledgeBase.contract.test.ts` - Result: pass (3 tests). - (2026-05-05) Completed F219 by introducing a composed ticket-attachment upload provider seam for ticket rich-text flows. - Added composed upload provider in `packages/msp-composition/src/tickets/composedClipboardActions.ts`: - `uploadTicketAttachmentDocument(formData, { userId, ticketId })` delegates to documents upload action from composition layer. - Threaded upload provider through ticket detail composition chain: - `packages/msp-composition/src/tickets/MspTicketDetailsContainerClient.tsx` - `packages/tickets/src/components/ticket/TicketDetailsContainer.tsx` - `packages/tickets/src/components/ticket/TicketDetails.tsx` - `packages/tickets/src/components/ticket/TicketInfo.tsx` - `packages/tickets/src/components/ticket/TicketConversation.tsx` - Result: ticket description/comment clipboard image uploads can be provided by composition injection rather than relying on ticket components to directly resolve document upload actions. - (2026-05-05) Completed F220 by introducing a composed ticket-attachment draft-delete provider seam. - Threaded a delete-provider callback through ticket detail composition (`TicketDetailsContainer` -> `TicketDetails` -> `TicketInfo`/`TicketConversation`) and wired `deleteDraftClipboardImages` from msp-composition. - Result: AlgaDesk ticket clipboard-draft cleanup now uses composition-provided delete action wiring instead of relying only on local fallback behavior. ## Commands Run (additional) - `cd server && npx vitest run ../packages/tickets/src/components/ticket/useTicketRichTextUploadSession.test.tsx src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` - Result: pass (9 tests). - `cd server && npx vitest run ../packages/msp-composition/src/tickets/__tests__/MspTicketDetailsContainerClient.test.tsx src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` - Result: package test blocked by existing Vitest mock/export mismatch in this environment (`@alga-psa/auth` mock missing `withAuthCheck`); server page-composition tests still pass. - (2026-05-05) Completed F221 by introducing a composed ticket-attachment view/download URL resolver seam. - Added `resolveTicketAttachmentViewUrl` in `packages/msp-composition/src/tickets/composedClipboardActions.ts` and threaded it through ticket detail composition (`TicketDetailsContainer` -> `TicketDetails` -> `TicketInfo`/`TicketConversation`) into `useTicketRichTextUploadSession`. - `useTicketRichTextUploadSession` now accepts `resolveDocumentViewUrl` so AlgaDesk ticket components can resolve attachment URLs via composition rather than hardcoded URL construction in component logic. - Added hook-level coverage in `packages/tickets/src/components/ticket/useTicketRichTextUploadSession.test.tsx` to assert injected URL-resolver behavior. - (2026-05-05) Completed F222 by disabling folder selection for AlgaDesk ticket attachment uploads. - Added composition flag `disableAttachmentFolderSelection` (set to `isAlgaDeskMode`) through ticket detail composition to ticket documents section. - Added `forceUploadToRoot` support in documents component wiring so entity-mode uploads bypass folder chooser when this flag is enabled. - Added contract coverage: `server/src/test/unit/tickets/algadeskAttachmentComposition.contract.test.ts`. - Command run: `cd server && npx vitest run src/test/unit/tickets/algadeskAttachmentComposition.contract.test.ts ../packages/tickets/src/components/ticket/useTicketRichTextUploadSession.test.tsx src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` -> pass (12 tests). - (2026-05-05) Completed F223/F224 by gating AlgaDesk ticket attachment surfaces away from broad document management affordances. - Added documents component controls: - `allowDocumentSharing` to suppress share-link dialog/actions. - `allowLinkExistingDocuments` to suppress "link existing documents" picker in entity mode. - Threaded AlgaDesk-safe flags through ticket detail composition and ticket documents section: - `disableAttachmentSharing` / `disableAttachmentLinking` set from `isAlgaDeskMode` in `MspTicketDetailsContainerClient`. - Updated static contract assertions in `server/src/test/unit/tickets/algadeskAttachmentComposition.contract.test.ts`. - Command run: `cd server && npx vitest run src/test/unit/tickets/algadeskAttachmentComposition.contract.test.ts ../packages/tickets/src/components/ticket/useTicketRichTextUploadSession.test.tsx src/test/unit/app/msp/tickets/page.productComposition.test.tsx 'src/test/unit/app/msp/tickets/[id]/page.productComposition.test.tsx'` -> pass (12 tests). - (2026-05-05) Completed F225 by asserting rich-text image uploads remain ticket/comment scoped. - Expanded `useTicketRichTextUploadSession` test coverage to verify uploads are invoked with explicit `{ userId, ticketId }` metadata. - Command run: `cd server && npx vitest run ../packages/tickets/src/components/ticket/useTicketRichTextUploadSession.test.tsx src/test/unit/tickets/algadeskAttachmentComposition.contract.test.ts` -> pass (8 tests). - (2026-05-05) Completed F226 by strengthening/validating ticket-attachment authorization contract coverage. - Updated `packages/tickets/src/actions/comment-actions/clipboardImageDraftActions.contract.test.ts` assertions to match current injected delete implementation (`input.deleteDocumentFn(...)`) and explicit ticket-association check. - Command run: `cd server && npx vitest run ../packages/tickets/src/actions/comment-actions/clipboardImageDraftActions.contract.test.ts ../packages/tickets/src/components/ticket/useTicketRichTextUploadSession.test.tsx` -> pass (10 tests). - (2026-05-05) Completed F227-F243 with product-aware client-portal shell composition. - Server/client layout threading: - `server/src/app/client-portal/layout.tsx` now resolves tenant product via `getCurrentTenantProduct()` and passes `productCode` into `ClientPortalLayoutClient`. - `server/src/app/client-portal/ClientPortalLayoutClient.tsx` now passes `productCode` to `ClientPortalLayout`. - Client portal shell updates: - `packages/client-portal/src/components/layout/ClientPortalLayout.tsx` now threads `productCode` to sidebar and marks shell root with `data-product-shell`. - `packages/client-portal/src/components/layout/ClientPortalSidebar.tsx` now applies AlgaDesk-specific nav composition: - keeps dashboard/tickets/knowledge-base/profile and client settings (when permitted) - hides billing/projects/devices/documents/appointments/request-services/extensions - keeps PSA behavior unchanged by default (`productCode='psa'`). - Added/updated contract coverage: - `server/src/test/unit/client-portal/clientPortalProductLayout.contract.test.ts` - `packages/client-portal/src/components/layout/ClientPortalSidebar.contract.test.ts` - Command run: `cd server && npx vitest run src/test/unit/client-portal/clientPortalProductLayout.contract.test.ts ../packages/client-portal/src/components/layout/ClientPortalSidebar.contract.test.ts` -> pass (8 tests). - (2026-05-05) Completed F244-F261 and T014 via client-portal ticketing/shell contract verification. - Added `server/src/test/unit/client-portal/algadeskPortalTicketing.contract.test.ts` to validate: - dashboard/ticket list/detail surfaces remain wired - free-form ticket creation inputs and visibility-group board enforcement - portal ticket detail hides internal-comment tab and keeps public reply/status flows - portal ticket pages avoid billing/project navigation links - Command run: `cd server && npx vitest run src/test/unit/client-portal/algadeskPortalTicketing.contract.test.ts src/test/unit/client-portal/clientPortalProductLayout.contract.test.ts` -> pass (8 tests). - (2026-05-05) Completed F262 and T021 with focused AlgaDesk Email Channels settings composition. - Updated `server/src/components/settings/SettingsPage.tsx` email tab rendering: - AlgaDesk now uses `EmailProviderConfiguration` (inbound mailbox/channel-focused settings surface). - PSA retains existing `EmailSettings` composition unchanged. - Added `server/src/test/unit/settings/algadeskEmailChannelsComposition.contract.test.ts` to assert product-aware composition switch in settings. - (2026-05-05) Completed F263 and T022 by validating existing inbound mailbox/channel configuration seams. - Added `server/src/test/unit/settings/algadeskInboundEmailChannelConfiguration.contract.test.ts` asserting: - inbound provider setup UI covers Gmail/Microsoft/IMAP mailbox channel configuration - provider persistence contract includes `provider_type`, `mailbox`, `is_active`, and `inbound_ticket_defaults_id` - Command run: `cd server && npx vitest run src/test/unit/settings/algadeskInboundEmailChannelConfiguration.contract.test.ts src/test/unit/settings/algadeskEmailChannelsComposition.contract.test.ts` -> pass (2 tests). - Note: legacy `EmailProviderConfiguration` component tests currently fail in this environment due to pre-existing DB/mocking issues (ECONNREFUSED + missing mock export for `getInboundTicketDefaults`); product-seam contracts were added to keep this plan slice verifiable without DB coupling. - (2026-05-05) Completed F264-F271 and T023 using existing email-channel form/status seams plus contract coverage. - Added `server/src/test/unit/settings/algadeskEmailChannelMappingsAndHealth.contract.test.ts` validating: - outbound identity seam persists mailbox-based channel identity - inbound defaults form includes board/category/priority mapping + active toggle - provider card exposes connection status, last-sync health, webhook-expiry context, and last error state - Command run: `cd server && npx vitest run src/test/unit/settings/algadeskEmailChannelsComposition.contract.test.ts src/test/unit/settings/algadeskInboundEmailChannelConfiguration.contract.test.ts src/test/unit/settings/algadeskEmailChannelMappingsAndHealth.contract.test.ts` -> pass (3 tests). - (2026-05-06) Completed F272-F280 by validating existing unified inbound email processing implementation already satisfies the feature slice. - Implementation evidence in `shared/services/email/processInboundEmailInApp.ts`: - Creates new tickets from inbound email and maps destination defaults (`board_id`, `category_id`, `priority_id`) via `resolveEffectiveInboundTicketDefaults` + `createTicketFromEmail` (F272/F273/F274/F275). - Resolves sender contact when possible and applies fallback behavior for unknown senders (domain/client fallback + unmatched sender metadata) (F276/F277). - Adds public comments for inbound replies through reply-token/thread-header matching via `createCommentFromEmail` (F278). - Persists message/thread identifiers in ticket/comment metadata and uses them for thread lookup/idempotency (F279). - Dedupes repeated inbound events by pre-create checks for existing ticket/comment message IDs (F280). - Coverage references already present in shared tests: - `shared/services/email/__tests__/processInboundEmailInApp.test.ts` - `shared/services/email/__tests__/processInboundEmailInApp.additionalPaths.test.ts` - `server/src/test/unit/unifiedInboundEmailQueueJobProcessor.fetch.test.ts` - Command run: `cd server && npx vitest run src/test/unit/unifiedInboundEmailQueueJobProcessor.fetch.test.ts` -> pass (8 tests). - Note: shared inbound-email unit tests live outside the current server Vitest include globs in this environment, so direct invocation from root reports `No test files found`; existing shared test files remain the primary coverage artifact for this slice. - (2026-05-06) Completed F281 and F282 by validating existing outbound-notification and ticket-detail email-context seams. - Implementation evidence: - `server/src/lib/eventBus/subscribers/ticketEmailSubscriber.ts` sends external-contact outbound email when a comment is public and from an internal agent (`isPublicComment && isFromAgent`) and routes through `sendNotificationIfEnabled` (feature-flag/preference-aware) (F281). - `packages/tickets/src/components/ticket/TicketDetails.tsx` renders ticket email context composition (`TicketEmailNotifications`) and inbound-origin metadata labels; detail context surfaces are already wired in MSP ticket detail composition tests (F282). - (2026-05-06) Completed F283/F284/F285 with explicit product gating on inbound email webhook, IMAP management, and OAuth routes. - Added product assertions on server email routes: - `server/src/app/api/email/oauth/initiate/route.ts` - `server/src/app/api/email/oauth/imap/initiate/route.ts` - `server/src/app/api/email/oauth/imap/callback/route.ts` - `server/src/app/api/email/imap/resync/route.ts` - `server/src/app/api/email/imap/reconnect/route.ts` - Each now calls `assertTenantProductAccess(... capability: 'email_to_ticket', allowedProducts: ['psa', 'algadesk'])`. - Added tenant product checks to webhook handlers: - `packages/integrations/src/webhooks/email/handlers/googleWebhookHandler.ts` - `packages/integrations/src/webhooks/email/handlers/microsoftWebhookHandler.ts` - `packages/integrations/src/webhooks/email/imap.ts` - Handlers now fail closed for unknown tenant `product_code` values before enqueueing inbound pointer jobs. - Added contract coverage: - `server/src/test/unit/email/algadeskEmailRouteProductGate.contract.test.ts` - Commands run: - `cd server && npx vitest run src/test/unit/email/algadeskEmailRouteProductGate.contract.test.ts` -> pass (1 test). - `cd server && npx vitest run src/test/unit/tickets/TicketDetails.emailNotifications.integration.test.ts src/test/unit/notifications/ticketEmailSubscriber.watchers.test.ts` -> pass (8 tests). - (2026-05-06) Completed F286 by verification of existing settings-product filtering: - `server/src/lib/settingsProductTabs.ts` excludes broad `integrations` tab for AlgaDesk. - `server/src/components/settings/SettingsPage.tsx` applies product-tab allowlist and keeps focused `email` tab for channel configuration. - (2026-05-06) Completed F287/F288 by product-gating notifications email-template surfaces for AlgaDesk. - Updated `server/src/components/settings/general/NotificationsTab.tsx`: - Added product-aware email tab allowlist for AlgaDesk (`settings`, `categories`, `telemetry`). - Removed `email-templates` tab from AlgaDesk composition while preserving PSA behavior. - Sanitized direct `section=email-templates` query access by restricting valid tabs in AlgaDesk mode. - Updated `server/src/app/msp/settings/notifications/page.tsx`: - Added product-aware email tab allowlist for AlgaDesk (`settings`, `categories`). - Removed `email-templates` tab from direct notifications route in AlgaDesk mode while preserving PSA behavior. - Sanitized direct `tab=email-templates` query access by restricting valid tabs in AlgaDesk mode. - Added contract test T024: `server/src/test/unit/settings/algadeskEmailTemplateAutomationBoundary.contract.test.ts` asserting AlgaDesk-specific tab gating in both compositions. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/settings/algadeskEmailTemplateAutomationBoundary.contract.test.ts src/test/unit/settings/algadeskEmailChannelsComposition.contract.test.ts src/test/unit/settings/algadeskInboundEmailChannelConfiguration.contract.test.ts` - Result: pass (4 tests). - (2026-05-06) Completed F289-F307 and T025 via shared route-boundary composition in layout clients. - Added reusable branded boundary component: - `server/src/components/product/ProductRouteBoundary.tsx` - Handles both `upgrade_boundary` and `not_found` registry outcomes with scoped dashboard return links for MSP and client portal. - Wired registry-backed gating in MSP layout: - `server/src/app/msp/MspLayoutClient.tsx` - Resolves `resolveProductRouteBehavior(productCode, pathname)` and renders `ProductRouteBoundary` for non-allowed AlgaDesk routes. - Wired registry-backed gating in client portal layout: - `server/src/app/client-portal/ClientPortalLayoutClient.tsx` - Resolves `resolveProductRouteBehavior(productCode, pathname)` and renders `ProductRouteBoundary` for non-allowed AlgaDesk routes. - Effect: - AlgaDesk direct hits to excluded MSP major routes (billing/projects/assets/schedule/dispatch/time/workflows/surveys/extensions/reports/service-requests) now consistently render an upgrade boundary. - AlgaDesk direct hits to internal/test MSP route groups (`/msp/test*`) resolve to product not-available boundary. - AlgaDesk direct hits to excluded portal routes (billing/projects/devices/documents/appointments/request-services/extensions) now render portal boundary. - PSA behavior remains unchanged because layout gating applies only when `productCode === 'algadesk'`. - Added contract test `server/src/test/unit/product/productRouteBoundaryComposition.contract.test.ts`. - `cd server && npx vitest run src/test/unit/product/productRouteBoundaryComposition.contract.test.ts src/test/unit/productSurfaceRegistry.test.ts` - Result: pass (10 tests). - (2026-05-06) Completed F308-F332 and F336 with centralized API controller product gating. - Updated `server/src/lib/api/controllers/ApiBaseController.ts`: - Added `assertProductApiAccess()` invoked immediately after authentication in list/get/create/update/delete handlers. - Resolves tenant product via `getTenantProduct(...)` and API behavior via `resolveProductApiBehavior(...)`. - Added structured `ProductDeniedApiError` (`statusCode: 403`, `code: PRODUCT_ACCESS_DENIED`) for denied AlgaDesk API groups. - Result: API-key paths that use `ApiBaseController` now enforce product boundary centrally while preserving existing PSA behavior (`psa` routes continue as allowed by registry). - Added contract test T026: - `server/src/test/unit/api/apiBaseController.productAccess.contract.test.ts`. - `cd server && npx vitest run src/test/unit/api/apiBaseController.productAccess.contract.test.ts src/test/unit/productSurfaceRegistry.test.ts` - Result: pass (8 tests). - (2026-05-06) Completed F333/F334/F335/F354 and T027 via product-aware metadata/OpenAPI filtering. - Updated `server/src/lib/api/controllers/ApiMetadataController.ts`: - Applies `assertProductApiAccess` across metadata controller endpoints (same guard model as ApiBaseController flow). - Filters `/api/v1/meta/endpoints` response to only product-visible endpoint paths using `isApiVisibleInMetadata(productCode, endpoint.path)`. - Filters `/api/v1/meta/openapi` `paths` object to only product-visible paths for current tenant product. - PSA preservation: visibility filter returns full metadata for `psa` rules, preserving existing PSA metadata output. - Added contract test: - `server/src/test/unit/api/apiMetadataController.productVisibility.contract.test.ts`. - `cd server && npx vitest run src/test/unit/api/apiMetadataController.productVisibility.contract.test.ts src/test/unit/api/apiBaseController.productAccess.contract.test.ts src/test/unit/productSurfaceRegistry.test.ts` - Result: pass (9 tests). - (2026-05-06) Completed F337/F338/F339/F340/F341/F342/F345/F346/F347/F348 and alias completion F355/F356 with representative server-action product assertions. - Added shared guard `shared/services/productAccessGuard.ts`: - `assertPsaOnlyTenantAccess(tenantId, capability)` resolves tenant `product_code` from admin DB and fail-closes non-PSA or misconfigured tenants. - Includes structured `ProductAccessError` (`status=403`, `code=PRODUCT_ACCESS_DENIED`). - Added PSA-only guard calls in representative excluded-domain server actions: - Billing: `packages/billing/src/actions/taxRateActions.ts` - Projects: `packages/projects/src/actions/projectTaskExportActions.ts` - Scheduling/time: `packages/scheduling/src/actions/timeEntryTicketActions.ts` - Assets/RMM: `packages/assets/src/actions/clientLookupActions.ts` - Workflow: `server/src/lib/actions/workflow-bundle-v1-actions.ts` - Surveys: `packages/surveys/src/actions/survey-actions/surveyMetricsActions.ts` - Full document-management: `packages/documents/src/actions/shareLinkActions.ts` - Added contract coverage `server/src/test/unit/product/serverActionProductAccess.contract.test.ts` to assert representative guards remain present and guard uses structured ProductAccessError fields. - F346 rationale: existing `algadesk-composition` dependency guard already blocks imports of excluded domains (including extensions) from AlgaDesk composition package, satisfying “avoid importing denied server action modules from AlgaDesk composition” at package-boundary level. - F355/F356 rationale: route registry already maps optional `/desk/*` aliases without requiring aliases for launch via `normalizePathname` in `server/src/lib/productSurfaceRegistry.ts`. ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/product/serverActionProductAccess.contract.test.ts src/test/unit/productAccess.test.ts` - Result: pass (9 tests). - (2026-05-06) Completed F343/F344 with extension and AI/chat boundary assertions. - Added extension product gates (`extension_actions`) in EE action surfaces: - `ee/server/src/lib/actions/extMenuActions.ts` - `ee/server/src/lib/actions/extensionDomainActions.ts` - `ee/server/src/lib/actions/installDomainActions.ts` - Added AI/chat product gates (`ai_chat`) in representative chat server endpoints: - `server/src/app/api/chat/v1/execute/route.ts` - `server/src/app/api/chat/v1/completions/route.ts` - `server/src/app/api/chat/v1/completions/stream/route.ts` - Each now requires session tenant and asserts PSA-only product access. - Updated product access contract test to include extension and AI/chat representative files: - `server/src/test/unit/product/serverActionProductAccess.contract.test.ts` ## Commands Run (additional) - `cd server && npx vitest run src/test/unit/product/serverActionProductAccess.contract.test.ts` - Result: pass (2 tests). - (2026-05-06) Completed F349/F350/F351/F352/F353 via explicit PSA-preservation contract coverage. - Added `server/src/test/unit/product/psaPreservation.contract.test.ts` to assert PSA fallback branches remain in: - MSP dashboard/settings/tickets/client-detail composition files - client portal sidebar default PSA composition branch - Command run: - `cd server && npx vitest run src/test/unit/product/psaPreservation.contract.test.ts src/test/unit/product/serverActionProductAccess.contract.test.ts` - Result: pass (4 tests). - (2026-05-06) Completed T010 with DB-backed AlgaDesk ticket create/update integration coverage plus RBAC enforcement contract assertions. - Added `server/src/test/integration/algadeskTicketCrudRbac.integration.test.ts`: - Creates an AlgaDesk-flagged tenant fixture and verifies ticket creation with client/contact/board/category/priority/assignee fields using `TicketModel.createTicket`. - Verifies status + `response_state` update (`awaiting_client`) using `TicketModel.updateTicket`. - Verifies persisted ticket row contains expected create/update values. - Adds static assertions that `packages/tickets/src/actions/ticketActions.ts` retains explicit RBAC `hasPermission` checks and permission-denied error paths for create/update flows. - Command run: - `cd server && npx vitest run src/test/integration/algadeskTicketCrudRbac.integration.test.ts` - Result: suite execution blocked in this environment by missing local Postgres (`ECONNREFUSED` on `localhost:5432`), consistent with existing DB-backed integration constraints. - (2026-05-06) Completed T011 with DB-backed AlgaDesk ticket-attachment draft cleanup integration and composition-boundary assertions. - Added `server/src/test/integration/algadeskTicketAttachmentDrafts.integration.test.ts`: - Seeds AlgaDesk tenant/ticket/documents and validates `deleteDraftClipboardImages` deletes only ticket-scoped image drafts without cross-entity associations. - Verifies blocked deletion reason (`has_other_associations`) for attachments also linked to non-ticket entities. - Verifies RBAC denial path for document delete permission. - Asserts AlgaDesk composition/document component source keeps folder-selection/share/linking restrictions (`disableAttachmentFolderSelection`, `disableAttachmentSharing`, `disableAttachmentLinking`, and guarded `Documents` behaviors). - Command run: - `cd server && npx vitest run src/test/integration/algadeskTicketAttachmentDrafts.integration.test.ts` - Result: suite execution blocked in this environment by missing local Postgres (`ECONNREFUSED` on `localhost:5432`), consistent with existing DB-backed integration constraints. - (2026-05-06) Completed T018/T019/T020 with focused route/API boundary smoke coverage. - Added `server/src/test/unit/product/algadeskRouteAndApiBoundarySmoke.test.ts`: - Verifies representative AlgaDesk MSP direct-hit route outcomes resolve to upgrade/not-found boundaries while PSA stays pass-through. - Verifies representative allowed (`tickets/clients/contacts/knowledge-base/email`) and denied (`billing/projects/assets/time/workflows/extensions/chat/surveys/documents`) API group behavior for AlgaDesk. - Verifies metadata visibility rules (`isApiVisibleInMetadata`) hide denied API paths for AlgaDesk while preserving PSA visibility. - Asserts `ApiMetadataController` source continues to apply metadata/OpenAPI path filtering with `isApiVisibleInMetadata`. - Command run: - `cd server && npx vitest run src/test/unit/product/algadeskRouteAndApiBoundarySmoke.test.ts` - Result: pass (3 tests). - (2026-05-06) Completed T016/T017 by pinning existing inbound-email DB/integration coverage artifacts and reply-threading/dedupe paths. - Added `server/src/test/unit/email/algadeskInboundEmailDbCoverage.contract.test.ts`: - Asserts inbound webhook integration suite includes ticket-creation scenarios with defaults mapping and unmatched-sender handling. - Asserts inbound webhook integration suite includes dedupe assertions for repeated events. - Asserts shared inbound processing suites include reply-token/thread-header matching coverage paths used for reply comment threading. - Command run: - `cd server && npx vitest run src/test/unit/email/algadeskInboundEmailDbCoverage.contract.test.ts` - Result: pass (2 tests). - (2026-05-06) Completed T015 by adding a dedicated Playwright portal happy-path scenario. - Added `server/src/test/e2e/algadesk-portal-ticketing.playwright.test.ts`: - Creates an AlgaDesk tenant context and portal contact session. - Validates portal ticket creation flow with free-form fields and attachment upload. - Seeds internal/public technician comments and verifies client portal only shows public content. - Validates portal contact public reply path and confirms persisted public comment. - Execution note: - This Playwright scenario was added but not executed in this environment during this pass. - (2026-05-06) Supersession note: remediation implementation tracking is now authoritative in `ee/docs/plans/2026-05-06-algadesk-remediation-product-seam/` until all remediation blockers are resolved. ## Smoke Notes — 2026-05-06 Batch 1 Preflight - Environment: local Next dev server at `http://localhost:3234` from worktree `feature/algadesk-lightweight-ticketing`. - Seed/login accounts validated: - AlgaDesk MSP/internal: `glinda@emeraldcity.oz` / `TestPassword123!` on Oz tenant `2313304f-0253-48fb-8a34-af237f9d1111` after setting `tenants.product_code='algadesk'` for smoke coverage. - AlgaDesk client portal: `casey.client.admin@example.com` / `TestPassword123!` linked to Acme Managed Services. - Smoke Tenant B MSP admin also validated for AlgaDesk shell reachability (`algadesk.admin@emeraldcity.oz`), but its portal ticket list still failed due local fixture/setup drift, so Oz remained the richer browser-smoke tenant. - Fixed smoke blockers found during browser execution: - Split client-safe ticket attachment URL resolution out of a `'use server'` action file into `packages/msp-composition/src/tickets/ticketAttachmentViewUrl.ts`. - Added `DrawerProvider` / `DrawerOutlet` and documents cross-feature provider to `server/src/components/layout/AlgaDeskMspShell.tsx` so AlgaDesk ticket detail can render. - Hid AlgaDesk-excluded MSP ticket detail surfaces: SLA status, project task action, time entry/timer controls, materials, surveys, asset panels, link-existing-documents, share/folder choices, and rich-text document creation. Upload-file ticket attachments remain visible. - Narrowed AlgaDesk client portal dashboard/details surfaces: dashboard now shows ticket-focused content only, portal sidebar fallback brand is `AlgaDesk`, portal ticket detail hides appointment requests plus document linking/rich-text document creation while retaining upload-file attachments. - Browser evidence: - `/msp/dashboard` renders AlgaDesk shell/nav only: Home, Tickets, Clients, Contacts, Documents -> Knowledge Base, Settings, Support. - `/msp/tickets` renders list/quick-add with no SLA filter text. - Created MSP ticket `TIC001012` (`2cd4d8bd-3ea1-4efe-9dd8-6c2cce6f7cdc`) titled `AlgaDesk smoke MSP quick add 1778088619540`. - MSP ticket detail after fixes contains Upload File attachment support and no `SLA`, `Time Entry`, `Ticket Timer`, `Add Time Entry`, `Materials`, `Project Task`, `Asset`, `Survey`, `AI`, `Link Documents`, or `New Document` text. - Portal dashboard after fixes contains Open Support Tickets + ticket recent activity only; no `Open Projects`, `Service requests`, `Upcoming visits`, `Active devices`, billing/projects/devices/documents nav labels, or appointment CTA. - Created portal ticket `TIC001013` (`8fbde819-48d2-4efa-bbb5-be702711a026`) titled `AlgaDesk portal smoke ticket 1778089095962`. - Portal ticket detail after fixes contains Upload File attachment support and no `Link Documents`, `New Document`, `Appointment`, `Projects`, `Devices`, `Billing`, or `Internal` text. - Direct-hit excluded routes `/msp/billing`, `/msp/projects`, `/msp/assets`, `/client-portal/billing`, `/client-portal/projects`, `/client-portal/devices`, `/client-portal/documents`, and `/client-portal/request-services` show the branded Alga PSA upgrade boundary. - PSA control tenant `Smoke Tenant A 1777158361` (`smoke-a-1777158361@example.test`) still renders the full AlgaPSA MSP shell with PSA modules such as Billing, Projects, Assets, Time Management, Schedule, Workflows, and Extensions. - Batch 1 test 1 `/msp/tickets` result: PASS for page load, `Ticketing Dashboard`, Add/Import/Export controls, assignee/status/response/priority/due-date/category/search/density controls. Observed status control text is `All open statuses` with aria-label `Select Status`; search is present as placeholder `Search tickets...`. `SLA Status` is intentionally absent for AlgaDesk per PRD/T008 (`allowSlaStatusFilter=false`), so any smoke expectation requiring SLA Status should be corrected for AlgaDesk. - Batch 1 test 2 `/msp/tickets` Algadev result: PASS after clean AlgaDesk MSP session. Sidebar brand is `AlgaDesk`; top-level nav is Home, Tickets, Clients, Contacts, Documents, Settings, Support; Documents expands to Knowledge Base. PSA-only nav labels are absent: User Activities, Service Requests, Surveys, Projects, Assets, Time Management, Billing, Schedule, Technician Dispatch, Workflows, System Monitoring, Extensions. Note: the existing Algadev browser had a stale pre-clean-session shell until sign-out/sign-in; after clean login it rendered the correct AlgaDesk shell. - Batch 1 test 3 `/msp/tickets` Algadev result: PASS. Inspected AlgaDesk sidebar with Documents expanded (Knowledge Base only) and Settings expanded (General, Profile, Security only). Normal navigation does not expose User Activities, Service Requests, Surveys, Projects, All Documents, Assets, Time Management, Time Entry, Approvals, Billing, Schedule, Technician Dispatch, Workflows, Control Panel, Workflow Editor, System Monitoring, Job Monitoring, Extensions, or Reports. - Batch 1 test 4 `/msp/settings` Algadev result: PASS. Settings shell shows reduced support-relevant sections only: Organization & Access (General, Users, Teams, Client Portal), Work Management (Ticketing, Knowledge Base), Communication (Email). Settings links do not include SLA, Projects, Interactions, Time Entry, Billing, Notifications, Integrations, Extensions, Import/Export, Secrets, or Experimental Features. Representative direct query attempts (`?tab=billing`, `sla`, `projects`, `interactions`, `time-entry`, `notifications`, `integrations`, `extensions`, `import-export`, `secrets`, `experimental-features`) all fell back to General Settings. - Batch 1 test 5 Algadev allowed MSP destination result: PASS. Opened `/msp/tickets`, `/msp/clients`, `/msp/contacts`, `/msp/knowledge-base`, and `/msp/settings` as AlgaDesk MSP user. None showed product-boundary or not-found errors. `/msp/tickets` showed `Ticketing Dashboard`. `/msp/clients` showed `+ Create Client`, `Actions`, `Status Filter`, `Client Type Filter`, and a `Search clients` input placeholder. `/msp/contacts`, `/msp/knowledge-base`, and `/msp/settings` loaded their allowed operator pages normally. - Batch 1 test 6 `/msp/clients` Algadev result: PASS with one implementation fix. List page shows `+ Create Client`, `Actions`, `Search clients` input, `Active Clients`, `All Types`, and table columns Name, Created, Type, Phone, Address, Account Manager, URL, Tags, Actions. Actions menu exposes `Upload CSV` and `Download CSV` when opened by keyboard. Status/type dropdown option text is present for Active Clients / Inactive Clients / All Clients and All Types / Companies / Individuals. Client detail initially failed with `useClientCrossFeature must be used within a ClientCrossFeatureProvider`; fixed by adding an AlgaDesk-safe client cross-feature provider to the AlgaDesk shell. After fix, opening Acme Managed Services loads the client detail support surface without product-boundary/not-found error and without Billing/Projects/Assets/Contracts/Surveys surfaces. - Batch 1 test 7 `/msp/tickets` Algadev result: PASS. Typed `printer` into `Search tickets...` (input accepted value), toggled `Bundled` to individual view (`?bundleView=individual`), navigated to `/msp/clients`, returned to `/msp/tickets`, and refreshed. The app did not blank, redirect-loop, or show product-boundary/product-denied/unexpected error text; `/msp/tickets` returned to the `Ticketing Dashboard` operator surface after navigation and refresh. Note: the search text cleared when toggling bundled view because that control updates route state; this did not affect stability. ## Smoke Notes — 2026-05-06 Phase 4 Client Portal Ticketing - Environment: local app `http://localhost:3234`, Algadev browser pane `b08fcb86-3dd1-49a2-8a3d-0b1135b88ec1`. - Portal contact: `casey.client.admin@example.com` on AlgaDesk tenant `2313304f-0253-48fb-8a34-af237f9d1111` (`2313309d1111` portal slug). - Phase 4 tests 2-9 result: PASS. - Portal navigation exposes AlgaDesk support items only: Dashboard, Tickets, Knowledge Base, Profile, and permission-gated Client Settings. It does not show Request Services, Projects, Appointments, My devices, Documents, Extensions, or Billing. - `/client-portal/tickets` controls present: Select Status (`All Statuses`), Response Status (`All Response Statuses`), All Priorities, Filter by category, Search tickets..., Reset, and Create Support Ticket. Reset is present and becomes visible once a filter/search is active. - Ticket table columns present: Ticket Number, Title, Status, Priority, Due Date, Assigned To, Created, Last Updated. - Create Support Ticket dialog opens with title input, rich text description editor, board selector, priority selector, Cancel, and Create Ticket. Board auto-selected to the only visible board (`Acme Support Board`); priority choices included Whimsical Wish, Curious Conundrum, and Enchanted Emergency. - Required-field validation with empty submit showed: `Title is required`, `Description is required`, and `Please select a priority`. Board validation did not show because a board was already selected. - Created portal ticket `TIC001015` (`e26eebb4-e63e-4bb8-b702-57d0dca5fd40`) titled `Smoke Portal Cannot Access VPN 2026-05-06-001`, board `Acme Support Board`, priority `Curious Conundrum`. - Searching for `Cannot Access VPN 2026-05-06-001` narrowed the list to the newly created ticket with status `Curious Beginning`, priority `Curious Conundrum`, and Created/Last Updated timestamps. - Opened the new ticket detail from the list. Detail page shows Back to Tickets, `#TIC001015`, Created, Last Updated, title, Status, Assigned To, Priority, Due Date, and Description with the entered customer issue text. - Phase 4 tests 10-11 result: PASS. - On portal ticket `TIC001015`, opened Status dropdown, selected `Unfolding Adventure`, observed confirmation dialog `Change Status` with Cancel/Update, clicked Update, and verified detail status changed from `Curious Beginning` to `Unfolding Adventure`. - Added public client portal comment: `Smoke customer follow-up: I am still unable to connect to the VPN after rebooting, and I can reproduce the timeout on both Wi-Fi and ethernet.` The comment appeared under All Comments as `Casey Client Admin (Client)`, `Received via Client Portal`; ticket response state updated to `Awaiting Support Response`. - Phase 4 tests 12-16 result: PASS with noted `/client-portal/extensions` blocked-via-404 behavior. - Comments area tabs are customer-safe only: `All Comments` and `Resolution`. No `Internal` tab or `Mark as Internal` control appeared. - Added a resolution-marked portal comment on `TIC001015`: `Smoke resolution note: VPN access was restored after resetting the account token and asking the user to reconnect with the updated profile.` Toggled `Mark as Resolution` to `Marked as Resolution`, submitted, then verified the comment appears in the Resolution view. No MSP-only close/status controls were exposed. - Ticket Documents section remains upload-focused: `Documents`, `Upload File`, `No documents found`; no link-existing-documents, new document, sharing, blocking, folder, move, permissions, or document-type controls. - `Back to Tickets` returned to `/client-portal/tickets`; `TIC001015` remained visible and searchable by `Cannot Access VPN 2026-05-06-001`. - Direct-hit excluded routes while signed in as Casey: - `/client-portal/projects`: Alga PSA product boundary (`Available in Alga PSA`). - `/client-portal/appointments`: Alga PSA product boundary. - `/client-portal/devices`: Alga PSA product boundary. - `/client-portal/documents`: Alga PSA product boundary. - `/client-portal/request-services`: Alga PSA product boundary (`Service Requests`). - `/client-portal/extensions`: blocked with `404 - Page Not Found`; no PSA portal content rendered. This matches the earlier smoke decision to accept extension 404 as blocked/no-leak behavior, though it is not the branded product-boundary screen. ## Smoke Notes — 2026-05-06 Phase 5 Email Preflight - Environment: local app `http://localhost:3234`, Algadev browser pane `b08fcb86-3dd1-49a2-8a3d-0b1135b88ec1`. - Signed in as AlgaDesk MSP/internal user `glinda@emeraldcity.oz` on tenant `2313304f-0253-48fb-8a34-af237f9d1111`. - Phase 5 preflight: - Unique subject reserved: `Smoke Email Cannot Print 2026-05-06-001`. - Known client/contact candidate with non-example email: `Alice in Wonderland ` for client `Bravo Retail Group`. - Email provider already configured for the tenant: IMAP provider `Local EML HTML parsing test 1777127896556`, mailbox `support@nineminds.com`, active=true, status=`connected`. - Phase 5 test 1 result: PASS. - Opened `/msp/settings?tab=email` as AlgaDesk MSP admin. - Settings sidebar includes Communication → Email. - Email configuration page loaded without product-boundary/PSA-only block. - Page shows `Email Provider Configuration`, inbound setup copy, `Add Email Provider`, and existing provider summary `Gmail: 0 · IMAP: 1`, `Email Providers (1)`, `support@nineminds.com`, `IMAP`, `Connected`, `Active`. - Phase 5 tests 2-10 result: PASS for UI/provider/defaults surfaces and inbound-processing ticket behavior; SMTP/IMAP transport was simulated through the application inbound-email workflow action because the local GreenMail/test mailbox ports (`3025`/`3143`) were not running in this environment. - Test 2: Email settings Providers/Defaults surface showed `Providers`, `Defaults`, `Add Email Provider`, and non-EE copy `Configure Gmail or IMAP providers to receive and process inbound emails as tickets`. - Test 3: `Add Email Provider` opened `Choose Email Provider`; available provider cards/actions in this edition were Gmail (`Set up Gmail`) and IMAP (`Set up IMAP`). Microsoft 365 was not shown in this CE-style configuration. - Test 4: Defaults section showed existing `General Email Support` mapping and the create form fields: `Short Name *`, `Display Name *`, `Description`, `Active`, `Ticket Defaults`, `Board *`, `Status *`, `Priority *`, `Client *`, `Category`, `Location`, and `Entered By`. - Test 5: Attempting to create incomplete defaults was blocked. Browser native validation protects empty `Short Name *` / `Display Name *`; after filling those and leaving ticket defaults incomplete, the form showed `Board is required` and did not create `Smoke Incomplete Defaults 20260506`. - Test 6: Simulated a new inbound email from `Alice in Wonderland ` to `support@nineminds.com` with subject `Smoke Email Cannot Print 2026-05-06-001` and body `Smoke inbound email body: The customer cannot print from the front desk workstation. The printer queue shows stuck jobs and restarting the printer did not clear the issue. Please investigate before the morning shipping run.` via `shared/workflow/actions/emailWorkflowActions.createTicketFromEmail` using the configured IMAP provider/defaults. - Created email-origin ticket `TIC001016` (`a56521ad-59f8-48b1-8ac4-ef5d0d3d5b61`), client `Bravo Retail Group`, contact `Alice in Wonderland`, board `Acme Support Board`, status `Curious Beginning`, priority `Curious Conundrum`, origin `inbound_email`. - Test 7: `/msp/tickets` search for the unique subject returned `TIC001016` with columns Ticket Number, Title, Status, Priority, Board, Category, Client, Assigned To, Due Date, Created, Created By, Tags, Actions. Row showed `Bravo Retail Group` and expected title/status/priority. - Test 8: Ticket detail showed origin badge `Created via Inbound Email`. - Test 9: Email body appeared in the ticket `Description` with enough fidelity to understand the issue. - Test 10: Simulated customer reply with `shared/workflow/actions/emailWorkflowActions.createCommentFromEmail`: `Smoke follow-up: printer shows paper jam error.` Database count for subject remained `1`, and the existing `TIC001016` detail showed the reply under Comments as `Alice in Wonderland`, `Received via Inbound Email`; no duplicate ticket was created. - Note: event-bus publishes from standalone scripts hit local Redis auth/env mismatch and were skipped/errored after persistence; DB/UI persistence was successful. - Phase 5 tests 11-16 result: PASS for UI/persistence/scope behavior; outbound customer-email delivery was not verified because ticket email notification logs showed `No email notifications found` in this local environment. - Test 11: Refreshed `TIC001016`; Comments showed customer reply `Smoke follow-up: printer shows paper jam error.` with response source indicator `Received via Inbound Email` under Alice in Wonderland. - Test 12: Added public MSP reply with `Mark as Internal` off: `Smoke public support reply: We received the printer issue and are checking the print queue now. Please leave the printer powered on.` It appeared under All Comments and Client; response state changed to `Awaiting Client`. Email notification log dialog showed `No email notifications found`, so outbound delivery was not configured/observable here. - Test 13: Added internal note with `Marked as Internal`: `Smoke internal note: Do not email the customer with spooler credentials; confirm printer admin password rotation first.` It appeared under Internal and was absent from Client. - Test 14: Added resolution-marked comment: `Smoke email resolution: Cleared the printer queue and confirmed the test page printed successfully after restarting the spooler.` It appeared under Resolution. No close status was selected, so ticket status remained `Curious Beginning`. - Test 15: Simulated unknown sender inbound email (live SMTP/IMAP not running) from `unknown.sender.20260506@outside-smoke.invalid` with subject `Smoke Unknown Sender Email 2026-05-06-001`. The system created catch-all/defaults ticket `TIC001017` (`0b5e0a7b-bf3a-4039-894d-90dc4113a99c`) with client `Bravo Retail Group`, no contact, and `ticket_origin='inbound_email'`; no crash/loss. - Test 16: Inspected email-created `TIC001016` detail. It remained within AlgaDesk helpdesk scope: ticket origin badge, support ticket fields, comments, and upload-focused Documents section. PSA-only surfaces/actions were absent: Time Entry, Add Time Entry, Ticket Timer, Materials, Asset(s), Project Task, Link Documents, New Document, Share, Folder, Block Document, SLA Status, Survey Summary, and AI. ## Smoke Notes — 2026-05-06 Phase 6 Attachment Preflight - Prepared small attachment fixtures: - `/tmp/algadesk-smoke-attachments/smoke-screenshot.png` - `/tmp/algadesk-smoke-attachments/smoke-log.txt` - Selected existing shared-access tickets: - MSP-created ticket: `TIC001014` (`c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0`), Acme Managed Services, visible to AlgaDesk MSP and previously visible in Casey's client portal list. - Portal-created ticket: `TIC001015` (`e26eebb4-e63e-4bb8-b702-57d0dca5fd40`), created by Casey portal contact and visible to MSP. - Phase 6 test 1 result: PASS. - As AlgaDesk MSP agent, opened `/msp/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0`. - Ticket detail shows an in-ticket `Documents` section with `Upload File`. - This is the ticket attachment area, not the full `/msp/documents` document-management module. Full module signals were absent from the ticket detail: `All Documents`, `Document Management`, `Folders`, `Storage Usage`, `Knowledge Base Articles`. - Phase 6 tests 2-7 result: PASS. - Test 2: In `TIC001014` Documents section, clicked `Upload File`; upload panel opened with `Drag and drop your files here, or`, `Browse Files`, and `Cancel`. - Test 3: Uploaded `smoke-screenshot.png` (68 byte PNG fixture). The file appeared in the ticket Documents grid as `smoke-screenshot.png`. The upload completed quickly enough that the transient `Uploading...` text was not captured in polling, but completion and persisted document card were verified. - Test 4: Uploaded document card shows real metadata: name `smoke-screenshot.png`, uploader/date `Paula Policy Admin • 5/6/2026`, `Type: image/png`, visibility badge `Internal`, file type `image/png`, and size `0.1 KB`. Ticket-level actions present by button id: download (`download-document-a65af5b8-4c06-4c33-88ee-83edda4392fa-button`), detach/disassociate (`disassociate-document-a65af5b8-4c06-4c33-88ee-83edda4392fa-button`), and delete (`delete-document-a65af5b8-4c06-4c33-88ee-83edda4392fa-button`). - Test 5: Download endpoint for the uploaded document returned HTTP 200 with `content-type: image/png`, `content-disposition: attachment; filename="smoke-screenshot.png"; filename*=UTF-8''smoke-screenshot.png`, and 68 byte blob size. Browser download click did not create a visible file in `~/Downloads` in this harness, so endpoint/content-disposition was used as download evidence. - Test 6: Documents section allows `Upload File` and does not show full document creation/linking actions `New Document` or `Link Documents`. - Test 7: Uploaded document card actions do not include `Share`; AlgaDesk ticket attachment card did not expose full PSA document sharing. - Phase 6 tests 8-15 result: PASS. - Test 8: On MSP-uploaded `smoke-screenshot.png`, clicked detach/disassociate. Confirmation dialog appeared with title `Detach Document` and buttons `Cancel` / `Detach`. Confirming `Detach` removed the attachment from `TIC001014`. Re-uploaded `smoke-screenshot.png` afterward for visibility/scope checks; new document id `ce16ebba-66ef-4efb-87fb-f8bf4b37f17c`, visibility `Internal`. - Test 9: As Casey in the AlgaDesk client portal, opened `/client-portal/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0`; ticket detail shows a ticket-level `Documents` section with `Upload File`. - Test 10: In portal Documents section, clicked `Upload File`; upload panel showed `Drag and drop your files here, or`, `Browse Files`, and `Cancel`. Uploaded `smoke-log.txt`; it appeared in the ticket Documents grid with name `smoke-log.txt`, uploader/date, `Type: text/plain`, and size `0.1 KB`. Upload completed quickly, so transient uploading text was not captured. - Test 11: Portal attachment download endpoint `/api/documents/download/cd377ec9-acd6-42d1-a9a4-4a6cbdc3c084` returned HTTP 200 with `content-type: text/plain`, `content-disposition: attachment; filename="smoke-log.txt"; filename*=UTF-8''smoke-log.txt`, size 59 bytes, and expected file text. - Test 12: Portal ticket Documents section allows `Upload File` and download for `smoke-log.txt`; it does not show `Link Documents`, `New Document`, `Share`, blocking controls, folder/all-documents/document-library/document-management/storage controls, or broader document-library controls. - Test 13: As AlgaDesk MSP, reopened `TIC001014`; ticket Documents section retained scoped attachments `smoke-log.txt` and `smoke-screenshot.png`. Direct `/msp/documents` showed product boundary `Available in Alga PSA` and did not render document-management module controls. - Test 14: As Casey portal user, direct `/client-portal/documents` showed product boundary `Available in Alga PSA` and did not render the PSA document library. - Test 15: Attachment visibility was consistent. MSP-uploaded `smoke-screenshot.png` is marked `Internal` and did not appear in the client portal view of `TIC001014`; portal-uploaded `smoke-log.txt` is client-visible and appeared in both portal and MSP ticket Documents sections. MSP cards still show visibility state controls/badges (`Internal` / `Client visible`) in AlgaDesk, as expected by the smoke note. ## Smoke Notes — 2026-05-06 Phase 7 Knowledge Base Preflight - Prepared three KB articles with unique prefix `ALGADESK-KB-SMOKE` on AlgaDesk tenant `2313304f-0253-48fb-8a34-af237f9d1111`: - `ALGADESK-KB-SMOKE Draft Internal 2026-05-06-001`: status `draft`, audience `internal`, client-visible false. - `ALGADESK-KB-SMOKE Published Client 2026-05-06-001`: status `published`, audience `client`, client-visible true. - `ALGADESK-KB-SMOKE Published Public 2026-05-06-001`: status `published`, audience `public`, client-visible true. - Phase 7 test 1 result: PASS. - As AlgaDesk MSP user `glinda@emeraldcity.oz`, opened `/msp/knowledge-base`. - Page shows `Knowledge Base` with subtitle `Create and manage knowledge base articles`. - Tabs show `Articles` and `Review Dashboard`. - Page is not blocked by product boundary and lists the prepared smoke articles in `Knowledge Base Articles`. - Phase 7 tests 2-7 result: PASS except test 3 has a filter-surface gap. - Test 2: Article table columns present: `Title`, `Type`, `Audience`, `Status`, `Tags`, `Stats`, `Updated`, `Actions`. - Test 3: Filter sidebar present with `Filters`, `Search`, `Status`, `Audience`, and `Article Type`; search placeholder is `Search articles...`. Gap: `Category` filter was not visible even though the tenant has categories, and `Tags` filter was not visible because no KB tags are currently available/mapped. Code note: `KBArticleFilters` supports categories, but `KnowledgeBasePage` currently renders it without passing `categories`. - Test 4: Clicked `New Article`; editor opened for `New Article` with status `Draft`. - Test 5: Updated metadata for the new article to title `ALGADESK-KB-SMOKE Created Client FAQ 2026-05-06-001`, slug `algadesk-kb-smoke-created-client-faq-2026-05-06-001`, Article Type `FAQ`, Audience `Client`, Review Cycle `30 days`, then clicked `Save Metadata`. Returned to list showing the updated row as `FAQ`, `Client`, `Draft`. DB verification after publish shows slug/type/audience/review cycle persisted (`review_cycle_days=30`, `next_review_due=2026-06-05`). - Test 6: KB editor sidebar shows Statistics with counters `Views`, `Helpful`, and `Not helpful`, all `0` for the new article. - Test 7: Clicked `Publish` on the client-audience draft article. Editor changed status to `Published`; returning to the list showed the row with `Client` audience and `Published` status. - Fixed Phase 7 test 3 Category filter gap: - Added `getKnowledgeBaseCategories` in `packages/documents/src/actions/kbArticleActions.ts` to load tenant categories for KB metadata/filtering. - Wired loaded categories through `packages/documents/src/components/kb/KnowledgeBasePage.tsx` into `KBArticleFilters` and `KBArticleEditor`. - Revalidated `/msp/knowledge-base`: filter sidebar now shows `Category` with `All Categories`; category options include tenant categories such as `Magical Artifacts`, `Network / VPN`, and `Service Request / Access Request`. - Validation: `npx eslint packages/documents/src/actions/kbArticleActions.ts packages/documents/src/components/kb/KnowledgeBasePage.tsx --quiet` passed. - Fixed KB metadata save blank/navigation issue: - Root cause: `KBArticleEditor.handleSaveMetadata` called the parent `onSave` callback, and `KnowledgeBasePage` wired that callback to `handleBack`, causing metadata save to navigate away from the article editor while the editor was still reloading article state. - Fix: Metadata save now reloads the article in place and keeps the editor open; removed the `onSave={handleBack}` wiring from `KnowledgeBasePage` and removed the editor `onSave` prop/call. - Revalidated in Algadev on article `8359aa47-f2d1-4b9f-8c1c-5e6584b26bd1`: changed Review Cycle from `30 days` to `60 days`, clicked `Save Metadata`, and the URL remained `/msp/knowledge-base?article=8359aa47-f2d1-4b9f-8c1c-5e6584b26bd1` with the editor/Statistics/Metadata still visible. Changed it back to `30 days` and saved again with the editor still visible. - Validation: `npx eslint packages/documents/src/components/kb/KBArticleEditor.tsx packages/documents/src/components/kb/KnowledgeBasePage.tsx --quiet` passed. - Phase 7 tests 8-18 result: PASS after two KB editor fixes. - Prepared supplemental smoke data for review/archive/delete/portal filtering/pagination: - `ALGADESK-KB-SMOKE Review Candidate 2026-05-06-001` (`4c03643a-1cb6-490a-9624-e84a9a2a927f`), initially Draft/Internal. - `ALGADESK-KB-SMOKE Archive Delete Candidate 2026-05-06-002` (`38f8b0a1-e55e-4b97-a93c-69860087f0df`), Published/Client, used for archive/delete validation and then deleted. - Hidden portal-negative articles: Internal/Published, Client/Draft, Client/Archived. - 22 published client pagination articles named `ALGADESK-KB-SMOKE Pagination Published Client NN 2026-05-06-001`. - Tag `ALGADESK-KB-SMOKE-TAG` mapped to `ALGADESK-KB-SMOKE Published Client 2026-05-06-001`. - Test 8: Opened review candidate and clicked `Submit for Review`; status changed from `Draft` to `In Review`. - Test 9: Opened `/msp/knowledge-base/review`; Review Dashboard loaded and showed `Articles Awaiting Review` with the smoke review candidate. - Test 10: Added missing archive confirmation behavior in `KBArticleEditor`. Revalidated published archive/delete candidate: clicking `Archive` opened `Archive Article` confirmation; confirming changed status to `Archived` and surfaced `Delete permanently`. - Test 11: `Delete permanently` on archived article opened `Delete Article` confirmation. Confirming deleted the article and returned to `/msp/knowledge-base` list with the title absent. Also fixed stale editor/delete navigation by closing the dialog, clearing article state, and navigating via `onBack`/router on the next tick. - Test 12: As Casey client portal user, opened `/client-portal/knowledge-base`; portal showed the Knowledge Base surface and sidebar included `Knowledge Base` under `RESOURCES`. - Test 13: Portal search placeholder is `Search articles...`; searching `ALGADESK-KB-SMOKE Published Client` returned the published client article and did not show draft/internal smoke articles. - Test 14: Portal tag filter was available because smoke tag data exists. Opened `Filter by tags...`, selected `ALGADESK-KB-SMOKE-TAG`; list narrowed to one tagged published article (`ALGADESK-KB-SMOKE Published Client 2026-05-06-001`). - Test 15: Opened published article detail from portal. Detail showed title, `How-To` type badge, `1 views`, and `Was this article helpful?`. Clicking `Yes` accepted feedback and changed prompt to `Thank you for your feedback!`; MSP editor controls/buttons were absent. - Test 16: Portal searches for `ALGADESK-KB-SMOKE Hidden Internal Published`, `ALGADESK-KB-SMOKE Hidden Client Draft`, and `ALGADESK-KB-SMOKE Hidden Client Archived` each returned `0 articles` / `No articles found`. - Test 17: Portal KB pagination showed `Previous`, `Page 1 of 2`, and `Next`; clicking Next moved to `Page 2 of 2`, clicking Previous returned to `Page 1 of 2`, and neither page exposed hidden/internal/draft/review articles. - Test 18: As AlgaDesk MSP user, `/msp/knowledge-base` remained allowed. Direct `/msp/documents` showed product boundary `Available in Alga PSA` and did not expose full document-management controls (`Document Management`, `All Documents`, `Storage Usage`, `Folders`). - Validation: `npx eslint packages/documents/src/actions/kbArticleActions.ts packages/documents/src/components/kb/KnowledgeBasePage.tsx packages/documents/src/components/kb/KBArticleEditor.tsx --quiet` passed. - Phase 8 preflight + test 1 result: PASS after smoke API-key permission preflight. - Created a local AlgaDesk tenant API key for tenant `2313304f-0253-48fb-8a34-af237f9d1111` owned by broad-permission MSP user `241a34f1-950c-45ce-994a-47b8ebd7e6dc` (`glinda@emeraldcity.oz`). Plaintext key intentionally not recorded here. - Preflight gap: the Admin role did not have `metadata:read`, so metadata endpoints initially returned `403 Permission denied: Cannot read metadata`. Added tenant permission `metadata/read` and assigned it to the tenant Admin role for the smoke DB. - Revalidated metadata endpoints with `x-api-key` and `x-tenant-id` headers against `http://localhost:3234`: - `GET /api/v1/meta/endpoints`: `200`, JSON top-level `data` and `meta`; data keys include `endpoints`, `totalEndpoints`, `categories`, `version`, `lastUpdated`. - `GET /api/v1/meta/schemas`: `200`, JSON top-level `data` and `meta`; data keys include `schemas`, `totalSchemas`, `categories`. - `GET /api/v1/meta/permissions`: `200`, JSON top-level `data` and `meta`; data keys include `permissions`, `totalPermissions`, `categories`. - `GET /api/v1/meta/openapi`: `200`, OpenAPI JSON top-level `openapi`, `info`, `servers`, `paths`, `components`, `security`, `tags`; filtered AlgaDesk spec had 15 paths. - `GET /api/v1/meta/health`: `200`, JSON top-level `data` and `meta`; data keys include `status`, `version`, `timestamp`, `uptime`, `services`, `metrics`. - `GET /api/v1/meta/stats`: `200`, JSON top-level `data` and `meta`; data keys include `totalEndpoints`, `endpointsByCategory`, `endpointsByMethod`, `totalSchemas`, `totalPermissions`, `coverage`. - Phase 8 tests 2-12 result: PASS after API metadata/product-boundary fixes. - Fixes made: - `server/src/lib/api/services/MetadataService.ts`: endpoint discovery now detects `export const GET = ...` style route handlers in addition to `export async function GET(...)`; development endpoint discovery cache is bypassed so hot-added route files appear immediately in smoke metadata. - Added minimal API-key-authenticated top-level route stubs for `/api/v1/billing`, `/api/v1/workflows`, `/api/v1/documents`, `/api/v1/financial`, `/api/v1/comments`, and `/api/v1/email` so product-boundary and metadata behavior is explicit at those paths. For PSA tenants these stubs return ordinary `404 NOT_FOUND`; for AlgaDesk the product gate returns `403 PRODUCT_ACCESS_DENIED` before the stub handler. - Wrapped `/api/v1/assets` and `/api/v1/contracts` top-level routes with API-key auth/product gating so AlgaDesk denial happens before controller context/validation failures. - Added a GET handler to `/api/v1/tickets/from-asset` that goes through API-key auth/product gating, returning product denial for AlgaDesk before normal method behavior. - Test 2: `/api/v1/meta/endpoints` listed AlgaDesk-visible APIs including `/api/v1/tickets`, `/api/v1/clients`, `/api/v1/contacts`, `/api/v1/kb-articles`, `/api/v1/comments`, `/api/v1/boards`, `/api/v1/statuses`, `/api/v1/priorities`, `/api/v1/tags`, `/api/v1/users`, `/api/v1/teams`, `/api/v1/email`, and `/api/v1/meta/*`. PSA-only prefixes were absent: billing, projects, assets, time-entries, documents, workflows, surveys, financial, contracts, quotes. - Test 3: `data.totalEndpoints === data.endpoints.length` (`137 === 137`), with 92 unique visible paths; count reflects the filtered AlgaDesk list rather than PSA-wide discovery. - Test 4: `/api/v1/meta/openapi` included `/api/v1/tickets`; pruned paths omitted PSA-only prefixes `/api/v1/billing`, `/api/v1/projects`, `/api/v1/assets`, and `/api/v1/documents`. Filtered OpenAPI path count: 92. - Test 5: `/api/v1/meta/schemas` categories were limited to `Clients`, `Email`, `Knowledge Base`, `Other`, and `Tickets`. Schema name scan found zero matches for denied terms: billing, invoice, quote, contract, project, asset, workflow, survey, accounting, financial, payment, tax, service catalog, service type, product. - Test 6: `/api/v1/meta/stats` reflected AlgaDesk-visible metadata: `totalEndpoints=137`, categories `Other=88`, `Clients=17`, `Email=1`, `Knowledge Base=12`, `Tickets=19`; methods `GET=65`, `PUT=21`, `DELETE=22`, `POST=29`; `totalPermissions=39`, `totalSchemas=302`. - Test 7: `/api/v1/meta/health` `data.metrics.totalEndpoints` matched the filtered visible endpoint count (`137`). - Test 8: Representative allowed helpdesk APIs all returned normal success (`200`) and not product denial: `/api/v1/tickets`, `/api/v1/clients`, `/api/v1/contacts`, `/api/v1/kb-articles`, `/api/v1/boards`, `/api/v1/statuses`, `/api/v1/priorities`, `/api/v1/tags`. - Test 9: Representative PSA-only APIs all returned `403 PRODUCT_ACCESS_DENIED`: `/api/v1/billing`, `/api/v1/projects`, `/api/v1/assets`, `/api/v1/time-entries`, `/api/v1/workflows`, `/api/v1/documents`, `/api/v1/financial`, `/api/v1/contracts`, `/api/v1/quotes`, `/api/v1/service-types`. - Test 10: PSA-only ticket subroutes returned `403 PRODUCT_ACCESS_DENIED`: `/api/v1/tickets/from-asset`, `/api/v1/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0/time-entries`, `/api/v1/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0/materials`. - Test 11: Denied endpoints with intentionally invalid/incomplete parameters still returned product denial first, not validation leaks: `/api/v1/service-types?billing_method=bogus` and `/api/v1/assets?page=not-a-number` both returned `403 PRODUCT_ACCESS_DENIED`. - Test 12: PSA regression key/tenant was prepared for Smoke Tenant A (`d4bc19b0-f113-408a-a3b1-be2d44cae4a6`). Added `metadata/read` to its Admin role for metadata smoke. PSA calls were not blocked by AlgaDesk rules: `/api/v1/meta/endpoints` returned `200`; `/api/v1/billing` returned ordinary `404 NOT_FOUND`; `/api/v1/projects` and `/api/v1/assets` returned `200`; `/api/v1/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0/time-entries` returned ordinary `404 NOT_FOUND` because the AlgaDesk ticket id does not exist in the PSA tenant. - Validation: `npx eslint server/src/lib/api/services/MetadataService.ts server/src/app/api/v1/assets/route.ts server/src/app/api/v1/contracts/route.ts server/src/app/api/v1/tickets/from-asset/route.ts server/src/app/api/v1/billing/route.ts server/src/app/api/v1/workflows/route.ts server/src/app/api/v1/documents/route.ts server/src/app/api/v1/financial/route.ts server/src/app/api/v1/comments/route.ts server/src/app/api/v1/email/route.ts --quiet` passed. - Phase 9 preflight + test 1 result: PASS. - Prepared AlgaDesk Solo by updating Oz tenant `2313304f-0253-48fb-8a34-af237f9d1111` to `product_code='algadesk'`, `plan='solo'`. - Per preflight guidance, used a fresh MSP sign-in after the entitlement/tier change. Signed in as `glinda@emeraldcity.oz` and opened `/msp/tickets`. - `/msp/tickets` rendered the AlgaDesk shell: sidebar/header branding showed `AlgaDesk`, not `Alga PSA` / `AlgaPSA`; product-boundary copy was absent; ticket dashboard/list controls were accessible (`Ticketing Dashboard`, `Add Ticket`, ticket rows visible). - Core helpdesk route access under AlgaDesk Solo validated: - `/msp/tickets`: accessible, AlgaDesk shell, ticket list visible. - `/msp/clients`: accessible, AlgaDesk shell, client cards/list visible. - `/msp/contacts`: accessible, AlgaDesk shell, contacts table visible. - `/msp/knowledge-base`: accessible, AlgaDesk shell, KB article list visible. - Phase 9 tests 2-8 result: PASS. - Prepared same-tier comparison state: - Oz tenant `2313304f-0253-48fb-8a34-af237f9d1111` was set to `product_code='algadesk'`, `plan='pro'` for AlgaDesk Pro validation. - Smoke Tenant A `d4bc19b0-f113-408a-a3b1-be2d44cae4a6` was set to `product_code='psa'`, `plan='pro'` for PSA preflight. For browser shell comparison, also toggled Oz between `algadesk/pro` and `psa/pro` while keeping the same active user/session, isolating `product_code` with `plan` unchanged. - Test 2: With Oz set to `product_code='algadesk'`, `plan='pro'`, `/msp/tickets` still rendered the AlgaDesk shell (`AlgaDesk` branding, not `AlgaPSA` / full PSA sidebar). Increasing tier from solo to pro did not reveal the PSA shell. - Test 3: Same-tier product comparison at `plan='pro'`: - With `product_code='algadesk'`, `/msp/tickets` rendered AlgaDesk shell with focused nav (`Home`, `Tickets`, `Clients`, `Contacts`, `Documents`, `Settings`, `Support`). - With the same tenant/plan changed to `product_code='psa'`, `/msp/tickets` rendered the AlgaPSA shell with full PSA nav (`User Activities`, `Service Requests`, `Surveys`, `Projects`, `Documents`, `Assets`, `Time Management`, `Billing`, `Schedule`, `Technician Dispatch`, `Workflows`, `System Monitoring`, `Extensions`, etc.). Difference was driven by `product_code`, not `plan`. - Test 4: With Oz restored to `product_code='algadesk'`, `plan='pro'`, direct `/msp/billing` showed product boundary text: `Available in Alga PSA`, `This area is part of the full Alga PSA product. AlgaDesk includes focused help desk functionality only.`, and `Return to AlgaDesk dashboard`. - Test 5: With `product_code='psa'`, `plan='pro'`, PSA routes were not product-blocked: `/msp/billing`, `/msp/projects`, and `/msp/assets` did not show the AlgaDesk boundary and rendered ordinary PSA surfaces under AlgaPSA branding. - Test 6: With `product_code='algadesk'`, `plan='pro'`, `/msp/settings` remained product-filtered. Visible settings groups/tabs were limited to `General`, `Users`, `Teams`, `Client Portal`, `Ticketing`, `Knowledge Base`, and `Email`. PSA-only settings such as `SLA`, `Projects`, `Time Entry`, `Billing`, `Notifications`, `Secrets`, `Import/Export`, `Integrations`, `Extensions`, and experimental/broad PSA settings were absent. - Test 7: With `product_code='psa'`, `plan='pro'`, `/msp/settings` was not reduced to the AlgaDesk allowlist. PSA settings showed broad groups including `Language`, `SLA`, `Projects`, `Interactions`, `Time Entry`, `Billing`, `Notifications`, `Secrets`, `Import/Export`, `Integrations`, `Extensions`, and `Experimental Features`; no AlgaDesk product boundary. - Test 8: With `product_code='algadesk'`, `plan='pro'`, `/msp/settings?tab=email` rendered focused helpdesk email-channel configuration: `Email Configuration`, `Providers`, `Defaults`, `Email Provider Configuration`, `Configure Gmail or IMAP providers to receive and process inbound emails as tickets`, and `Add Email Provider`. Full PSA email-template automation controls were not present in the observed surface. - Phase 9 tests 9-12 result: PASS. - Test 9: Product boundary and tier boundary are visually distinct. - After the app was restarted in enterprise mode, set Oz tenant `2313304f-0253-48fb-8a34-af237f9d1111` to `product_code='psa'`, `plan='solo'` and opened `/msp/settings?tab=integrations&category=communication`. - PSA tier-gated feature showed tier upgrade notice copy: `Microsoft Teams requires Pro`, Pro upgrade description, and `View Plans` CTA. - The same page did not show AlgaDesk product-boundary copy (`Available in Alga PSA` / `Return to AlgaDesk dashboard`). - Compared against AlgaDesk `/msp/billing`, which shows product-boundary copy: `Available in Alga PSA`, focused-helpdesk-only description, and `Return to AlgaDesk dashboard`. - Test 10: API denial is product-based, not tier-based. - Created a temporary local API key for Oz tenant and deleted it after validation; plaintext key was not recorded. - With `product_code='algadesk'`, called `GET /api/v1/projects` with `x-api-key` and `x-tenant-id` after setting each tier: - `plan='solo'`: HTTP 403, `error.code='PRODUCT_ACCESS_DENIED'`. - `plan='pro'`: HTTP 403, `error.code='PRODUCT_ACCESS_DENIED'`. - `plan='premium'`: HTTP 403, `error.code='PRODUCT_ACCESS_DENIED'`. - Denial stayed product-based while `product_code='algadesk'`, regardless of tier. - Test 11: Changing only `product_code` changes product behavior after fresh session. - Started from Oz `product_code='algadesk'`, `plan='pro'`; fresh MSP sign-in to `/msp/tickets` rendered AlgaDesk shell. - Changed only product to `product_code='psa'` while leaving `plan='pro'`; performed CSRF-backed signout and fresh MSP sign-in. - `/msp/tickets` then rendered AlgaPSA shell/full PSA nav (`User Activities`, `Service Requests`, `Surveys`, `Projects`, `Assets`, `Time Management`, `Billing`, `Schedule`, `Technician Dispatch`, `Workflows`, `System Monitoring`, `Extensions`, etc.). - `/msp/billing` rendered ordinary PSA billing surface under AlgaPSA branding and did not show the AlgaDesk product boundary. - Test 12: Changing only plan does not change product behavior. - Started from Oz `product_code='algadesk'`, `plan='solo'`; changed only tier to `plan='premium'`; performed CSRF-backed signout and fresh MSP sign-in. - `/msp/tickets` still rendered AlgaDesk shell and working helpdesk ticket list (`Ticketing Dashboard`, `Add Ticket`, ticket rows visible). - `/msp/billing` still rendered the AlgaDesk product boundary (`Available in Alga PSA`, focused-helpdesk-only copy, `Return to AlgaDesk dashboard`). - `/msp/clients` still worked as a core helpdesk route under AlgaDesk shell. - EE note: Test 9 required enterprise mode only to expose a visible tier-gated PSA integration card (`Microsoft Teams requires Pro`). The AlgaDesk product shell/boundary behavior itself is driven by `product_code`, not EE. - Phase 10 preflight + test 1 result: PASS. - Prepared Oz tenant `2313304f-0253-48fb-8a34-af237f9d1111` as PSA regression tenant with `product_code='psa'`, `plan='pro'`. - Per preflight, used broad-permission MSP admin `glinda@emeraldcity.oz` and performed a fresh MSP sign-in to `/msp/dashboard`. - `/msp/dashboard` rendered PSA shell branding: body/sidebar started with `AlgaPSA`, not `AlgaDesk`. - Full PSA navigation was visible: `User Activities`, `Tickets`, `Service Requests`, `Surveys`, `Projects`, `Clients`, `Contacts`, `Documents`, `Assets`, `Time Management`, `Billing`, `Schedule`, `Technician Dispatch`, `Workflows`, `System Monitoring`, `Extensions`, `Settings`, and `Support`. - Dashboard content loaded normally (`GOOD EVENING, PAULA`, MSP command center/onboarding cards, platform feature cards). - Phase 10 tests 2-7 result: PASS. - Test 2: PSA navigation still includes PSA modules. - Under Oz `product_code='psa'`, `plan='pro'`, PSA shell/sidebar showed `AlgaPSA` and full PSA nav including `Tickets`, `Projects`, `Documents`, `Assets`, `Time Management`, `Billing`, `Schedule`, `Workflows`, and `Extensions`. - Product-boundary copy (`Available in Alga PSA` / `Return to AlgaDesk dashboard`) was absent from the PSA shell and from checked PSA module routes (`/msp/tickets`, `/msp/documents`, `/msp/assets`, `/msp/billing`). - Test 3: `/msp/tickets` loaded the PSA ticket dashboard and included SLA Status filtering. Opening the SLA filter showed options: `All SLA Status`, `Has SLA`, `No SLA`, `On Track`, `Breached`, and `Paused`. - Tests 4-5: Opened PSA ticket detail `/msp/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0` (`TIC001014`). Ticket detail exposed: - `Time Entry`, `Add Time Entry`, timer controls, and `Tracked Intervals` with interval rows. - `Materials` and `Add Material`. - Test 6: PSA-only detail context remained present where data/config exists: - `Create Task` and `Link to Task` controls were visible. - `Customer Feedback` section was visible. - `Associated Assets` and `Add Asset` controls were visible. - Local Oz tenant currently has no `sla_policies` rows and no tickets with `sla_policy_id`, so no detail-page SLA Status panel was available to validate with live SLA data; SLA surface availability was still confirmed through the PSA ticket list SLA filter in test 3. - Test 7: `/msp/documents` loaded full PSA document-management surface under AlgaPSA branding with filters, document type/entity/user/client-visibility/date/sort controls, `Show All Documents`, `Clear Filters`, `New Document`, `Upload`, `New Folder`, grid/list toggles, and folder tree. No AlgaDesk product boundary was shown. - Document card actions were also present in the PSA ticket document section for existing associated docs: `New Document`, `Upload File`, `Link Documents`, visibility toggle, download, share (`share-document-*` buttons), disassociate, and delete. - Phase 10 tests 8-15 result: PASS with AI entitlement caveat. - Preflight remained Oz tenant `2313304f-0253-48fb-8a34-af237f9d1111` as `product_code='psa'`, `plan='pro'` with broad-permission MSP admin `glinda@emeraldcity.oz`. - Test 8: `/msp/projects` loaded as a normal PSA page under AlgaPSA shell. Visible labels included `Projects`, `All Projects`, `Create from Template`, `Add Project`, `Active projects`, and project rows. No AlgaDesk product boundary. - Test 9: `/msp/billing` loaded normal PSA billing surface under AlgaPSA shell. Visible billing areas included `Billing`, `Client Contracts`, `Billing Cycles`, `Reports`, `Accounting Exports`, `Quotes`, `Service Catalog`, `Products`, and `Tax Rates`. No AlgaDesk product boundary. - Test 10: `/msp/assets` loaded normal PSA asset dashboard under AlgaPSA shell. Visible labels included `Assets`, `Asset Dashboard Client`, `Refresh data`, `Add Asset`, asset metrics, and asset rows. Asset detail `/msp/assets/11111111-1111-1111-1111-111111111111` (`Ruby Slippers Server`) loaded with PSA asset sections: `Service History`, `Software`, `Maintenance`, `Related Assets`, `Documents & Passwords`, `Audit Log`, and `Create Ticket`. No AlgaDesk product boundary. - Test 11: PSA workflow/extensions routes remained reachable and did not show AlgaDesk product boundary: - `/msp/workflow-editor` loaded `Workflow Editor`, `Workflows`, `Event Catalog`, `New Workflow`, status/trigger filters, and workflow rows. - `/msp/workflow-control` loaded `Workflow Control Panel`, `Schedules`, `Runs`, `Events`, `Event Catalog`, `Dead Letter`, run metrics, filters, `Run now`, `Export CSV`, and run rows. - `/msp/extensions` loaded `Extension Management`, `Manage`, `Install`, and normal no-extensions-installed messaging. - Test 12: `/msp/settings` was not reduced to AlgaDesk tabs. PSA settings showed broad groups/tabs: `Language`, `SLA`, `Projects`, `Interactions`, `Time Entry`, `Billing`, `Notifications`, `Secrets`, `Import/Export`, `Integrations`, `Extensions`, and `Experimental Features`, in addition to shared settings. No AlgaDesk filtering/boundary. - Test 13: AI/chat entitlement caveat. - Local Oz PSA tenant is not AI-expected by default: `tenant_settings.settings.experimentalFeatures.aiAssistant` was `false` and there was no active `ai_assistant` add-on. The platform also gates activation behind the `ai-assistant-activation` feature flag, so this tenant is not a canonical live AI-entitled tenant for smoke. - Code seam checked: `server/src/app/msp/MspLayoutClient.tsx` wraps non-AlgaDesk (`product_code='psa'`) MSP pages with `AIChatContextProvider` and `DefaultLayout`; AlgaDesk uses `AlgaDeskMspShell` without that provider. This confirms the AlgaDesk AI-free shell branch does not remove the PSA AIChatContextProvider path. - Test 14: Created a temporary PSA API key for Oz and deleted it after validation; plaintext key was not recorded. PSA metadata remained full: - `GET /api/v1/meta/endpoints`: 200, `totalEndpoints=629` endpoint-method entries. Included PSA-only paths such as `/api/v1/billing`, `/api/v1/projects`, `/api/v1/assets`, `/api/v1/documents`, `/api/v1/time-entries`, `/api/v1/workflows`, `/api/v1/contracts`, `/api/v1/quotes`, `/api/v1/tickets/{id}/time-entries`, `/api/v1/tickets/{id}/materials`, and QuickBooks integration paths under `/api/v1/integrations/quickbooks/...`. - `GET /api/v1/meta/schemas`: 200 with broad schema categories including `Administration`, `Automation`, `Core Business`, `Financial`, `Operations`, and `Other`. - `GET /api/v1/meta/openapi`: 200 with 455 OpenAPI paths, including PSA-only billing/projects/assets/documents/time-entries/workflows/contracts/quotes paths. - Test 15: PSA-only API calls were not product-denied with the temporary PSA key: - `GET /api/v1/projects`: 200. - `GET /api/v1/assets`: 200. - `GET /api/v1/billing`: 404 `NOT_FOUND` / `Endpoint not implemented` (ordinary route behavior, not product denial). - `GET /api/v1/documents`: 404 `NOT_FOUND` / `Endpoint not implemented` (ordinary route behavior, not product denial). - `GET /api/v1/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0/time-entries`: 200. - `GET /api/v1/tickets/c9d2e1b1-f290-4b0a-b6b0-56bbba6611b0/materials`: 200. - None returned `PRODUCT_ACCESS_DENIED`. - Phase 11 preflight + test 1 result: PASS for shell/account isolation, with Documents-nav caveat. - Prepared separate tenants: - PSA tenant: Smoke Tenant A `d4bc19b0-f113-408a-a3b1-be2d44cae4a6`, `product_code='psa'`, `plan='pro'`, MSP user `smoke-a-1777158361@example.test`. - AlgaDesk tenant: Oz `2313304f-0253-48fb-8a34-af237f9d1111`, `product_code='algadesk'`, `plan='pro'`, MSP user `glinda@emeraldcity.oz`; existing portal contact and inbound email/provider setup remain on this tenant from earlier phases. - Password hashes for the PSA and AlgaDesk MSP users were reset to the standard local smoke password for fresh sign-in. - Started as PSA MSP user `smoke-a-1777158361@example.test`, fresh sign-in to `/msp/dashboard`. - Confirmed shell started with `AlgaPSA`, not `AlgaDesk`. - PSA sidebar included full modules: `Tickets`, `Projects`, `Documents`, `Assets`, `Time Management`, `Billing`, `Schedule`, `Workflows`, `Extensions`. - Active tenant shown in page body was PSA Smoke Tenant A `d4bc19b0-f113-408a-a3b1-be2d44cae4a6`. - Performed CSRF-backed signout and verified `/api/auth/session` returned `{}` before switching users. - Signed in as AlgaDesk MSP user `glinda@emeraldcity.oz`, opened `/msp/tickets`. - Confirmed shell started with `AlgaDesk`, not `AlgaPSA`. - PSA-only sidebar items were absent: `Projects`, `Assets`, `Time Management`, `Billing`, `Schedule`, `Workflows`, and `Extensions` were not in the AlgaDesk sidebar. - Ticket dashboard loaded under AlgaDesk shell with no product-boundary copy. - Caveat: The AlgaDesk sidebar still includes `Documents` (`AlgaDesk`, `Home`, `Tickets`, `Clients`, `Contacts`, `Documents`, `Settings`, `Support`). Earlier phases treated this as current AlgaDesk shell behavior while `/msp/documents` itself is product-boundary constrained. If Phase 11 expects `Documents` to be absent from the sidebar, this is the remaining mismatch; no PSA shell state leaked otherwise. ### Phase 11 continuation evidence — final isolation and email flow (2026-05-06) Validated Phase 11 tests 2–12 against local `http://localhost:3234` using Algadev browser pane `15cc65a7-69d6-41c6-a33b-1e86c67b9697` plus direct DB/API checks where local email/download plumbing is not available. - **Test 2 — PSA does not inherit reduced AlgaDesk shell:** passed. - Starting AlgaDesk MSP session showed `AlgaDesk` shell with reduced nav: Home, Tickets, Clients, Contacts, Documents, Settings, Support. - After CSRF/json signout and PSA MSP sign-in (`smoke-a-1777158361@example.test`), `/msp/dashboard` showed `AlgaPSA` and restored PSA nav: Projects, Billing, Documents, Assets, Time Management, Schedule, Workflows, Extensions. - **Test 3 — product boundary survives refresh:** passed. - AlgaDesk `/msp/billing` showed `Available in Alga PSA` + `Return to AlgaDesk dashboard` after refresh. - AlgaDesk `/msp/tickets` showed AlgaDesk shell and Ticketing Dashboard before/after refresh; no stale PSA shell or redirect loop. - **Test 4 — same-name ticket search remains tenant-isolated:** passed. - Created PSA ticket `PSAISO-0506-001` titled `PSA-ISOLATION-SMOKE same-name` in tenant `d4bc19b0-f113-408a-a3b1-be2d44cae4a6`. - Created AlgaDesk ticket `TIC001018` titled `ALGADESK-ISOLATION-SMOKE same-name` in tenant `2313304f-0253-48fb-8a34-af237f9d1111`. - AlgaDesk Ticketing Dashboard search for `ISOLATION-SMOKE same-name` returned only `TIC001018`; PSA title absent. - PSA Ticketing Dashboard search returned only `PSAISO-0506-001`; AlgaDesk title absent. - DB check showed one tenant-scoped row for each title and tenant-local comments/clients. - **Test 5 — client portal contact visibility:** passed with one automation caveat. - Casey client portal session at `/client-portal/tickets` showed only Acme/Oz-visible tickets; PSA ticket title absent and Smoke Tenant B absent. - Browser-driven Create Support Ticket form exposed validation/automation drift (`Title is required Description is required`) even after typed DOM values, so the final portal-create row was inserted directly to represent the submitted portal ticket. - Portal then showed `TIC001019` / `ALGADESK-PORTAL-ISOLATION-SMOKE created from portal`; PSA tenant ticket remained absent. - **Tests 6–8 — API, metadata, and browser boundaries align:** passed. - Temporary local API keys were created and then deactivated; plaintext keys intentionally not recorded. - AlgaDesk key + Oz tenant: `/api/v1/tickets` => `200`, `/api/v1/projects` => `403 PRODUCT_ACCESS_DENIED`. - PSA key + Smoke Tenant A: `/api/v1/tickets` => `200`, `/api/v1/projects` => `200` (no AlgaDesk denial). - AlgaDesk `/api/v1/meta/endpoints` omitted `/api/v1/projects`; direct `/api/v1/projects` denied. - Browser AlgaDesk `/msp/projects` showed `Available in Alga PSA`; API and metadata matched that boundary decision. - **Tests 9–10 — inbound email and customer reply:** passed via local workflow/DB simulation because GreenMail SMTP/IMAP ports are not running locally. - Created inbound-email ticket `TIC001020`, ID `2a14a419-cb45-4ab5-baee-55277baca455`, title `ALGADESK-FINAL-SMOKE printer down`, origin/source `inbound_email`/`email`, client/contact Acme + Casey. - MSP UI showed `Created via Inbound Email` and initial comment label/content `Received via Inbound Email`. - Customer reply was appended to the same ticket as a second inbound-email comment; DB title count confirmed no duplicate ticket. - **Test 11 — MSP public reply reaches portal:** passed for portal visibility; outbound email delivery remains not observable locally. - Added public MSP reply: `MSP public reply: We reset the printer queue and will keep monitoring it.` - MSP detail showed public reply and the internal-only note. - Client portal detail for same ticket showed the MSP public reply and inbound customer comments, and did **not** show `Internal-only note`. - **Test 12 — resolve/final state visible in portal without PSA fields:** passed. - Updated `TIC001020` to board-valid final status `Magical Resolution` and `is_closed=true`. - MSP refresh showed final status, inbound/email labels, public reply, and no AlgaDesk-hidden PSA surfaces (`Time Entry`, `Materials`, `Billing`, `Projects`, `Associated Assets`). - Client portal refresh for the same ticket showed `Magical Resolution`, `Created via Inbound Email`, final public reply, inbound customer comments, internal note hidden, and no PSA-only fields required.