# Gantt Real-Time Broadcast Architecture

## Table of Contents

- [Overview](#overview)
- [Architecture Components](#architecture-components)
  - [1. Event Broadcasting Layer](#1-event-broadcasting-layer)
  - [2. Channel Authorization](#2-channel-authorization)
  - [3. Frontend Integration](#3-frontend-integration)
- [Multi-Tab Notification Behavior](#multi-tab-notification-behavior)
  - [Scenario 1: Same User, Multiple Tabs](#scenario-1-same-user-multiple-tabs)
  - [Scenario 2: Same User, Different Views](#scenario-2-same-user-different-views)
  - [Scenario 3: Different Users](#scenario-3-different-users)
- [Import Suppression System](#import-suppression-system)
  - [Problem](#problem)
  - [Solution: Import Flag Pattern](#solution-import-flag-pattern)
  - [Observer Implementation](#observer-implementation)
  - [Import Flow](#import-flow)
- [Error Handling & Resilience](#error-handling--resilience)
  - [Broadcast Failure Isolation](#broadcast-failure-isolation)
  - [Component Cleanup](#component-cleanup)
  - [BroadcastHelper - Invalid Socket ID Protection](#broadcasthelper---invalid-socket-id-protection)
- [Data Flow Diagrams](#data-flow-diagrams)
  - [Update Flow (User → Gantt → Broadcast → Other Users)](#update-flow-user--gantt--broadcast--other-users)
  - [Import Flow (NetSuite → Database, No Broadcast)](#import-flow-netsuite--database-no-broadcast)
- [Developer Guidelines](#developer-guidelines)
  - [Adding New Broadcast Sources](#adding-new-broadcast-sources)
  - [Testing Broadcast Functionality](#testing-broadcast-functionality)
  - [Debugging Broadcast Issues](#debugging-broadcast-issues)
  - [Performance Considerations](#performance-considerations)
- [Security Considerations](#security-considerations)
  - [1. Channel Authorization](#1-channel-authorization)
  - [2. Tenant Isolation](#2-tenant-isolation)
  - [3. Data Validation](#3-data-validation)
- [Troubleshooting Guide](#troubleshooting-guide)
  - [Issue: Users Not Receiving Broadcasts](#issue-users-not-receiving-broadcasts)
  - [Issue: Self-Notification (User Sees Own Changes)](#issue-self-notification-user-sees-own-changes)
  - [Issue: Broadcasts During Imports](#issue-broadcasts-during-imports)
  - [Issue: Ghost Notifications (0 changes detected)](#issue-ghost-notifications-0-changes-detected)
- [Configuration](#configuration)
  - [Pusher Setup](#pusher-setup)
  - [Laravel Echo Setup](#laravel-echo-setup)
- [Related Documentation](#related-documentation)
- [Future Enhancements](#future-enhancements)
  - [Potential Improvements](#potential-improvements)
  - [Scaling Considerations](#scaling-considerations)
- [Changelog](#changelog)
- [Support & Contact](#support--contact)

---

## Overview

The Gantt chart implements a real-time synchronization system that allows multiple users and browser tabs to stay synchronized when project tasks are modified. The system uses Laravel Broadcasting with Pusher to deliver instant updates while preventing notification spam and maintaining security through role-based permissions and tenant isolation.

**Key Features**:
- ✅ Real-time task updates across multiple users
- ✅ Multi-tab support with intelligent notification filtering
- ✅ Import suppression (no notifications during NetSuite imports)
- ✅ Tenant isolation and role-based security
- ✅ Resilient error handling

---

## Architecture Components

### 1. Event Broadcasting Layer

**Event**: `Domain\ProjectTasks\Events\ProjectTaskUpdated`

This event is dispatched whenever a project task, subtask, or predecessor relationship is modified.

**Event Structure**:
```php
class ProjectTaskUpdated implements ShouldBroadcastNow
{
    public $projectId;      // ID of the project
    public $taskId;         // ID of the affected task
    public $taskTitle;      // Title of the task
    public $changeType;     // 'update', 'create', 'delete'
    public $userId;         // User who made the change
    public $tabId;          // Browser tab that initiated change (nullable)
    public $tenantDatabase; // Tenant database context

    public function broadcastOn()
    {
        return new PrivateChannel("project.{$this->projectId}");
    }
}
```

**Broadcast Triggers**:
- ProjectTask model updates (when `import_flag = false`)
- Subtask model updates (broadcasts parent task change)
- ProjectTaskPredecessor CRUD operations (broadcasts both related tasks)
- Assignee CRUD operations (broadcasts parent task change)

### 2. Channel Authorization

**Channel**: `private-project.{projectId}`

**Authorization Logic** (`routes/channels.php`):
```php
Broadcast::channel('project.{projectId}', function ($user, $projectId) {
    // 1. User authentication check
    if (!$user) return false;

    // 2. Project existence check
    $project = Project::find($projectId);
    if (!$project) return false;

    // 3. Tenant isolation check
    $currentDb = config('database.connections.tenant_connection.database');
    if (!$currentDb) return false;

    // 4. Role-based permission check
    if (!$user->hasPermissionForRecordType('read', 'projecttask')) {
        return false;
    }

    return true;
});
```

**Security Layers**:
1. **Authentication**: Only logged-in users
2. **Project Existence**: Project must exist in database
3. **Tenant Isolation**: User must be on correct tenant
4. **Role Permissions**: User must have 'read' permission for 'projecttask' record type

### 3. Frontend Integration

**Technology**: Laravel Echo + Alpine.js

**Location**: `resources/views/livewire/form-builder/show.blade.php`

**Key Components**:

#### Tab Identification
```javascript
// Generate unique tab ID on component initialization
this.tabId = 'tab_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
```

#### Channel Subscription
```javascript
// Subscribe to private channel with authorization
this.echoChannel = window.Echo.private(`project.${this.projectId}`)
    .listen('.task.updated', (event) => {
        self.handleExternalTaskUpdate(event);
    });
```

#### Event Handling
```javascript
handleExternalTaskUpdate(event) {
    // Filter out events from same tab
    if (event.tab_id && event.tab_id === this.tabId) {
        return; // Ignore own changes
    }

    // Accumulate changes
    this.externalChangesCount++;

    // Throttle UI updates (5 second delay)
    clearTimeout(this.externalChangesThrottle);
    this.externalChangesThrottle = setTimeout(() => {
        if (this.externalChangesCount > 0) {
            this.hasExternalChanges = true; // Show overlay
        }
    }, 5000);
}
```

#### Sending Updates
```javascript
async saveAllChanges() {
    const socketId = window.Echo?.socketId();
    const batchData = {
        _token: csrfToken,
        project_id: this.projectId,
        changes: changes,
        tab_id: this.tabId // Include for tab filtering
    };

    const headers = {
        'X-CSRF-TOKEN': csrfToken,
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    };

    // Include socket ID for Laravel's ->toOthers()
    if (socketId) {
        headers['X-Socket-ID'] = socketId;
    }

    await fetch(this.updateUrl, {
        method: 'POST',
        headers: headers,
        body: JSON.stringify(batchData)
    });
}
```

---

## Multi-Tab Notification Behavior

The system implements sophisticated multi-tab filtering to prevent self-notification spam:

### Scenario 1: Same User, Multiple Tabs
**Setup**: User has Gantt open in Tab A and Tab B

**Action**: User modifies task in Tab A

**Result**:
- ✅ Tab A: No notification (filtered by tab_id)
- ✅ Tab B: Notification appears (different tab_id)

**Implementation**:
- Each tab generates unique `tabId` on load
- Backend includes `tabId` in broadcast event
- Frontend filters events matching own `tabId`

### Scenario 2: Same User, Different Views
**Setup**: User has Gantt open in Tab A, task detail page in Tab B

**Action**: User modifies subtask in Tab B

**Result**:
- ✅ Tab A: Notification appears (subtask changes have `null` tab_id)
- ✅ Tab B: Standard page behavior

**Implementation**:
- Subtask/Predecessor changes don't include `tabId`
- All Gantt instances receive notification

### Scenario 3: Different Users
**Setup**: User A and User B both have Gantt open

**Action**: User A modifies task

**Result**:
- ✅ User A's tabs: Filtered (same socket/tab)
- ✅ User B's tabs: Notification appears

**Implementation**:
- Laravel's `->toOthers()` uses `X-Socket-ID` header
- Different users have different socket IDs

---

## Import Suppression System

### Problem
NetSuite imports can update hundreds of tasks. Without suppression, users would be bombarded with notifications.

### Solution: Import Flag Pattern

**Trait**: `App\Traits\HasImportFlag`

```php
trait HasImportFlag
{
    protected bool $importFlag = false;

    public function setImportFlag(bool $flag): void
    {
        $this->importFlag = $flag;
    }

    public function isImport(): bool
    {
        return $this->importFlag;
    }
}
```

**Models Using Trait**:
- `ProjectTask`
- `Subtask`
- `ProjectTaskPredecessor`
- `Assignee`

### Observer Implementation

**ProjectTask** (`src/Domain/ProjectTasks/Models/ProjectTask.php`):
```php
static::updated(function ($model) {
    // Skip broadcast if import flag is set
    if ($model->alreadyBroadcast ||
        (method_exists($model, 'isImport') && $model->isImport())) {
        return;
    }

    // Only broadcast if task has project
    if ($model->project) {
        // ... broadcast logic
    }
});
```

**Subtask** (`src/Domain/Subtasks/Models/Subtask.php`):
```php
static::updated(function ($model) {
    // Skip if import flag is set
    if (method_exists($model, 'isImport') && $model->isImport()) {
        return;
    }

    // Broadcast parent task change
    // ...
});
```

**ProjectTaskPredecessor** (`src/Domain/ProjectTaskPredecessors/Models/ProjectTaskPredecessor.php`):
```php
static::created(function ($model) {
    if (method_exists($model, 'isImport') && $model->isImport()) {
        return;
    }
    self::broadcastPredecessorChange($model);
});

static::updated(function ($model) {
    if (method_exists($model, 'isImport') && $model->isImport()) {
        return;
    }
    self::broadcastPredecessorChange($model);
});

static::deleted(function ($model) {
    if (method_exists($model, 'isImport') && $model->isImport()) {
        return;
    }
    self::broadcastPredecessorChange($model);
});
```

**Assignee** (`src/Domain/Assignees/Models/Assignee.php`):
```php
static::created(function ($model) {
    if (method_exists($model, 'getImportFlag') && $model->getImportFlag()) {
        return;
    }
    self::broadcastAssigneeChange($model, 'created');
});

static::updated(function ($model) {
    if (method_exists($model, 'getImportFlag') && $model->getImportFlag()) {
        return;
    }
    self::broadcastAssigneeChange($model, 'updated');
});

static::deleted(function ($model) {
    if (method_exists($model, 'getImportFlag') && $model->getImportFlag()) {
        return;
    }
    self::broadcastAssigneeChange($model, 'deleted');
});
```

### Import Flow

**Single Record Import**:
```php
// In CreateProjectTask::create()
$task = new ProjectTask($data);
$task->setImportFlag(true);  // Set flag
$task->save();               // Observer checks flag, skips broadcast
```

**Batch Import**:
```php
// ProjectTaskBatchUpsertService uses raw SQL
// Bypasses Eloquent events entirely - no broadcasts
DB::connection('tenant_connection')->table('projecttasks')->insert($records);
```

---

## Error Handling & Resilience

### Broadcast Failure Isolation

All model observers implement try-catch blocks to prevent broadcast failures from affecting data operations:

```php
try {
    // Broadcast logic
    broadcast(new ProjectTaskUpdated(...))->toOthers();
} catch (\Exception $e) {
    // Log but don't fail the update
    Log::warning('Failed to broadcast project task update', [
        'task_id' => $model->id,
        'project' => $projectId ?? null,
        'error' => $e->getMessage(),
        'trace' => $e->getTraceAsString()
    ]);
}
```

**Guarantees**:
- ✅ Task updates always succeed
- ✅ Broadcast failures are logged
- ✅ No user-facing errors from broadcast issues

### Component Cleanup

The Alpine.js component implements cleanup on navigation:

```javascript
destroy() {
    // Clean up throttle timer when component is destroyed
    if (this.externalChangesThrottle) {
        clearTimeout(this.externalChangesThrottle);
        this.externalChangesThrottle = null;
    }
}
```

**Prevents**:
- Memory leaks from orphaned timers
- Ghost notifications after navigation
- Race conditions on component reload

### BroadcastHelper - Invalid Socket ID Protection

**Problem**: The `->toOthers()` method requires a valid socket ID from Laravel Echo. When changes are made via CLI, background jobs, or APIs without Echo, Pusher throws "Invalid socket ID undefined" errors.

**Solution**: The `BroadcastHelper` class (`app/Helpers/BroadcastHelper.php`) validates socket IDs before applying `->toOthers()`:

```php
<?php

namespace App\Helpers;

use Illuminate\Broadcasting\PendingBroadcast;

class BroadcastHelper
{
    /**
     * Check if a valid socket ID is present in the current request
     *
     * A valid socket ID should:
     * - Not be null or empty
     * - Not be the string "undefined" or "null"
     * - Follow the format: number.number (e.g., "870568.3802693")
     */
    public static function hasValidSocketId(): bool
    {
        $socketId = request()->header('X-Socket-ID');

        if (!$socketId) {
            return false;
        }

        if ($socketId === 'undefined' || $socketId === 'null') {
            return false;
        }

        if (!preg_match('/^\d+\.\d+$/', $socketId)) {
            return false;
        }

        return true;
    }

    /**
     * Conditionally apply ->toOthers() only if valid socket ID present
     *
     * Usage: BroadcastHelper::toOthers(broadcast(new MyEvent()));
     */
    public static function toOthers(PendingBroadcast $broadcast): PendingBroadcast
    {
        if (self::hasValidSocketId()) {
            return $broadcast->toOthers();
        }

        return $broadcast;
    }
}
```

**Usage in Model Observers**:

```php
// Before (causes errors when no socket ID):
broadcast(new ProjectTaskUpdated(...))->toOthers();

// After (gracefully handles missing socket ID):
BroadcastHelper::toOthers(
    broadcast(new ProjectTaskUpdated(...))
);
```

**Frontend Socket ID Validation**:

The frontend also validates socket IDs before sending them:

```javascript
// Only include socket ID if it's valid (not undefined, null, or invalid format)
if (socketId && typeof socketId === 'string' &&
    socketId !== 'undefined' && socketId !== 'null' &&
    /^\d+\.\d+$/.test(socketId)) {
    headers['X-Socket-ID'] = socketId;
}
```

**Behavior**:
- ✅ **With valid socket ID** (browser + Echo): Uses `->toOthers()`, excludes originating connection
- ✅ **Without socket ID** (CLI/API/jobs): Broadcasts to all subscribers, no exclusion
- ✅ **No errors**: Gracefully handles both contexts

---

## Data Flow Diagrams

### Update Flow (User → Gantt → Broadcast → Other Users)

```
┌──────────────────────────────────────────────────────────────────┐
│ User A modifies task in Gantt (Tab 1)                          │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Frontend: saveAllChanges()                                       │
│ - Captures tab_id                                               │
│ - Includes X-Socket-ID header                                   │
│ - POSTs to GanttController                                      │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Backend: GanttController::batchUpdate()                         │
│ - Receives tab_id                                               │
│ - Sets $task->broadcastTabId = $tabId                          │
│ - Saves task                                                     │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Model Observer: ProjectTask::updated                            │
│ - Checks import flag (false)                                    │
│ - Dispatches ProjectTaskUpdated event                           │
│ - Includes tab_id in event                                      │
│ - Uses ->toOthers() (excludes sender's socket)                  │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Laravel Broadcasting → Pusher → Laravel Echo                    │
└────────┬───────────────────────┬─────────────────────────────────┘
         │                       │
         ▼                       ▼
┌─────────────────┐     ┌─────────────────┐
│ User A (Tab 1)  │     │ User A (Tab 2)  │
│ Filters:        │     │ Receives:       │
│ tab_id matches  │     │ tab_id differs  │
│ ✗ No notify    │     │ ✓ Show overlay  │
└─────────────────┘     └─────────────────┘

         ┌─────────────────┐
         │ User B (Tab 1)  │
         │ Receives:       │
         │ Different user  │
         │ ✓ Show overlay  │
         └─────────────────┘
```

### Import Flow (NetSuite → Database, No Broadcast)

```
┌──────────────────────────────────────────────────────────────────┐
│ NetSuite Import Job                                              │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ ProjectTaskBatchUpsertService::handle()                         │
│ - Uses raw SQL: DB::connection()->table()->insert()            │
│ - Bypasses Eloquent entirely                                    │
│ - No model events fired                                         │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Database: Tasks inserted/updated                                │
│ - synced = true                                                 │
│ - No observers triggered                                        │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Result: No broadcasts sent                                      │
│ Users see no notifications during import                        │
└──────────────────────────────────────────────────────────────────┘
```

---

## Developer Guidelines

### Adding New Broadcast Sources

If you need to broadcast task changes from a new source (e.g., new action, API endpoint):

**1. For Single Record Operations**:
```php
// In your action/service
$task = ProjectTask::find($id);
$task->setImportFlag(false);  // Ensure broadcasts are enabled
$task->broadcastTabId = $tabId ?? null;  // Include tab ID if available
$task->save();  // Observer handles broadcast
```

**2. For Import Operations**:
```php
// In your import service
$task = new ProjectTask($data);
$task->setImportFlag(true);  // Suppress broadcast
$task->save();
```

**3. For Batch Operations**:
```php
// Use raw SQL to bypass observers entirely
DB::connection('tenant_connection')
    ->table('projecttasks')
    ->insert($records);  // No broadcasts
```

### Testing Broadcast Functionality

**Unit Tests**: `tests/Unit/Models/`
- `ProjectTaskBroadcastTest.php` - Import suppression tests
- `SubtaskBroadcastTest.php` - Subtask broadcast tests
- `ProjectTaskPredecessorBroadcastTest.php` - Predecessor broadcast tests

**Integration Tests**: Manual verification required
- Multi-tab behavior
- Multi-user synchronization
- Channel authorization with permissions

**Run Tests**:
```bash
./vendor/bin/pest tests/Unit/Models/ProjectTaskBroadcastTest.php --no-coverage
```

### Debugging Broadcast Issues

**1. Check if broadcasts are being sent**:
```bash
# Monitor Pusher debug console
# https://dashboard.pusher.com → Your App → Debug Console
```

**2. Check if user can subscribe to channel**:
```javascript
// In browser console
Echo.private('project.1').subscription.state
// Should be: 'subscribed'

// If failed, check authorization
// Network tab: Look for POST to /broadcasting/auth
// Should return 200, not 403
```

**3. Check import flag**:
```php
// In Tinker or debug
$task = ProjectTask::find(1);
$task->isImport();  // Should be false for manual updates
```

**4. Check logs for broadcast failures**:
```bash
tail -f storage/logs/laravel.log | grep "Failed to broadcast"
```

### Performance Considerations

**Broadcast Volume**:
- One broadcast per task update
- Subtask updates trigger parent task broadcast
- Predecessor changes trigger broadcasts for both tasks

**Optimization Tips**:
1. Batch updates when possible (uses raw SQL, no broadcasts)
2. Set import flag for bulk operations
3. Monitor Pusher message volume (Pusher dashboard)

**Expected Volume**:
- Normal usage: <100 broadcasts/day per project
- Import operations: 0 broadcasts (suppressed)
- High activity: 1000+ broadcasts/day (acceptable)

---

## Security Considerations

### 1. Channel Authorization

**Risk**: Unauthorized users viewing task updates
**Mitigation**:
- Private channel requires authentication
- Role-based permission check (`hasPermissionForRecordType`)
- Tenant database isolation

**Test**:
```php
// User without 'read' permission for projecttask
$user = User::find(1);
$user->hasPermissionForRecordType('read', 'projecttask'); // false

// Attempting to subscribe returns 403 Forbidden
```

### 2. Tenant Isolation

**Risk**: Cross-tenant data leakage
**Mitigation**:
- Authorization checks tenant database context
- Project lookup scoped to tenant connection
- All broadcasts include tenant_database in payload

**Verification**:
```php
// Authorization callback
$currentDb = config('database.connections.tenant_connection.database');
if (!$currentDb) return false;  // Reject if no tenant context
```

### 3. Data Validation

**Risk**: Malicious broadcast data
**Mitigation**:
- Events use type-hinted properties
- Frontend validates event structure
- Only authenticated users can broadcast

**Best Practice**:
```javascript
// Frontend validation
if (!event.project_id || !event.task_id) {
    console.warn('Invalid event structure', event);
    return;
}
```

---

## Troubleshooting Guide

### Issue: Users Not Receiving Broadcasts

**Possible Causes**:
1. User not subscribed to channel (permission issue)
2. Pusher credentials not configured
3. Laravel Echo not initialized
4. Import flag incorrectly set

**Resolution**:
```javascript
// 1. Check subscription
console.log(window.Echo.connector.pusher.connection.state);
// Should be: 'connected'

// 2. Check channel subscription
console.log(this.echoChannel.subscription.state);
// Should be: 'subscribed'

// 3. Check if events are firing
window.Echo.private('project.1').listen('.task.updated', (e) => {
    console.log('Event received:', e);
});
```

### Issue: Self-Notification (User Sees Own Changes)

**Possible Causes**:
1. Tab ID not included in request
2. X-Socket-ID header missing
3. Frontend filtering not working

**Resolution**:
```javascript
// Check if tab ID is set
console.log('My tab ID:', this.tabId);

// Check if socket ID is being sent
const socketId = window.Echo?.socketId();
console.log('Socket ID:', socketId);

// Verify filtering logic
handleExternalTaskUpdate(event) {
    console.log('Event tab_id:', event.tab_id, 'My tab_id:', this.tabId);
    // Should filter if they match
}
```

### Issue: Broadcasts During Imports

**Possible Causes**:
1. Import flag not set
2. Using Eloquent instead of raw SQL for batch operations
3. Observer not checking import flag

**Resolution**:
```php
// Verify import flag is set
$task = new ProjectTask($data);
$task->setImportFlag(true);
Log::info('Import flag:', ['is_import' => $task->isImport()]);
$task->save();

// For batch operations, use raw SQL
DB::connection('tenant_connection')->table('projecttasks')->insert($records);
```

### Issue: Ghost Notifications (0 changes detected)

**Possible Causes**:
1. Throttle timer not cleared
2. Race condition between dismiss and throttle

**Resolution**:
- Already fixed in codebase
- Throttle is cleared in `loadExternalChanges()` and `dismissExternalChanges()`
- Check added: `if (this.externalChangesCount > 0)` before showing overlay

---

## Configuration

### Pusher Setup

**File**: `.env`
```env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_app_id
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_APP_CLUSTER=your_cluster
```

**File**: `config/broadcasting.php`
```php
'pusher' => [
    'driver' => 'pusher',
    'key' => env('PUSHER_APP_KEY'),
    'secret' => env('PUSHER_APP_SECRET'),
    'app_id' => env('PUSHER_APP_ID'),
    'options' => [
        'cluster' => env('PUSHER_APP_CLUSTER'),
        'encrypted' => true,
    ],
],
```

### Laravel Echo Setup

**File**: `resources/js/bootstrap.js`
```javascript
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
    forceTLS: true,
    encrypted: true,
    authEndpoint: '/broadcasting/auth',
    auth: {
        headers: {
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
        }
    }
});
```

---

## Related Documentation

- **Pull Request**: [gantt-broadcast-pr.md](./gantt-broadcast-pr.md) - Comprehensive PR description
- **Test Suite**: `tests/Unit/Models/*BroadcastTest.php` - Unit tests for broadcast functionality
- **Test Suite**: `tests/Unit/Broadcasting/ProjectChannelAuthorizationTest.php` - Channel authorization tests

---

## Future Enhancements

### Potential Improvements

1. **Optimistic UI Updates**: Update local Gantt immediately, rollback on server error
2. **Conflict Resolution**: Handle simultaneous edits by multiple users
3. **Bandwidth Optimization**: Send only changed fields, not full task data
4. **Offline Support**: Queue changes when connection is lost
5. **Read Receipts**: Show which users have loaded the latest changes

### Scaling Considerations

**Current Limits**:
- Pusher free tier: 200k messages/day
- Channel limit: 100 concurrent connections
- Message size: 10KB per message

**If Scaling Required**:
1. Upgrade Pusher plan
2. Consider Redis broadcasting (self-hosted)
3. Implement message batching
4. Add broadcast throttling for high-frequency updates

---

## Changelog

| Date | Author | Change |
|------|--------|--------|
| 2025-11-06 | AI Agent | Initial implementation with private channel security |
| 2025-11-06 | AI Agent | Added import suppression system (ProjectTask, Subtask, Predecessor) |
| 2025-11-06 | AI Agent | Fixed duplicate broadcasts from subtask updates |
| 2025-11-06 | AI Agent | Added tab-specific filtering to prevent self-notification |
| 2025-11-06 | AI Agent | Fixed ghost notification issue (0 changes detected) |
| 2025-11-06 | AI Agent | Fixed channel type mismatch (public → private) |
| 2025-11-06 | AI Agent | Added Assignee model broadcasting |
| 2025-11-06 | AI Agent | Added comprehensive test suite (15 tests, 100% passing) |
| 2025-11-06 | AI Agent | Removed debug logs for production |
| 2025-11-06 | AI Agent | Created architecture documentation and PR |
| 2025-11-07 | AI Agent | Fixed "Invalid socket ID undefined" error with BroadcastHelper |

---

## Support & Contact

For questions or issues related to the Gantt broadcast system:
1. Check logs: `storage/logs/laravel.log`
2. Review test suite: `tests/Unit/Models/*BroadcastTest.php`
3. Consult related documentation (links above)
4. Monitor Pusher dashboard for broadcast metrics

