# Real-time Monitoring

STA Connector provides real-time monitoring capabilities through Phoenix Channels, the Admin Portal, and Admin API.

## Phoenix Channels Overview

Phoenix Channels provide real-time, bidirectional communication over WebSockets. Unlike HTTP polling, channels maintain a persistent connection that allows the server to push updates to clients instantly.

### Key Benefits

- **Low Latency** - Instant updates without polling overhead
- **Efficient** - Single persistent connection instead of repeated HTTP requests
- **Bidirectional** - Clients can send messages and receive updates
- **Scalable** - Built on Phoenix.PubSub for distributed deployments
- **Fault Tolerant** - Automatic reconnection with exponential backoff

### Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                       Phoenix Application                        │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐  │
│  │   Endpoint  │────│   Socket    │────│      Channels       │  │
│  │   /socket   │    │   Handler   │    │  - files:lobby      │  │
│  └─────────────┘    └─────────────┘    │  - metrics:lobby    │  │
│                            │           │  - logs:lobby       │  │
│                     ┌──────┴──────┐    │  - health:status    │  │
│                     │ Phoenix.PubSub│   │  - admin:notifications│ │
│                     └─────────────┘    └─────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              │               │               │
        ┌─────┴─────┐   ┌─────┴─────┐   ┌─────┴─────┐
        │  Browser  │   │  Browser  │   │  Mobile   │
        │  Client   │   │  Client   │   │   App     │
        └───────────┘   └───────────┘   └───────────┘
```

## Available Channels

### files:lobby - File Status Updates

Real-time notifications for file processing events.

**Events:**
| Event | Description |
|-------|-------------|
| `file:discovered` | New file detected in STA |
| `file:downloading` | File download started |
| `file:downloaded` | File download completed |
| `file:processing` | File parsing in progress |
| `file:completed` | File successfully processed |
| `file:failed` | File processing failed |
| `file:retrying` | File scheduled for retry |

**Payload Example:**
```json
{
  "id": "file_abc123",
  "filename": "CCS_20260131_001.xml",
  "system": "CCS",
  "status": "completed",
  "size_bytes": 15360,
  "processed_at": "2026-01-31T10:30:00Z"
}
```

### metrics:lobby - System Metrics

Real-time system performance metrics pushed every 5 seconds.

**Events:**
| Event | Description |
|-------|-------------|
| `metrics:update` | Periodic metrics snapshot |
| `metrics:alert` | Threshold exceeded alert |

**Payload Example:**
```json
{
  "timestamp": "2026-01-31T10:30:00Z",
  "inbound": {
    "files_processed": 1200,
    "files_pending": 22,
    "throughput_per_min": 45
  },
  "outbound": {
    "files_uploaded": 4995,
    "files_pending": 2,
    "throughput_per_min": 30
  },
  "sta_client": {
    "avg_latency_ms": 450,
    "requests_total": 50000
  }
}
```

### logs:lobby - Log Streaming

Real-time log streaming for debugging and monitoring.

**Events:**
| Event | Description |
|-------|-------------|
| `log:entry` | New log entry |
| `log:batch` | Batch of log entries |

**Payload Example:**
```json
{
  "level": "info",
  "message": "File CCS_20260131_001.xml processed successfully",
  "timestamp": "2026-01-31T10:30:00Z",
  "metadata": {
    "file_id": "file_abc123",
    "system": "CCS",
    "duration_ms": 250
  }
}
```

### health:status - Health Updates

Real-time health status changes for all system components.

**Events:**
| Event | Description |
|-------|-------------|
| `health:update` | Component health changed |
| `health:degraded` | System entered degraded state |
| `health:recovered` | System recovered from degraded state |

**Payload Example:**
```json
{
  "overall": "healthy",
  "components": {
    "database": {"status": "healthy", "latency_ms": 5},
    "sta_connection": {"status": "healthy", "latency_ms": 120},
    "inbound_poller": {"status": "healthy", "last_poll": "2026-01-31T10:29:00Z"},
    "outbound_api": {"status": "healthy", "requests_active": 3}
  },
  "timestamp": "2026-01-31T10:30:00Z"
}
```

### admin:notifications - Admin Alerts

Administrative alerts and system notifications.

**Events:**
| Event | Description |
|-------|-------------|
| `notification:info` | Informational message |
| `notification:warning` | Warning alert |
| `notification:error` | Error alert |
| `config:updated` | Configuration changed |
| `route:toggled` | Route enabled/disabled |

**Payload Example:**
```json
{
  "type": "warning",
  "title": "High Queue Depth",
  "message": "Outbound queue depth exceeded 100 files",
  "timestamp": "2026-01-31T10:30:00Z",
  "action_required": false
}
```

## JavaScript Client Examples

### Installation

Install the official Phoenix JavaScript client:

```bash
npm install phoenix
```

### Basic Connection

```javascript
import { Socket } from "phoenix";

