> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# CE 65 pipeline tile customization dashboard embeds INTEGRATION

# CE-65: Pipeline Tile Customization & Dashboard Embeds — Integration Contract

**Status:** ✅ Implemented\
**Spec:** [CE-65](../../../specs/ce/specs/CE-65-pipeline-tile-customization-dashboard-embeds.md)\
**Last Updated:** 2026-05-16

***

## Summary

CE-65 ships three independent surfaces, all built as **CE-owned consumers**
with strict tenant scoping:

1. **Pipeline tile field configuration** — org-level settings stored in
   `ce_module_settings.tile_field_config` (jsonb), normalized in
   `src/cores/ce/types/tile-config.ts`, and rendered by `LeadCardMetadata`
   inside `LeadCard`.
2. **Scheduled leads panel** — collapsible embed on `/ce/leads` that lists
   open CE leads with a future `expected_admission_date`.
3. **Bed Board embed** — read-only aggregated bed availability sourced from
   Recovery Housing (RH) through the **Platform Integration Layer**
   (`@/platform/rh`). Per constitution §1.3 Pattern 1, CE never imports
   from `@/cores/rh/*`.
4. **Insurance field masking + audit** — `insurance_member_id` is masked by
   default; reveals/edits are gated by permissions and recorded in
   `ce_insurance_access_audit` (append-only, no cleartext PHI).

No new cross-core domain events are introduced. All reads are synchronous
React Query calls with `organization_id` filters.

***

## Pattern

| Pattern         | Detail                                                                                                    |
| --------------- | --------------------------------------------------------------------------------------------------------- |
| Constitution    | §1.3 Pattern 1 — Platform Integration Layer                                                               |
| Consumers       | CE (`/ce/leads` pipeline, `/ce/settings`, `/ce/contacts/:id`)                                             |
| Provider data   | RH bed inventory / occupancy (RH-owned `rh_beds`)                                                         |
| CE-owned tables | `ce_module_settings` (tile config), `ce_leads`, `ce_contacts`, `ce_insurance_access_audit`                |
| Forbidden       | Direct imports from `@/cores/rh/*`; writing cleartext `insurance_member_id` into audit / logs / telemetry |

***

## Surface 1 — Pipeline tile field configuration

**Storage:** `public.ce_module_settings` (one row per `organization_id`),
column `tile_field_config jsonb` — an array of `TileFieldKey` strings.

**Canonical types:** `src/cores/ce/types/tile-config.ts`

* `TILE_FIELD_KEYS` (12 keys): `stage`, `assignee`, `next_action`,
  `scheduled_appointment`, `urgency`, `requested_services`, `source`,
  `partner`, `expected_admission_date`, `insurance_carrier`,
  `last_activity_at`, `days_in_stage`.
* `TILE_FIELD_DEFAULTS` (4 keys, used when settings row is empty/missing):
  `stage`, `assignee`, `next_action`, `scheduled_appointment`.
* `TILE_MAX_ROWS = 4` (FR-1.4); overflow ordered by `TILE_FIELD_PRIORITY`
  (FR-1.5, deterministic regardless of save order).
* `normalizeTileFieldConfig(raw)` — drops unknown values, dedupes, falls
  back to defaults when empty.
* `resolveTileFields(configured)` → `{ visible, overflow }`.

**Hook:** `useTileFieldConfig()` (`src/cores/ce/hooks/useTileFieldConfig.ts`)

| Aspect               | Detail                                                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Query key            | `['ce-tile-field-config', organizationId]`                                                                                                             |
| Read                 | `ce_module_settings.select('tile_field_config').eq('organization_id', orgId).maybeSingle()`                                                            |
| Write                | UPSERT pattern: existing row → `update({ tile_field_config, updated_by })`; missing row → `insert({ organization_id, tile_field_config, created_by })` |
| Cache                | `staleTime: 5 * 60_000`, `gcTime: 10 * 60_000`                                                                                                         |
| Invalidation on save | `['ce-tile-field-config', orgId]` and `['ce-leads']` (so pipeline metadata refreshes)                                                                  |
| Permission gate      | Settings card (`TileFieldConfigCard`) requires `ce.settings.pipeline.configure`; hook itself is read-only callable                                     |

