# Presence & Real-Time Updates Integration Guide

> **Version:** 1.0 (Phase 3 Complete)
> **Last Updated:** 2026-02-05

## Overview

The Presence system provides real-time awareness of who is viewing or editing a record. This helps prevent conflicts before they happen (Soft Locking) and enhances collaboration. It is backed by Redis for high performance and uses a heartbeat mechanism.

## Architecture

*   **Backend:** Laravel API (`PresenceController`) + Redis Service (`RecordPresenceService`).
*   **Frontend:** Alpine.js Heartbeat Component + "Who's Viewing" UI.
*   **Storage:** Redis (Ephemeral keys with 90s TTL).
*   **Scope:** Multi-tenant aware (Tenant ID isolation).

---

## Backend API

### 1. Send Heartbeat

Reports current user status (viewing/editing). Should be called every **30 seconds**.

*   **Endpoint:** `POST /api/v1/presence/{type}/{id}/heartbeat`
*   **Headers:** `X-SX-Tenant: {tenant_id}`
*   **Payload:**
    ```json
    {
      "tab_id": "uuid-v4-string",
      "mode": "view" | "edit",
      "dirty": boolean // true if user has unsaved changes
    }
    ```

### 2. Get Viewers

Retrieves a list of other users currently viewing the record.

*   **Endpoint:** `GET /api/v1/presence/{type}/{id}/viewers`
*   **Headers:** `X-SX-Tenant: {tenant_id}`
*   **Response:**
    ```json
    {
      "success": true,
      "data": {
        "viewers": [
          {
            "id": 123,
            "user_name": "Alice Smith",
            "tabs": 1,
            "has_dirty_tab": true, // Warn: User is editing!
            "mode": "edit"
          }
        ],
        "current_user_id": 456
      }
    }
    ```

### 3. Unregister

Removes a specific tab's presence (e.g., on `beforeunload`).

*   **Endpoint:** `DELETE /api/v1/presence/{type}/{id}/unregister`
*   **Payload:**
    ```json
    {
      "tab_id": "uuid-v4-string"
    }
    ```

---

## Frontend Integration

### Alpine.js Component (`presence-heartbeat`)

This logic automatically handles the heartbeat loop and visibility state.

```javascript
/* Usage in Blade */
<div x-data="presenceHeartbeat({
    recordType: 'project',
    recordId: '{{ $project->id }}',
    initialMode: 'view'
})">
    <!-- Viewers UI -->
</div>
```

**Features:**
*   Generates unique `tab_id` on load.
*   Pings heartbeat every 30s.
*   Detects `dirty` state (integrates with form changes).
*   Handles `beforeunload` to clean up.
*   Stops polling if tab is hidden (visibility API).

### Service Logic (`RecordPresenceService.php`)

*   **Keys:** `tenant_{id}_record_presence:{type}:{id}`
*   **Structure:** Redis Hash
    *   **Field:** `{tab_id}`
    *   **Value:** JSON encoded metadata
*   **TTL:** 90 seconds (auto-expires if heartbeat stops).
*   **Conflict Logic:** If a user has *any* tab marked as `dirty`, they are shown as "Editing".

---

## Testing

*   **Unit Tests:** `tests/Unit/Services/RecordPresenceServiceTest.php` (Mocked Redis).
*   **Integration:** `tests/Integration/Services/RecordPresenceServiceIntegrationTest.php` (InMemory Redis).
*   **Feature:** `tests/Feature/Http/Controllers/Api/v1/PresenceControllerTest.php`.

## Troubleshooting

*   **Viewers list empty:** Ensure Redis is running and `tab_id` is persistent across pings.
*   **"Editing" status stuck:** Wait 90s for key expiration or verify `unregister` call on close.
*   **Cross-Tenant Leak:** Check `X-SX-Tenant` header and `tenant_{id}` key prefix.