// Create socket connection with authentication
const socket = new Socket("/socket", {
  params: { token: userToken }
});

// Connect to the server
socket.connect();

// Handle connection events
socket.onOpen(() => console.log("Connected to Phoenix Channels"));
socket.onError((err) => console.error("Socket error:", err));
socket.onClose(() => console.log("Socket closed"));
```

### Joining Channels

```javascript
// Join the files channel
const filesChannel = socket.channel("files:lobby", {});

filesChannel.join()
  .receive("ok", (resp) => {
    console.log("Joined files channel", resp);
  })
  .receive("error", (resp) => {
    console.error("Failed to join files channel", resp);
  });

// Listen for file events
filesChannel.on("file:discovered", (payload) => {
  console.log("New file discovered:", payload.filename);
  updateFileList(payload);
});

filesChannel.on("file:completed", (payload) => {
  console.log("File processed:", payload.filename);
  showSuccessNotification(payload);
});

filesChannel.on("file:failed", (payload) => {
  console.error("File failed:", payload.filename, payload.error);
  showErrorNotification(payload);
});
```

### Metrics Channel Example

```javascript
const metricsChannel = socket.channel("metrics:lobby", {});

metricsChannel.join()
  .receive("ok", () => console.log("Joined metrics channel"));

// Update dashboard with real-time metrics
metricsChannel.on("metrics:update", (metrics) => {
  updateDashboardCharts(metrics);
  updateCounters(metrics.inbound, metrics.outbound);
});

// Handle metric alerts
metricsChannel.on("metrics:alert", (alert) => {
  showMetricAlert(alert.metric, alert.value, alert.threshold);
});
```

### Log Streaming Example

```javascript
const logsChannel = socket.channel("logs:lobby", {
  level: "info"  // Filter: debug, info, warn, error
});

logsChannel.join()
  .receive("ok", () => console.log("Joined logs channel"));

logsChannel.on("log:entry", (entry) => {
  appendLogEntry(entry);

  if (entry.level === "error") {
    highlightError(entry);
  }
});

// Request log history
logsChannel.push("get_history", { limit: 100 })
  .receive("ok", (response) => {
    response.logs.forEach(appendLogEntry);
  });
```

### Health Monitoring Example

```javascript
const healthChannel = socket.channel("health:status", {});

healthChannel.join()
  .receive("ok", (response) => {
    // Initial health state
    updateHealthIndicators(response.health);
  });

healthChannel.on("health:update", (health) => {
  updateHealthIndicators(health);
});

healthChannel.on("health:degraded", (data) => {
  showDegradedBanner(data.component, data.reason);
  triggerAlert(data);
});

healthChannel.on("health:recovered", (data) => {
  hideDegradedBanner();
  showRecoveryNotification(data);
});
```

### Admin Notifications Example

```javascript
const adminChannel = socket.channel("admin:notifications", {});

adminChannel.join()
  .receive("ok", () => console.log("Joined admin notifications"));

adminChannel.on("notification:warning", (notification) => {
  showToast("warning", notification.title, notification.message);
});

adminChannel.on("notification:error", (notification) => {
  showToast("error", notification.title, notification.message);
  playAlertSound();
});

adminChannel.on("config:updated", (data) => {
  showToast("info", "Configuration Updated", data.message);
  refreshConfigDisplay();
});

