# Digital Signage Integration Map

> **Last Updated**: December 2025
> **Related**: [Overview](./digital-signage-overview.md)

## Integration Architecture

```
┌──────────────────────────────────────────────────────────────────┐
│ ADMIN PANEL (Web UI)                                            │
├──────────────────────────────────────────────────────────────────┤
│ UploadController → Upload images/videos                         │
│ LoopController → Manage playlist & scheduling                   │
│ SlideController → CRUD store slides                             │
│ CorpUploadController → Corporate slides                         │
└──────────────────────────────────────────────────────────────────┘
         ↓                          ↓                    ↓
    [dsSlides]             [digitalSignSchedule]  [corpSlides]
         ↓                          ↓                    ↓
┌──────────────────────────────────────────────────────────────────┐
│ GLOBAL COORDINATION LAYER                                       │
├──────────────────────────────────────────────────────────────────┤
│ SlideScheduleController (Cron/Background)                        │
│   - Detects schedule activation/expiration                       │
│   - Publishes Ably refresh messages                              │
└──────────────────────────────────────────────────────────────────┘
         ↓
    [Ably Channel: {typeNum}]  ←─── Real-time push
         ↓
┌──────────────────────────────────────────────────────────────────┐
│ DISPLAY DEVICES (Client-Side)                                   │
├──────────────────────────────────────────────────────────────────┤
│ loop.html + JavaScript                                          │
│   - Renders StoreLoop via Twig template                          │
│   - Manages Cycle.js slideshow                                   │
│   - Listens to Ably for refresh                                  │
│   - Polls queue JSON endpoints                                   │
└──────────────────────────────────────────────────────────────────┘
         ↓
    Queue Display Integration (type 3 slides)
         ↓
    AJAX Calls:
    - GET /{typeNum}/json → Current queue
    - GET /{typeNum}/jsonWait → Wait time
    - GET /{typeNum}/jsonCompleted → Completed customers

┌──────────────────────────────────────────────────────────────────┐
│ SYNC APP (Desktop/Offline)                                      │
├──────────────────────────────────────────────────────────────────┤
│ DownloadDigitalSignSyncApp                                       │
│   GET: Returns XML (version + filename)                          │
│   POST: Streams binary config file (API key auth)                │
└──────────────────────────────────────────────────────────────────┘
```

---

## Database Schema

### Global Database (`kiosk_buykiosk`)

**corpSlides** - Corporate-wide slides
```sql
CREATE TABLE corpSlides (
    id INT PRIMARY KEY AUTO_INCREMENT,
    slideName VARCHAR(255),
    fileName VARCHAR(255),
    enabled TINYINT(1) DEFAULT 1,
    type TINYINT(1),           -- 0=image, 1=video
    embed TEXT,
    videoDuration INT,
    storeType VARCHAR(10),      -- Filter: ou, pa, wi, etc.
    dateAdded DATETIME,
    plays INT DEFAULT 0
);
```

**hbSlides** - Hipbone/partner slides
```sql
CREATE TABLE hbSlides (
    id INT PRIMARY KEY AUTO_INCREMENT,
    slideName VARCHAR(255),
    fileName VARCHAR(255),
    enabled TINYINT(1) DEFAULT 1,
    type TINYINT(1),
    embed TEXT,
    videoDuration INT,
    storeType VARCHAR(10),
    dateAdded DATETIME
);
```

**digitalSignSchedule** - Global scheduling tracker
```sql
CREATE TABLE digitalSignSchedule (
    id INT PRIMARY KEY AUTO_INCREMENT,
    startDate DATETIME,
    expireDate DATETIME,
    loopID INT,                 -- References dsLoop.id in store DB
    typeNum VARCHAR(10),        -- Store identifier
    started TINYINT(1),         -- NULL=not yet, 1=activated
    finished TINYINT(1) DEFAULT 0
);
```

### Per-Store Database (`kiosk_{typeNum}`)

**dsSlides** - Store-specific slides
```sql
CREATE TABLE dsSlides (
    id INT PRIMARY KEY AUTO_INCREMENT,
    slideName VARCHAR(255),
    fileName VARCHAR(255),
    enabled TINYINT(1) DEFAULT 1,
    type TINYINT(1),           -- 0=image, 1=video
    embed TEXT,
    videoDuration INT,
    dateAdded DATETIME,
    plays INT DEFAULT 0
);
```