***

## Surface 2 — Scheduled leads panel

**Hook:** `useScheduledLeads({ organizationId, windowDays = 30, limit = 25 })`
(`src/cores/ce/hooks/useScheduledLeads.ts`)

| Aspect        | Detail                                                                                                                                              |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Query key     | `['ce-leads', 'scheduled', organizationId, { windowDays, limit }]`                                                                                  |
| Source        | `ce_leads` with PostgREST embeds `ce_contacts!ce_leads_contact_id_fkey` and `ce_lead_stages!ce_leads_lead_stage_id_fkey`                            |
| Filters       | `organization_id = orgId`, `status = 'open'`, `expected_admission_date IS NOT NULL`, `expected_admission_date BETWEEN today AND today + windowDays` |
| Order / limit | `expected_admission_date ASC`, `limit(25)` default                                                                                                  |
| Cache         | `staleTime: 5 * 60_000`, `gcTime: 10 * 60_000`                                                                                                      |
| UI            | `ScheduledLeadsPanel` on `/ce/leads`; collapse state persisted to session storage with an always-visible Restore control (FR-2.3, no dead-end)      |

***

## Surface 3 — Bed Board embed (RH platform adapter)

**Hook:** `useBedBoardSummary({ organizationId, residenceId?, enabled? })`
exported from `@/platform/rh` (`src/platform/rh/useBedBoardSummary.ts` →
re-exported by `src/platform/rh/index.ts`).

| Aspect       | Detail                                                                                                                         |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| Query key    | `['platform-rh', 'bed-board-summary', organizationId, residenceId ?? null]`                                                    |
| Source       | `rh_beds.select('status').eq('organization_id', orgId).is('deleted_at', null)` (optionally `.eq('residence_id', residenceId)`) |
| Cache        | `staleTime: 5 * 60_000`, `gcTime: 10 * 60_000`                                                                                 |
| Tenant scope | Enforced both by the `organization_id` filter above and by RLS on `rh_beds`                                                    |

**Returned contract (`BedBoardSummary`):**

| Field                | Type   | Notes                                             |
| -------------------- | ------ | ------------------------------------------------- |
| `total`              | number | Count of non-deleted beds in scope                |
| `counts.available`   | number | `status = 'available'`                            |
| `counts.occupied`    | number | `status = 'occupied'`                             |
| `counts.reserved`    | number | `status = 'reserved'`                             |
| `counts.maintenance` | number | `status = 'maintenance'`                          |
| `counts.offline`     | number | `status IN ('offline', 'out_of_service')`         |
| `counts.other`       | number | Any other / unrecognized status value             |
| `availabilityRate`   | number | `counts.available / total` (0 when `total === 0`) |

**Consumer:** `src/cores/ce/components/BedBoardEmbed.tsx`, rendered on
`/ce/leads` under the kanban grid alongside `ScheduledLeadsPanel`.

**Permission gate:** `ce.pipeline.view_bed_board` via `useHasPermission`.
When denied, the embed renders nothing — no toast, no skeleton, no error.

***

## Surface 4 — Insurance masking + audit

**Masking utility:** `src/cores/ce/utils/insurance-mask.ts`

* `maskInsuranceMemberId(value)` — always masks; reveals at most the last
  4 characters (CAC-005). Strings of length ≤ 4 are fully masked.
* `resolveInsuranceMemberId(value, canViewDetail)` — returns
  `{ display, isMasked }`; `null`/`undefined` always → `""`.

**Hook:** `useInsuranceMasking()` (`src/cores/ce/hooks/useInsuranceMasking.ts`)

| Return           | Meaning                                                 |
| ---------------- | ------------------------------------------------------- |
| `canViewDetail`  | `useHasPermission('ce.contacts.insurance.view_detail')` |
| `canEdit`        | `useHasPermission('ce.contacts.insurance.edit')`        |
| `resolve(value)` | Permission-aware display resolver                       |
| `mask(value)`    | Force-mask helper for any log/telemetry path            |