adminChannel.on("route:toggled", (data) => {
  updateRouteStatus(data.system, data.enabled);
});
```

### Sending Messages to Server

```javascript
// Trigger a manual poll from the client
filesChannel.push("trigger_poll", {})
  .receive("ok", (response) => {
    console.log("Poll triggered:", response.poll_id);
  })
  .receive("error", (response) => {
    console.error("Failed to trigger poll:", response.reason);
  })
  .receive("timeout", () => {
    console.error("Request timed out");
  });

// Retry a failed file
filesChannel.push("retry_file", { file_id: "file_abc123" })
  .receive("ok", (response) => {
    showSuccessNotification("Retry scheduled");
  });
```

### Presence Tracking

Track connected admin users in real-time:

```javascript
import { Presence } from "phoenix";

const presences = {};
const presence = new Presence(adminChannel);

presence.onSync(() => {
  const userList = presence.list((id, { metas: [first, ...rest] }) => {
    return { id, user: first.user, online_at: first.online_at };
  });
  updateOnlineUsers(userList);
});

presence.onJoin((id, current, newPres) => {
  console.log("User joined:", newPres.metas[0].user);
});

presence.onLeave((id, current, leftPres) => {
  console.log("User left:", leftPres.metas[0].user);
});
```

### Complete React Hook Example

```javascript
import { useEffect, useState, useCallback } from "react";
import { Socket } from "phoenix";

export function usePhoenixChannel(channelName, token) {
  const [channel, setChannel] = useState(null);
  const [connected, setConnected] = useState(false);
  const [data, setData] = useState(null);

  useEffect(() => {
    const socket = new Socket("/socket", { params: { token } });
    socket.connect();

    const chan = socket.channel(channelName, {});

    chan.join()
      .receive("ok", (response) => {
        setConnected(true);
        setData(response);
      })
      .receive("error", (err) => {
        console.error("Join error:", err);
        setConnected(false);
      });

    setChannel(chan);

    return () => {
      chan.leave();
      socket.disconnect();
    };
  }, [channelName, token]);

  const subscribe = useCallback((event, callback) => {
    if (channel) {
      channel.on(event, callback);
      return () => channel.off(event, callback);
    }
  }, [channel]);

  const push = useCallback((event, payload) => {
    if (channel) {
      return channel.push(event, payload);
    }
  }, [channel]);

  return { channel, connected, data, subscribe, push };
}

// Usage in component
function FileMonitor() {
  const { connected, subscribe, push } = usePhoenixChannel("files:lobby", token);
  const [files, setFiles] = useState([]);

  useEffect(() => {
    const unsubscribe = subscribe("file:completed", (file) => {
      setFiles(prev => [file, ...prev]);
    });
    return unsubscribe;
  }, [subscribe]);

  const triggerPoll = () => push("trigger_poll", {});

  return (
    <div>
      <span>Status: {connected ? "Connected" : "Disconnected"}</span>
      <button onClick={triggerPoll}>Trigger Poll</button>
      {files.map(f => <FileCard key={f.id} file={f} />)}
    </div>
  );
}
```

## Server-Side Broadcasting with Phoenix.PubSub

### PubSub Configuration

```elixir
# config/config.exs
config :sta_connector, StaConnectorWeb.Endpoint,
  pubsub_server: StaConnector.PubSub

# lib/sta_connector/application.ex
children = [
  {Phoenix.PubSub, name: StaConnector.PubSub},
  StaConnectorWeb.Endpoint
]
```

### Broadcasting from Channels

```elixir
defmodule StaConnectorWeb.FilesChannel do
  use Phoenix.Channel

  def join("files:lobby", _params, socket) do
    {:ok, socket}
  end

  # Broadcast file status update
  def handle_in("file_status", %{"file_id" => id, "status" => status}, socket) do
    broadcast!(socket, "file:#{status}", %{
      id: id,
      status: status,
      timestamp: DateTime.utc_now()
    })
    {:noreply, socket}
  end