**dsLoop** - Slide display loop/playlist
```sql
CREATE TABLE dsLoop (
    id INT PRIMARY KEY AUTO_INCREMENT,
    slideID INT,               -- References slide in appropriate table
    position INT,              -- Order in loop (0-based)
    duration INT,              -- Display time in seconds
    animation VARCHAR(50) DEFAULT 'fadeIn',
    slideUploader TINYINT(1),  -- 0=store, 1=corp, 2=hipbone
    scheduled TINYINT(1) DEFAULT 0,
    startDate DATETIME,
    expireDate DATETIME,
    embed TEXT,
    videoDuration INT
);
```

**DigitalSignSync** - Sync app version tracking
```sql
CREATE TABLE DigitalSignSync (
    id INT PRIMARY KEY AUTO_INCREMENT,
    filename VARCHAR(255),
    version VARCHAR(50),
    date DATETIME
);
```

---

## API Endpoints

### Management Routes (`routes/groups/digitalsign.php`)

| Method | Endpoint | Controller | Purpose |
|--------|----------|------------|---------|
| POST | `/:typeNum/upload` | UploadController::upload() | Upload media file |
| POST | `/:typeNum/loop/` | LoopController::addSlideToLoop() | Add slide to playlist |
| DELETE | `/:typeNum/loop/:slideID` | LoopController::deleteSlide() | Remove from playlist |

### API Routes (`routes/api.php`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| POST | `/upload-media/:typeNum` | Upload store-specific media |
| DELETE | `/upload-media` | Delete media file |
| POST | `/upload-media/corp/` | Upload corporate media |

### Sync App Routes (`routes/groups/drs.php`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/:typeNum/DigitalSignSyncApp/` | Get version info (XML) |
| POST | `/:typeNum/DigitalSignSyncApp/` | Download sync app binary |

### Queue Display Routes

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/:typeNum/json` | Current queue items |
| GET | `/:typeNum/jsonWait` | Wait time estimation |
| GET | `/:typeNum/jsonCompleted` | Today's completed customers |

---

## External Service Integrations

### Ably (Real-time Messaging)

**Purpose**: Push notifications for schedule changes

**Configuration**:
```php
$ably = new \Ably\AblyRest($_ENV['ABLY_KEY']);
```

**Channel Pattern**: `{typeNum}` (e.g., `ou00`)

**Message Format**:
```json
{
    "action": "refresh"
}
```

**Trigger Events**:
- Slide schedule activation
- Slide schedule deactivation/expiration

**Code Reference**: `SlideScheduleController.php:68-109`

### FFMpeg/FFProbe (Video Processing)

**Purpose**: Video thumbnail extraction and duration detection

**Dependencies**:
```php
use FFMpeg\FFMpeg;
use FFMpeg\FFProbe;
use FFMpeg\Coordinate\TimeCode;
```

**Operations**:
| Operation | Method | Output |
|-----------|--------|--------|
| Thumbnail | `$video->frame(TimeCode::fromSeconds(5))` | JPEG at 5s mark |
| Duration | `$ffprobe->streams()->videos()->first()->get('duration')` | Seconds (int) |

**Code Reference**: `UploadHandler.php:1061-1097`

### jQuery Cycle2 (Frontend Animations)

**Purpose**: Slide transitions on display

**Dependencies**:
- `jquery.cycle.min.js`
- `animate.css`

**Configuration** (in loop.html):
```javascript
$('#mainLoop').cycle({
    fx: 'fade',
    timeout: duration * 1000,
    slides: '> div'
});
```

### Moment.js (Timezone Handling)

**Purpose**: Display timestamps in store's local timezone

**Usage** (in queue.html):
```javascript
moment(timestamp).tz(storeTimezone).format('h:mm A');
```

---

## Queue Display Integration

### Overview

When a slide of type `3` is encountered in the loop, it renders live queue data:

```twig
{% if slide.type == 3 %}
    {% if isFirstQueue == 1 %}
        {% include 'ds/snips/queue.html' %}
        {% set isFirstQueue = 0 %}
    {% endif %}
{% endif %}
```

### Data Flow

```
Display Device
    ↓
AJAX: GET /{typeNum}/json
    ↓
Service Queue System
    ↓