**Audit hook:** `useInsuranceRevealAudit()`
(`src/cores/ce/hooks/useInsuranceRevealAudit.ts`)

| Method                 | Action value written    |
| ---------------------- | ----------------------- |
| `logReveal(contactId)` | `reveal_member_id`      |
| `logEdit(contactId)`   | `edit_insurance_fields` |

**Audit table:** `public.ce_insurance_access_audit` (append-only)

| Column            | Type        | Notes                                         |
| ----------------- | ----------- | --------------------------------------------- |
| `organization_id` | uuid        | RLS scope (`pf_has_org_access`)               |
| `contact_id`      | uuid        | FK to `ce_contacts`                           |
| `actor_user_id`   | uuid        | Authenticated user                            |
| `action`          | text        | `reveal_member_id` \| `edit_insurance_fields` |
| `occurred_at`     | timestamptz | Default `now()`                               |

**Compliance invariants** (regression = blocker):

* The audit row MUST NOT contain `insurance_member_id` or any other
  cleartext PHI (CAC-006).
* INSERT and SELECT are allowed for org members with the relevant
  permission; UPDATE and DELETE are denied (`USING (false)`).
* No insurance member identifier appears in toasts, error messages,
  analytics events, or telemetry payloads (FR-4.5).

***

## Permissions

| Key                                 | Gate                                                                                  |
| ----------------------------------- | ------------------------------------------------------------------------------------- |
| `ce.settings.pipeline.configure`    | `TileFieldConfigCard` on `/ce/settings`                                               |
| `ce.pipeline.view_bed_board`        | `BedBoardEmbed` on `/ce/leads`; fail-closed (renders nothing) when absent             |
| `ce.contacts.insurance.view_detail` | "Reveal" control for masked Member ID; required to receive plaintext from `resolve()` |
| `ce.contacts.insurance.edit`        | Edit insurance carrier + member ID                                                    |
| RH / platform                       | `rh_beds` RLS enforces tenant scoping for `useBedBoardSummary`                        |

***

## Events

**None** for CE-65 MVP. Bed counts and scheduled leads are fetched for
display only. CE-65 MUST NOT publish insurance identifiers or unmasked PII
in any optional analytics hooks.

***

## Testing expectations

* **Unit:** `tests/unit/ce/insurance-masking.test.ts`,
  `tests/unit/ce/pipeline-tile-field-config.test.ts` (15 tests covering
  mask reveal-length, null handling, normalization, FR-1.4 cap, FR-1.5
  deterministic priority).
* **Integration:** `tests/integration/ce/pipeline-scheduled-panel.test.ts`,
  `tests/integration/ce/pipeline-bed-board-embed.test.ts`,
  `tests/integration/ce/insurance-permissions.test.ts` (permission-denied
  vs empty vs error states; T031).
* **RLS:** `tests/rls/ce/ce-65-insurance-audit.rls.test.ts` — append-only
  invariant on `ce_insurance_access_audit` and cross-org denial (T050).
* **E2E:** `tests/e2e/ce/ce65-pipeline-tile-and-embeds.spec.ts`,
  `tests/e2e/ce/ce65-insurance-masking-permissions.spec.ts` (T051/T052,
  self-skip when seeded permissions are absent).

***

## Related documents

* [PLATFORM\_INTEGRATION\_LAYERS.md](./PLATFORM_INTEGRATION_LAYERS.md)
* [CROSS\_CORE\_INTEGRATIONS.md](./CROSS_CORE_INTEGRATIONS.md) (CE-65 row)
* [CE-65 spec](../../../specs/ce/specs/CE-65-pipeline-tile-customization-dashboard-embeds.md)
* [CE-65 CONTEXT](../../../specs/ce/specs/CE-65-CONTEXT.md)
* [CE-65 compliance sign-off](../../../specs/reviews/CE-65-COMPLIANCE-SIGNOFF.md)
* [Pipeline tile user guide](../../ce/pipeline-tile-customization-user-guide.md)
* [Pipeline settings admin guide](../../ce/pipeline-settings-admin-guide.md)