end
```

### Broadcasting from Anywhere in the Application

```elixir
defmodule StaConnector.FileProcessor do
  alias StaConnectorWeb.Endpoint

  def process_file(file) do
    # Broadcast file processing started
    Endpoint.broadcast("files:lobby", "file:processing", %{
      id: file.id,
      filename: file.name,
      started_at: DateTime.utc_now()
    })

    case do_process(file) do
      {:ok, result} ->
        # Broadcast success
        Endpoint.broadcast("files:lobby", "file:completed", %{
          id: file.id,
          filename: file.name,
          size_bytes: result.size,
          processed_at: DateTime.utc_now()
        })
        {:ok, result}

      {:error, reason} ->
        # Broadcast failure
        Endpoint.broadcast("files:lobby", "file:failed", %{
          id: file.id,
          filename: file.name,
          error: reason,
          failed_at: DateTime.utc_now()
        })
        {:error, reason}
    end
  end
end
```

### Broadcasting Metrics Updates

```elixir
defmodule StaConnector.MetricsBroadcaster do
  use GenServer
  alias StaConnectorWeb.Endpoint

  @broadcast_interval 5_000  # 5 seconds

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def init(state) do
    schedule_broadcast()
    {:ok, state}
  end

  def handle_info(:broadcast_metrics, state) do
    metrics = StaConnector.Metrics.collect()

    Endpoint.broadcast("metrics:lobby", "metrics:update", %{
      timestamp: DateTime.utc_now(),
      inbound: metrics.inbound,
      outbound: metrics.outbound,
      sta_client: metrics.sta_client
    })

    # Check for alerts
    if metrics.outbound.queue_depth > 100 do
      Endpoint.broadcast("metrics:lobby", "metrics:alert", %{
        metric: "queue_depth",
        value: metrics.outbound.queue_depth,
        threshold: 100
      })
    end

    schedule_broadcast()
    {:noreply, state}
  end

  defp schedule_broadcast do
    Process.send_after(self(), :broadcast_metrics, @broadcast_interval)
  end
end
```

### Broadcasting Health Updates

```elixir
defmodule StaConnector.HealthMonitor do
  use GenServer
  alias StaConnectorWeb.Endpoint

  def report_health_change(component, status, details \\ %{}) do
    GenServer.cast(__MODULE__, {:health_change, component, status, details})
  end

  def handle_cast({:health_change, component, status, details}, state) do
    previous = Map.get(state.components, component, %{status: :unknown})

    if previous.status != status do
      event = case status do
        :healthy when previous.status in [:degraded, :unhealthy] -> "health:recovered"
        :degraded -> "health:degraded"
        :unhealthy -> "health:degraded"
        _ -> "health:update"
      end

      Endpoint.broadcast("health:status", event, %{
        component: component,
        status: status,
        previous_status: previous.status,
        details: details,
        timestamp: DateTime.utc_now()
      })
    end

    {:noreply, put_in(state.components[component], %{status: status, details: details})}
  end
end
```

### Admin Notifications

```elixir
defmodule StaConnector.AdminNotifier do
  alias StaConnectorWeb.Endpoint

  def notify(type, title, message, opts \\ []) do
    Endpoint.broadcast("admin:notifications", "notification:#{type}", %{
      type: type,
      title: title,
      message: message,
      action_required: Keyword.get(opts, :action_required, false),
      timestamp: DateTime.utc_now()
    })
  end

  def notify_config_update(changes) do
    Endpoint.broadcast("admin:notifications", "config:updated", %{
      message: "Configuration updated",
      changes: changes,
      timestamp: DateTime.utc_now()
    })
  end

  def notify_route_toggle(system, enabled) do
    Endpoint.broadcast("admin:notifications", "route:toggled", %{
      system: system,
      enabled: enabled,
      message: "Route #{system} #{if enabled, do: "enabled", else: "disabled"}",
      timestamp: DateTime.utc_now()
    })
  end