Response: [
    {
        "firstName": "John",
        "lastName": "D.",
        "timeEntered": "2025-12-05 10:30:00",
        "numContainers": 3,
        "processedContainers": 1,
        "status": "in_progress"
    },
    ...
]
```

### Queue Display Components

| Section | Endpoint | Data |
|---------|----------|------|
| Current Queue | `/{typeNum}/json` | Active customers |
| Wait Time | `/{typeNum}/jsonWait` | Estimated wait |
| Last Completed | `/{typeNum}/jsonCompleted` | Today's completions |

### Refresh Interval

```javascript
setInterval(function() {
    showBuyQueue(typeNum);
}, 5000);  // 5 seconds
```

---

## Sync App Integration

### Purpose

Enables offline/disconnected displays by downloading configuration files.

### Authentication Flow

```
GET /{typeNum}/DigitalSignSyncApp/
    ↓
Returns XML:
    <ROOT>
        <VERSION>1.2.3</VERSION>
        <FILENAME>DigitalSignSyncApp_1.2.3.exe</FILENAME>
    </ROOT>

POST /{typeNum}/DigitalSignSyncApp/
    Parameters: api (60-char alphanumeric key)
    ↓
Validation:
    1. HTTPS required
    2. API key validated against store
    ↓
Response: Binary file stream
    Content-Disposition: attachment
```

### Security Measures

| Check | Implementation |
|-------|----------------|
| HTTPS | `isHTTPS($app)` check |
| API Key | 60-char alphanumeric validation |
| File Access | Streams from protected directory |
| Logging | IP address logged on download |

**Code Reference**: `SyncApp/DownloadDigitalSignSyncApp.php`

---

## File Storage Integration

### Directory Structure

```
public_html/upload/digitalSign/
├── corp/                    # Corporate slides (global)
│   ├── thumbs/             # Thumbnails
│   └── thumbs/small/       # Small thumbnails
├── {typeNum}/              # Store-specific (e.g., ou00, pa00)
│   ├── thumbs/
│   └── thumbs/small/
├── templates/              # Template slides
└── hipbone/                # Partner slides
```

### File Naming

| Type | Pattern | Example |
|------|---------|---------|
| Media | `[32-char-hex].[ext]` | `a1b2c3d4e5f6...89.jpg` |
| Video Thumb | `[original].jpg` | `a1b2c3...89.mp4.jpg` |
| Image Thumb | `[original]` | `a1b2c3...89.jpg` |

### URL Patterns

| Type | URL Pattern |
|------|-------------|
| Store Media | `/upload/digitalSign/{typeNum}/{filename}` |
| Store Thumb | `/upload/digitalSign/{typeNum}/thumbs/{filename}` |
| Corp Media | `/upload/digitalSign/corp/{filename}` |
| Corp Thumb | `/upload/digitalSign/corp/thumbs/{filename}` |

---

## Main Kiosk System Integration

### Entry Point

The digital signage display is rendered from the main index:

```php
// public_html/index.php (simplified)
$storeLoop = new StoreLoop($store);
$slidesArray = $storeLoop->getCurrentSignLoopForSign();
$app->render('ds/loop.html', ['slidesArray' => $slidesArray]);
```

### Store Context

All operations use the platform's store resolution:

```php
$storeController = new BuyerKiosk\StoreController($typeNum);
$store = $storeController->getStore();
$db = dbConnectByName($store->getDbName());
```

### Permission Integration

Access control follows platform patterns:

```php
// Check store access
if (!$app->user->checkStoreGroup($typeNum)) {
    $app->notAuthorized();
}
```

---

## Daily Tasks Integration

### Schedule Processing

The system integrates with the platform's daily task runner:

```php
// userfrosting/dailyTasks.php
$scheduleController = new SlideScheduleController($app, $store);
$scheduleController->processScheduleForDate(date('Y-m-d'));
```

### Processing Steps

1. Query `digitalSignSchedule` for pending activations/expirations
2. For each matching entry:
   - Update `dsLoop` in store database
   - Update `digitalSignSchedule` in global database
   - Publish Ably refresh message
3. Log completion

---

## Error Handling Integration

### Logging Destinations

| Component | Log File |
|-----------|----------|
| Digital Sign | `logs/digital_sign.log` |
| Uploads | `logs/upload_log.txt` |
| General | PHP error_log |

### KLogger Integration

```php
$this->log = new \KLogger($_ENV['LOG_DIR']."digital_sign.log", \KLogger::DEBUG);
$this->log->LogDebug("Slide added to loop");
$this->log->LogError("Failed to process schedule");
```