end
```

## Authentication with Phoenix.Token

### Generating Tokens (Server-Side)

```elixir
defmodule StaConnectorWeb.TokenAuth do
  @salt "sta_connector_socket_auth"
  @max_age 86400  # 24 hours

  # Generate token for authenticated user
  def generate_token(conn, user) do
    Phoenix.Token.sign(
      StaConnectorWeb.Endpoint,
      @salt,
      %{user_id: user.id, role: user.role}
    )
  end

  # Verify token
  def verify_token(token) do
    case Phoenix.Token.verify(StaConnectorWeb.Endpoint, @salt, token, max_age: @max_age) do
      {:ok, data} -> {:ok, data}
      {:error, :expired} -> {:error, :token_expired}
      {:error, :invalid} -> {:error, :invalid_token}
    end
  end
end
```

### Socket Authentication

```elixir
defmodule StaConnectorWeb.UserSocket do
  use Phoenix.Socket

  channel "files:*", StaConnectorWeb.FilesChannel
  channel "metrics:*", StaConnectorWeb.MetricsChannel
  channel "logs:*", StaConnectorWeb.LogsChannel
  channel "health:*", StaConnectorWeb.HealthChannel
  channel "admin:*", StaConnectorWeb.AdminChannel

  @impl true
  def connect(%{"token" => token}, socket, _connect_info) do
    case StaConnectorWeb.TokenAuth.verify_token(token) do
      {:ok, %{user_id: user_id, role: role}} ->
        socket = socket
          |> assign(:user_id, user_id)
          |> assign(:role, role)
        {:ok, socket}

      {:error, reason} ->
        {:error, %{reason: reason}}
    end
  end

  def connect(_params, _socket, _connect_info) do
    {:error, %{reason: :missing_token}}
  end

  @impl true
  def id(socket), do: "user_socket:#{socket.assigns.user_id}"
end
```

### Channel Authorization

```elixir
defmodule StaConnectorWeb.AdminChannel do
  use Phoenix.Channel

  # Only allow admin users to join admin channel
  def join("admin:notifications", _params, socket) do
    if socket.assigns.role == :admin do
      {:ok, socket}
    else
      {:error, %{reason: :unauthorized}}
    end
  end
end

defmodule StaConnectorWeb.LogsChannel do
  use Phoenix.Channel

  # Allow joining with role-based log level filtering
  def join("logs:lobby", %{"level" => level}, socket) do
    allowed_levels = case socket.assigns.role do
      :admin -> [:debug, :info, :warn, :error]
      :operator -> [:info, :warn, :error]
      _ -> [:warn, :error]
    end

    if String.to_atom(level) in allowed_levels do
      {:ok, assign(socket, :log_level, String.to_atom(level))}
    else
      {:error, %{reason: :unauthorized_level}}
    end
  end
end
```

### Client-Side Token Handling

```javascript
// Fetch token from authentication endpoint
async function getSocketToken() {
  const response = await fetch('/api/socket-token', {
    headers: {
      'Authorization': `Bearer ${authToken}`
    }
  });
  const data = await response.json();
  return data.socket_token;
}

// Connect with token
async function connectSocket() {
  const token = await getSocketToken();

  const socket = new Socket("/socket", {
    params: { token },
    reconnectAfterMs: (tries) => {
      // Exponential backoff: 1s, 2s, 4s, 8s, max 30s
      return Math.min(1000 * Math.pow(2, tries), 30000);
    }
  });

  socket.onError((error) => {
    if (error.reason === "token_expired") {
      // Refresh token and reconnect
      refreshTokenAndReconnect(socket);
    }
  });

  socket.connect();
  return socket;
}

async function refreshTokenAndReconnect(socket) {
  const newToken = await getSocketToken();
  socket.params.token = newToken;
  socket.connect();
}
```

### Token Refresh Strategy

```javascript
class SocketManager {
  constructor(tokenEndpoint) {
    this.tokenEndpoint = tokenEndpoint;
    this.socket = null;
    this.tokenRefreshInterval = null;
  }

  async connect() {
    const token = await this.fetchToken();

    this.socket = new Socket("/socket", {
      params: { token }
    });

    this.socket.connect();

    // Refresh token every 23 hours (token valid for 24h)
    this.tokenRefreshInterval = setInterval(async () => {
      const newToken = await this.fetchToken();
      this.socket.params.token = newToken;
    }, 23 * 60 * 60 * 1000);

    return this.socket;
  }

  async fetchToken() {
    const response = await fetch(this.tokenEndpoint, {
      credentials: 'include'
    });
    const data = await response.json();
    return data.socket_token;
  }

  disconnect() {
    if (this.tokenRefreshInterval) {
      clearInterval(this.tokenRefreshInterval);
    }
    if (this.socket) {
      this.socket.disconnect();
    }
  }
}
```

## Admin Portal

The React-based Admin Portal provides a real-time dashboard for monitoring file processing, system health, and metrics.

### Features

- **Health Dashboard** - Real-time system status overview
- **File Monitoring** - Live view of file processing with filters
- **Metrics Visualization** - Charts for throughput, latency, and queue depth
- **Configuration Editor** - Modify settings without restart
- **Route Management** - Enable/disable routes with toggles
- **Dark/Light Mode** - User preference support

### Accessing the Portal

```bash
# With Docker
open http://localhost:3000

# Development mode
cd admin-portal
npm run dev
open http://localhost:5173
```

## Dashboard Overview

The main dashboard displays:

```
┌─────────────────────────────────────────────────────────────┐
│  STA Connector Admin                              [Dark]    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  System Health: ● Healthy                                   │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│  │ Inbound  │  │ Outbound │  │ Database │  │   STA    │    │
│  │    ●     │  │    ●     │  │    ●     │  │    ●     │    │
│  │  Active  │  │  Active  │  │Connected │  │   OK     │    │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘    │
│                                                             │
│  Files Processed Today                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ ▂▃▅▇█▇▅▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁                           │   │
│  │ 00:00        06:00        12:00        18:00   24:00│   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────┐  ┌─────────────────────────────┐  │
│  │ Recent Activity     │  │ System Metrics              │  │
│  │ • SPI file uploaded │  │ Uptime: 3d 4h 25m          │  │
│  │ • CCS file received │  │ Memory: 256 MB             │  │
│  │ • DICT file sent    │  │ Queue Depth: 5             │  │
│  └─────────────────────┘  └─────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

## API Integration

The Admin Portal integrates with the Admin API (port 8081) for all data.

### Base URL Configuration

```typescript
// src/config.ts
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8081';
```

### Authentication

The portal uses HTTP Basic authentication for REST APIs:

```typescript
// src/api/client.ts
const credentials = btoa(`${username}:${password}`);

const headers = {
  'Authorization': `Basic ${credentials}`,
  'Content-Type': 'application/json'
};
```

### Available REST Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/admin/health` | GET | System health status |
| `/admin/metrics` | GET | Operational metrics |
| `/admin/config` | GET | Current configuration |
| `/admin/config` | PATCH | Update configuration |
| `/admin/files` | GET | List files with filters |
| `/admin/retry/{id}` | POST | Retry failed file |
| `/admin/poller/trigger` | POST | Trigger poll cycle |

## Metrics Display

### File Processing Metrics

```json
{
  "timestamp": "2026-01-31T10:30:00Z",
  "inbound": {
    "files_discovered": 1234,
    "files_processed": 1200,
    "files_failed": 12,
    "files_pending": 22
  },
  "outbound": {
    "files_submitted": 5000,
    "files_uploaded": 4995,
    "files_failed": 3,
    "files_pending": 2
  }
}
```

### System Metrics

```json
{
  "uptime_seconds": 86400,
  "poller": {
    "enabled": true,
    "last_poll": "2026-01-31T10:29:00Z",
    "poll_count": 500
  },
  "sta_client": {
    "requests_total": 50000,
    "requests_failed": 45,
    "avg_latency_ms": 450
  }
}
```

## File Monitoring

### Filtering Files

The file list supports multiple filters:

```
/admin/files?status=failed&system=SPI&limit=50&offset=0
```

| Filter | Description |
|--------|-------------|
| `status` | Filter by status: `pending`, `completed`, `failed` |
| `system` | Filter by BCB system: `CCS`, `SPI`, `DICT`, etc. |
| `limit` | Maximum files to return (default: 100, max: 1000) |
| `offset` | Pagination offset |
| `start_date` | Start date (ISO 8601 or YYYY-MM-DD) |
| `end_date` | End date (ISO 8601 or YYYY-MM-DD) |

### File Status Colors

| Status | Color | Description |
|--------|-------|-------------|
| `pending` | Yellow | Awaiting processing |
| `uploading` | Blue | Upload in progress |
| `completed` | Green | Successfully processed |
| `failed` | Red | Processing failed |
| `retrying` | Orange | Scheduled for retry |

## Configuration Management

### Viewing Configuration

The configuration page displays all settings with sensitive values redacted:

```json
{
  "service": {
    "name": "sta-connector",
    "log_level": "info"
  },
  "sta": {
    "environment": "homologation",
    "username": "user123",
    "password": "[REDACTED]",
    "timeout_seconds": 30
  },
  "poller": {
    "enabled": true,
    "interval_seconds": 30
  }
}
```

### Updating Configuration

Hot-reloadable settings can be changed via the portal:

```javascript
// Update poller interval
await fetch('/api/admin/config', {
  method: 'PATCH',
  headers: {
    'Authorization': `Basic ${credentials}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    poller: {
      interval_seconds: 60
    }
  })
});
```

## Route Management

### Listing Routes

Routes are displayed with their current status:

```
┌──────────┬────────────┬─────────────────────────────────┬─────────┐
│ System   │ Status     │ URL                             │ Actions │
├──────────┼────────────┼─────────────────────────────────┼─────────┤
│ CCS      │ ● Enabled  │ https://fluxiq.com.br/api/ccs   │ [Toggle]│
│ SPI      │ ● Enabled  │ https://fluxiq.com.br/api/spi   │ [Toggle]│
│ DICT     │ ○ Disabled │ https://fluxiq.com.br/api/dict  │ [Toggle]│
└──────────┴────────────┴─────────────────────────────────┴─────────┘
```

### Toggling Routes

Enable or disable routes without restart:

```javascript
// Disable a route
await fetch('/api/admin/config', {
  method: 'PATCH',
  headers: { /* ... */ },
  body: JSON.stringify({
    routes: {
      DICT: {
        enabled: false
      }
    }
  })
});
```

## Manual Operations

### Retry Failed File

```javascript
async function retryFile(fileId) {
  const response = await fetch(`/api/admin/retry/${fileId}`, {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${credentials}`
    }
  });
  return response.json();
}
```

### Trigger Poll Cycle

```javascript
async function triggerPoll() {
  const response = await fetch('/api/admin/poller/trigger', {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${credentials}`
    }
  });
  return response.json();
}
```

## Dark Mode

The Admin Portal supports dark mode:

```typescript
// Toggle theme
const toggleTheme = () => {
  const current = localStorage.getItem('theme') || 'light';
  const next = current === 'light' ? 'dark' : 'light';
  localStorage.setItem('theme', next);
  document.documentElement.classList.toggle('dark');
};
```

## Building the Portal

### Development

```bash
cd admin-portal
npm install
npm run dev
```

### Production Build

```bash
cd admin-portal
npm run build
```

The build output is in `admin-portal/dist/` and can be served by any static file server.

### Docker

The portal is included in the Docker Compose setup:

```yaml
services:
  admin-portal:
    build:
      context: ../admin-portal
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    depends_on:
      - sta-connector
```

## Customization

### Environment Variables

```bash
# API URL for the admin portal
VITE_API_URL=http://localhost:8081

# Socket URL for Phoenix Channels
VITE_SOCKET_URL=ws://localhost:4000/socket

# Fallback polling intervals when channels unavailable (milliseconds)
VITE_HEALTH_POLL_INTERVAL=30000
VITE_METRICS_POLL_INTERVAL=5000
VITE_FILES_POLL_INTERVAL=10000
```

### Proxy Configuration

For development, configure the Vite proxy:

```typescript
// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8081',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      },
      '/socket': {
        target: 'ws://localhost:4000',
        ws: true
      }
    }
  }
});
```

## Next Steps

- [Admin API](/api/admin) - Full API reference
- [Configuration](/guide/configuration) - Configuration options
- [Troubleshooting](/troubleshooting) - Common issues
