# Test Files and Real Integration Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Create 80 BCB test files (5 inbound + 5 outbound per 8 systems), build test infrastructure to serve them, and connect all frontend components to real Phoenix channels with no mock data.

**Architecture:** Test files stored in `backend/priv/testdata/`. TestFileServer GenServer serves files as mock STA. MockSTAUploader simulates BCB responses with webhook callbacks. All frontend views connect to real Phoenix channels (files:lobby, metrics:lobby, health:status, logs:stream). WebhookNotifier calls FluxiQ STA callback URLs.

**Tech Stack:** Elixir/Phoenix, Ecto, Phoenix Channels, Vue 3 + TypeScript, Pinia stores

---

## Wave 1: Test Infrastructure

### Task 1.1: Create CCS Test Files (Inbound + Outbound)

**Files:**
- Create: `backend/priv/testdata/inbound/ccs/ccs_success_basic.txt`
- Create: `backend/priv/testdata/inbound/ccs/ccs_large_batch.txt`
- Create: `backend/priv/testdata/inbound/ccs/ccs_special_chars.txt`
- Create: `backend/priv/testdata/inbound/ccs/ccs_multiple_types.txt`
- Create: `backend/priv/testdata/inbound/ccs/ccs_edge_single.txt`
- Create: `backend/priv/testdata/outbound/ccs/ccs_upload_success.txt`
- Create: `backend/priv/testdata/outbound/ccs/ccs_upload_rejected.txt`
- Create: `backend/priv/testdata/outbound/ccs/ccs_upload_pending.txt`
- Create: `backend/priv/testdata/outbound/ccs/ccs_upload_large.txt`
- Create: `backend/priv/testdata/outbound/ccs/ccs_upload_retry.txt`

**Step 1: Create inbound CCS test files**

CCS files use fixed-width format: Header (type 0), Detail (type 1), Trailer (type 9).
Each line is 200 characters, padded with spaces.

```text
# ccs_success_basic.txt - 10 records, standard valid file
0CCS00100000001202602010001
112345678901  1JOAO SILVA                                                                                                                           001123456789012345678
198765432109  1MARIA SANTOS                                                                                                                         002987654321098765432
... (8 more detail lines)
9000000010
```

Create all 5 inbound files with variations:
- `ccs_success_basic.txt`: 10 records, CPF/CNPJ mix, ACSC status
- `ccs_large_batch.txt`: 500 records for performance testing
- `ccs_special_chars.txt`: Brazilian names with accents (José, João, Conceição)
- `ccs_multiple_types.txt`: Mix of CCS001, CCS002, CCS003 record types
- `ccs_edge_single.txt`: Single record edge case

**Step 2: Create outbound CCS test files**

Same format but for upload testing:
- `ccs_upload_success.txt`: Valid file that will return ACSC
- `ccs_upload_rejected.txt`: Invalid format that will return RJCT
- `ccs_upload_pending.txt`: File that will return PDNG initially
- `ccs_upload_large.txt`: Large file for upload performance
- `ccs_upload_retry.txt`: File for testing retry logic

**Step 3: Verify files parse correctly**

```bash
cd backend && mix run -e "
  content = File.read!('priv/testdata/inbound/ccs/ccs_success_basic.txt')
  {:ok, result} = StaConnector.Parsers.CCS.parse(content)
  IO.inspect(result, limit: :infinity)
"
```

Expected: Successfully parsed with header, 10 details, trailer

---

### Task 1.2: Create CIR Test Files

**Files:**
- Create: `backend/priv/testdata/inbound/cir/cir_success_basic.txt`
- Create: `backend/priv/testdata/inbound/cir/cir_large_batch.txt`
- Create: `backend/priv/testdata/inbound/cir/cir_special_chars.txt`
- Create: `backend/priv/testdata/inbound/cir/cir_multiple_types.txt`
- Create: `backend/priv/testdata/inbound/cir/cir_edge_single.txt`
- Create: `backend/priv/testdata/outbound/cir/` (5 outbound files)

**Step 1: Create inbound CIR test files**

CIR (Cadastro de Instituições Reguladas) format. Same structure approach as CCS.

---

### Task 1.3: Create CMP Test Files

**Files:**
- Create: `backend/priv/testdata/inbound/cmp/` (5 inbound files)
- Create: `backend/priv/testdata/outbound/cmp/` (5 outbound files)

CMP (Câmara de Moeda Provisória) files.

---

### Task 1.4: Create STR Test Files

**Files:**
- Create: `backend/priv/testdata/inbound/str/` (5 inbound files)
- Create: `backend/priv/testdata/outbound/str/` (5 outbound files)

STR (Sistema de Transferência de Reservas) files for reserve transfers.

---

### Task 1.5: Create SPI Test Files

**Files:**
- Create: `backend/priv/testdata/inbound/spi/` (5 inbound files)
- Create: `backend/priv/testdata/outbound/spi/` (5 outbound files)

SPI (Sistema de Pagamentos Instantâneos) PIX transaction files.

---

### Task 1.6: Create CAM Test Files

**Files:**
- Create: `backend/priv/testdata/inbound/cam/` (5 inbound files)
- Create: `backend/priv/testdata/outbound/cam/` (5 outbound files)

CAM (Câmara de Ativos) asset management files.

---

### Task 1.7: Create LDL Test Files

**Files:**
- Create: `backend/priv/testdata/inbound/ldl/` (5 inbound files)
- Create: `backend/priv/testdata/outbound/ldl/` (5 outbound files)

LDL (Liquidação Diferida de Lotes) batch settlement files.

---

### Task 1.8: Create DICT Test Files

**Files:**
- Create: `backend/priv/testdata/inbound/dict/` (5 inbound files)
- Create: `backend/priv/testdata/outbound/dict/` (5 outbound files)

DICT (Diretório de Identificadores de Contas Transacionais) PIX key registry files.

---

### Task 1.9: Build TestFileServer GenServer

**Files:**
- Create: `backend/lib/sta_connector/testing/file_server.ex`
- Create: `backend/test/sta_connector/testing/file_server_test.exs`

**Step 1: Write failing test**

```elixir
# backend/test/sta_connector/testing/file_server_test.exs
defmodule StaConnector.Testing.FileServerTest do
  use ExUnit.Case, async: true

  alias StaConnector.Testing.FileServer

  describe "list_available/1" do
    test "returns available test files for system" do
      {:ok, files} = FileServer.list_available(["CCS"])

      assert length(files) > 0
      assert Enum.all?(files, fn f -> f.system_id == "CCS" end)
    end
  end

  describe "download/1" do
    test "returns file content" do
      {:ok, [file | _]} = FileServer.list_available(["CCS"])
      {:ok, content} = FileServer.download(file.protocol_number)

      assert is_binary(content)
      assert byte_size(content) > 0
    end
  end
end
```

**Step 2: Run test to verify it fails**

```bash
cd backend && mix test test/sta_connector/testing/file_server_test.exs
```

Expected: FAIL with "FileServer not defined"

**Step 3: Implement FileServer**

```elixir
# backend/lib/sta_connector/testing/file_server.ex
defmodule StaConnector.Testing.FileServer do
  @moduledoc """
  Test file server that serves BCB test files from priv/testdata.
  Simulates STA WebService responses for testing.
  """

  use GenServer
  require Logger

  @testdata_path "priv/testdata"

  defmodule FileInfo do
    defstruct [:protocol_number, :system_id, :filename, :size, :direction, :available_at]
  end

  # Client API

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def list_available(system_ids) when is_list(system_ids) do
    GenServer.call(__MODULE__, {:list_available, system_ids})
  end

  def download(protocol_number) do
    GenServer.call(__MODULE__, {:download, protocol_number})
  end

  def reset do
    GenServer.call(__MODULE__, :reset)
  end

  # Server Callbacks

  @impl true
  def init(_opts) do
    state = %{
      files: load_test_files(),
      downloaded: MapSet.new()
    }
    {:ok, state}
  end

  @impl true
  def handle_call({:list_available, system_ids}, _from, state) do
    files = state.files
    |> Enum.filter(fn f ->
      f.system_id in system_ids and
      f.direction == :inbound and
      f.protocol_number not in state.downloaded
    end)

    {:reply, {:ok, files}, state}
  end

  @impl true
  def handle_call({:download, protocol_number}, _from, state) do
    case Enum.find(state.files, fn f -> f.protocol_number == protocol_number end) do
      nil ->
        {:reply, {:error, :not_found}, state}

      file ->
        path = file_path(file)
        case File.read(path) do
          {:ok, content} ->
            downloaded = MapSet.put(state.downloaded, protocol_number)
            {:reply, {:ok, content}, %{state | downloaded: downloaded}}

          {:error, reason} ->
            {:reply, {:error, reason}, state}
        end
    end
  end

  @impl true
  def handle_call(:reset, _from, state) do
    {:reply, :ok, %{state | downloaded: MapSet.new()}}
  end

  # Private Functions

  defp load_test_files do
    inbound = load_files_from_dir("inbound", :inbound)
    outbound = load_files_from_dir("outbound", :outbound)
    inbound ++ outbound
  end

  defp load_files_from_dir(subdir, direction) do
    base_path = Path.join([:code.priv_dir(:sta_connector), "testdata", subdir])

    if File.exists?(base_path) do
      File.ls!(base_path)
      |> Enum.flat_map(fn system_dir ->
        system_id = String.upcase(system_dir)
        system_path = Path.join(base_path, system_dir)

        if File.dir?(system_path) do
          File.ls!(system_path)
          |> Enum.filter(&String.ends_with?(&1, ".txt"))
          |> Enum.with_index()
          |> Enum.map(fn {filename, idx} ->
            file_path = Path.join(system_path, filename)
            stat = File.stat!(file_path)

            %FileInfo{
              protocol_number: generate_protocol(system_id, direction, idx),
              system_id: system_id,
              filename: filename,
              size: stat.size,
              direction: direction,
              available_at: DateTime.utc_now()
            }
          end)
        else
          []
        end
      end)
    else
      []
    end
  end

  defp generate_protocol(system_id, direction, idx) do
    date = Date.utc_today() |> Date.to_iso8601(:basic)
    dir_code = if direction == :inbound, do: "I", else: "O"
    "#{date}#{system_id}#{dir_code}#{String.pad_leading(to_string(idx), 4, "0")}"
  end

  defp file_path(%FileInfo{system_id: system_id, filename: filename, direction: direction}) do
    dir = if direction == :inbound, do: "inbound", else: "outbound"
    Path.join([:code.priv_dir(:sta_connector), "testdata", dir, String.downcase(system_id), filename])
  end
end
```

**Step 4: Run test to verify it passes**

```bash
cd backend && mix test test/sta_connector/testing/file_server_test.exs
```

Expected: PASS

**Step 5: Commit**

```bash
git add backend/lib/sta_connector/testing/file_server.ex backend/test/sta_connector/testing/file_server_test.exs
git commit -m "feat: add TestFileServer for BCB test files"
```

---

### Task 1.10: Build MockSTAUploader

**Files:**
- Create: `backend/lib/sta_connector/testing/mock_sta_uploader.ex`
- Create: `backend/test/sta_connector/testing/mock_sta_uploader_test.exs`

**Step 1: Write failing test**

```elixir
# backend/test/sta_connector/testing/mock_sta_uploader_test.exs
defmodule StaConnector.Testing.MockSTAUploaderTest do
  use ExUnit.Case, async: true

  alias StaConnector.Testing.MockSTAUploader

  describe "request_protocol/1" do
    test "returns a protocol number" do
      {:ok, protocol} = MockSTAUploader.request_protocol("CCS")

      assert is_binary(protocol)
      assert String.length(protocol) == 20
    end
  end

  describe "upload_file/2" do
    test "simulates upload and returns status" do
      {:ok, protocol} = MockSTAUploader.request_protocol("CCS")
      content = "test file content"

      {:ok, result} = MockSTAUploader.upload_file(protocol, content)

      assert result.status in ["ACSC", "RJCT", "PDNG"]
      assert result.protocol == protocol
    end
  end
end
```

**Step 2: Run test to verify it fails**

```bash
cd backend && mix test test/sta_connector/testing/mock_sta_uploader_test.exs
```

Expected: FAIL

**Step 3: Implement MockSTAUploader**

```elixir
# backend/lib/sta_connector/testing/mock_sta_uploader.ex
defmodule StaConnector.Testing.MockSTAUploader do
  @moduledoc """
  Mock STA uploader that simulates BCB upload responses.
  Supports configurable delays and response scenarios.
  """

  use GenServer
  require Logger

  @default_delay_ms 500

  defmodule UploadResult do
    defstruct [:protocol, :status, :message, :processed_at]
  end

  # Client API

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def request_protocol(system_id) do
    GenServer.call(__MODULE__, {:request_protocol, system_id})
  end

  def upload_file(protocol, content, opts \\ []) do
    GenServer.call(__MODULE__, {:upload_file, protocol, content, opts}, 30_000)
  end

  def configure(opts) do
    GenServer.call(__MODULE__, {:configure, opts})
  end

  # Server Callbacks

  @impl true
  def init(_opts) do
    state = %{
      protocols: %{},
      delay_ms: @default_delay_ms,
      success_rate: 0.9,  # 90% success rate
      pending_rate: 0.05  # 5% pending rate
    }
    {:ok, state}
  end

  @impl true
  def handle_call({:request_protocol, system_id}, _from, state) do
    protocol = generate_protocol(system_id)
    protocols = Map.put(state.protocols, protocol, %{system_id: system_id, requested_at: DateTime.utc_now()})
    {:reply, {:ok, protocol}, %{state | protocols: protocols}}
  end

  @impl true
  def handle_call({:upload_file, protocol, content, opts}, _from, state) do
    delay = Keyword.get(opts, :delay_ms, state.delay_ms)

    # Simulate processing delay
    if delay > 0, do: Process.sleep(delay)

    result = determine_result(protocol, content, state, opts)

    {:reply, {:ok, result}, state}
  end

  @impl true
  def handle_call({:configure, opts}, _from, state) do
    state = Enum.reduce(opts, state, fn
      {:delay_ms, v}, s -> %{s | delay_ms: v}
      {:success_rate, v}, s -> %{s | success_rate: v}
      {:pending_rate, v}, s -> %{s | pending_rate: v}
      _, s -> s
    end)
    {:reply, :ok, state}
  end

  # Private Functions

  defp generate_protocol(system_id) do
    date = Date.utc_today() |> Calendar.strftime("%Y%m%d")
    seq = :rand.uniform(999999) |> Integer.to_string() |> String.pad_leading(6, "0")
    "#{date}#{system_id}O#{seq}"
  end

  defp determine_result(protocol, content, state, opts) do
    status = cond do
      Keyword.get(opts, :force_status) -> Keyword.get(opts, :force_status)
      content =~ "FORCE_REJECT" -> "RJCT"
      content =~ "FORCE_PENDING" -> "PDNG"
      :rand.uniform() <= state.success_rate -> "ACSC"
      :rand.uniform() <= state.pending_rate / (1 - state.success_rate) -> "PDNG"
      true -> "RJCT"
    end

    message = case status do
      "ACSC" -> "Arquivo processado com sucesso"
      "PDNG" -> "Arquivo em processamento"
      "RJCT" -> "Erro de validação no arquivo"
    end

    %UploadResult{
      protocol: protocol,
      status: status,
      message: message,
      processed_at: DateTime.utc_now()
    }
  end
end
```

**Step 4: Run test to verify it passes**

```bash
cd backend && mix test test/sta_connector/testing/mock_sta_uploader_test.exs
```

Expected: PASS

**Step 5: Commit**

```bash
git add backend/lib/sta_connector/testing/mock_sta_uploader.ex backend/test/sta_connector/testing/mock_sta_uploader_test.exs
git commit -m "feat: add MockSTAUploader for simulating BCB uploads"
```

---

### Task 1.11: Build WebhookNotifier

**Files:**
- Create: `backend/lib/sta_connector/webhooks/notifier.ex`
- Create: `backend/test/sta_connector/webhooks/notifier_test.exs`

**Step 1: Write failing test**

```elixir
# backend/test/sta_connector/webhooks/notifier_test.exs
defmodule StaConnector.Webhooks.NotifierTest do
  use ExUnit.Case, async: false

  alias StaConnector.Webhooks.Notifier

  setup do
    # Start a simple HTTP server to receive webhooks
    bypass = Bypass.open()
    {:ok, bypass: bypass}
  end

  describe "notify/2" do
    test "sends webhook to callback URL", %{bypass: bypass} do
      Bypass.expect_once(bypass, "POST", "/webhook", fn conn ->
        {:ok, body, conn} = Plug.Conn.read_body(conn)
        payload = Jason.decode!(body)

        assert payload["protocol"] == "20260201CCS0001"
        assert payload["status"] == "ACSC"

        Plug.Conn.resp(conn, 200, ~s({"ok": true}))
      end)

      callback_url = "http://localhost:#{bypass.port}/webhook"
      payload = %{
        protocol: "20260201CCS0001",
        status: "ACSC",
        file_id: "uuid-123"
      }

      assert {:ok, _response} = Notifier.notify(callback_url, payload)
    end
  end
end
```

**Step 2: Run test to verify it fails**

```bash
cd backend && mix test test/sta_connector/webhooks/notifier_test.exs
```

Expected: FAIL

**Step 3: Implement WebhookNotifier**

```elixir
# backend/lib/sta_connector/webhooks/notifier.ex
defmodule StaConnector.Webhooks.Notifier do
  @moduledoc """
  Sends webhook notifications to FluxiQ STA callback URLs.
  """

  require Logger

  @timeout_ms 30_000
  @max_retries 3

  def notify(callback_url, payload, opts \\ []) do
    retries = Keyword.get(opts, :max_retries, @max_retries)
    timeout = Keyword.get(opts, :timeout_ms, @timeout_ms)

    body = Jason.encode!(%{
      protocol: payload.protocol,
      status: payload.status,
      file_id: payload[:file_id],
      processed_at: payload[:processed_at] || DateTime.utc_now() |> DateTime.to_iso8601(),
      metadata: payload[:metadata]
    })

    headers = [
      {"Content-Type", "application/json"},
      {"X-Webhook-Signature", sign_payload(body)},
      {"User-Agent", "STAConnector/1.0"}
    ]

    do_request(callback_url, body, headers, timeout, retries)
  end

  defp do_request(url, body, headers, timeout, retries_left) do
    case Finch.build(:post, url, headers, body)
         |> Finch.request(StaConnector.Finch, receive_timeout: timeout) do
      {:ok, %{status: status}} when status in 200..299 ->
        Logger.info("[WebhookNotifier] Successfully notified #{url}")
        {:ok, %{status: status}}

      {:ok, %{status: status}} when retries_left > 0 ->
        Logger.warning("[WebhookNotifier] Got #{status} from #{url}, retrying...")
        Process.sleep(1000 * (@max_retries - retries_left + 1))
        do_request(url, body, headers, timeout, retries_left - 1)

      {:ok, %{status: status}} ->
        Logger.error("[WebhookNotifier] Failed to notify #{url}: status #{status}")
        {:error, {:http_error, status}}

      {:error, reason} when retries_left > 0 ->
        Logger.warning("[WebhookNotifier] Error notifying #{url}: #{inspect(reason)}, retrying...")
        Process.sleep(1000 * (@max_retries - retries_left + 1))
        do_request(url, body, headers, timeout, retries_left - 1)

      {:error, reason} ->
        Logger.error("[WebhookNotifier] Failed to notify #{url}: #{inspect(reason)}")
        {:error, reason}
    end
  end

  defp sign_payload(body) do
    secret = Application.get_env(:sta_connector, :webhook_secret, "default-secret")
    :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower)
  end
end
```

**Step 4: Add Bypass dependency to mix.exs for testing**

```elixir
# In mix.exs deps
{:bypass, "~> 2.1", only: :test}
```

**Step 5: Run test to verify it passes**

```bash
cd backend && mix deps.get && mix test test/sta_connector/webhooks/notifier_test.exs
```

Expected: PASS

**Step 6: Commit**

```bash
git add backend/lib/sta_connector/webhooks/notifier.ex backend/test/sta_connector/webhooks/notifier_test.exs backend/mix.exs
git commit -m "feat: add WebhookNotifier for FluxiQ STA callbacks"
```

---

### Task 1.12: Update Poller to Use TestFileServer

**Files:**
- Modify: `backend/lib/sta_connector/inbound/poller.ex`
- Create: `backend/test/sta_connector/inbound/poller_integration_test.exs`

**Step 1: Write integration test**

```elixir
# backend/test/sta_connector/inbound/poller_integration_test.exs
defmodule StaConnector.Inbound.PollerIntegrationTest do
  use StaConnector.DataCase, async: false

  alias StaConnector.Inbound.Poller
  alias StaConnector.Testing.FileServer

  setup do
    FileServer.reset()
    :ok
  end

  describe "poll with TestFileServer" do
    test "discovers and processes test files" do
      # Subscribe to events
      Phoenix.PubSub.subscribe(StaConnector.PubSub, "inbound:events")

      # Trigger poll
      Poller.poll_now()

      # Wait for file discovery event
      assert_receive {:file_discovered, file}, 5000
      assert file.system_id in ~w(CCS CIR SPI STR CMP CAM LDL DICT)
    end
  end
end
```

**Step 2: Update Poller to support test mode**

In `backend/lib/sta_connector/inbound/poller.ex`, add a configuration option to use TestFileServer:

```elixir
# In load_config/0, add:
test_mode: Application.get_env(:sta_connector, :test_mode, false)

# In do_poll/1, replace Client.list_available_files with:
defp list_files(state) do
  if state.test_mode do
    StaConnector.Testing.FileServer.list_available(state.system_ids)
  else
    Client.list_available_files(state.system_ids)
  end
end
```

**Step 3: Run integration test**

```bash
cd backend && mix test test/sta_connector/inbound/poller_integration_test.exs
```

**Step 4: Commit**

```bash
git add backend/lib/sta_connector/inbound/poller.ex backend/test/sta_connector/inbound/poller_integration_test.exs
git commit -m "feat: add test mode support to Poller with TestFileServer"
```

---

## Wave 2: Backend Channels

### Task 2.1: Fix health:status Channel

**Files:**
- Modify: `backend/lib/sta_connector_web/channels/health_channel.ex`
- Create: `backend/test/sta_connector_web/channels/health_channel_test.exs`

**Step 1: Write channel test**

```elixir
# backend/test/sta_connector_web/channels/health_channel_test.exs
defmodule StaConnectorWeb.HealthChannelTest do
  use StaConnectorWeb.ChannelCase

  setup do
    {:ok, _, socket} =
      StaConnectorWeb.UserSocket
      |> socket()
      |> subscribe_and_join(StaConnectorWeb.HealthChannel, "health:status")

    %{socket: socket}
  end

  test "joins and receives health status", %{socket: _socket} do
    assert_push "health_update", payload
    assert payload.status in ["healthy", "degraded", "unhealthy"]
    assert is_list(payload.components)
  end

  test "get_health returns current status", %{socket: socket} do
    ref = push(socket, "get_health", %{})
    assert_reply ref, :ok, payload
    assert payload.status in ["healthy", "degraded", "unhealthy"]
  end
end
```

**Step 2: Update HealthChannel with real checks**

```elixir
# backend/lib/sta_connector_web/channels/health_channel.ex
defmodule StaConnectorWeb.HealthChannel do
  use StaConnectorWeb, :channel
  require Logger

  @health_interval_ms 10_000

  @impl true
  def join("health:status", _params, socket) do
    send(self(), :check_health)
    {:ok, assign(socket, :timer_ref, nil)}
  end

  @impl true
  def handle_info(:check_health, socket) do
    health = perform_health_check()
    push(socket, "health_update", health)

    timer_ref = Process.send_after(self(), :check_health, @health_interval_ms)
    {:noreply, assign(socket, :timer_ref, timer_ref)}
  end

  @impl true
  def handle_in("get_health", _params, socket) do
    health = perform_health_check()
    {:reply, {:ok, health}, socket}
  end

  @impl true
  def terminate(_reason, socket) do
    if socket.assigns[:timer_ref], do: Process.cancel_timer(socket.assigns.timer_ref)
    :ok
  end

  defp perform_health_check do
    components = [
      check_database(),
      check_sta_connection(),
      check_poller()
    ]

    overall_status = determine_overall_status(components)

    %{
      status: overall_status,
      version: Application.spec(:sta_connector, :vsn) |> to_string(),
      uptime_seconds: get_uptime_seconds(),
      components: components,
      checked_at: DateTime.utc_now() |> DateTime.to_iso8601()
    }
  end

  defp check_database do
    start = System.monotonic_time(:millisecond)
    result = try do
      StaConnector.Repo.query!("SELECT 1")
      :up
    rescue
      _ -> :down
    end
    latency = System.monotonic_time(:millisecond) - start

    %{name: "database", status: result, latency_ms: latency}
  end

  defp check_sta_connection do
    # Check if STA client can connect (mock in test mode)
    status = if Application.get_env(:sta_connector, :test_mode, false) do
      :up
    else
      case StaConnector.Sta.Client.health_check() do
        {:ok, _} -> :up
        {:error, _} -> :down
      end
    end

    %{name: "sta_connection", status: status}
  end

  defp check_poller do
    status = try do
      case StaConnector.Inbound.Poller.status() do
        %{enabled: true} -> :up
        %{enabled: false} -> :degraded
      end
    rescue
      _ -> :down
    end

    %{name: "poller", status: status}
  end

  defp determine_overall_status(components) do
    cond do
      Enum.all?(components, &(&1.status == :up)) -> "healthy"
      Enum.any?(components, &(&1.status == :down)) -> "unhealthy"
      true -> "degraded"
    end
  end

  defp get_uptime_seconds do
    {uptime, _} = :erlang.statistics(:wall_clock)
    div(uptime, 1000)
  end
end
```

**Step 3: Run test**

```bash
cd backend && mix test test/sta_connector_web/channels/health_channel_test.exs
```

**Step 4: Commit**

```bash
git add backend/lib/sta_connector_web/channels/health_channel.ex backend/test/sta_connector_web/channels/health_channel_test.exs
git commit -m "fix: health channel with real database and component checks"
```

---

### Task 2.2: Fix metrics:lobby Channel

**Files:**
- Modify: `backend/lib/sta_connector_web/channels/metrics_channel.ex`
- Create: `backend/test/sta_connector_web/channels/metrics_channel_test.exs`

Real metrics from database file counts, Poller stats, rate limiter.

---

### Task 2.3: Fix files:lobby Channel

**Files:**
- Modify: `backend/lib/sta_connector_web/channels/files_channel.ex`

Connect to real PubSub events from Poller and Router. Broadcast file events when files are processed.

---

### Task 2.4: Fix logs:stream Channel

**Files:**
- Create: `backend/lib/sta_connector/log_backend.ex`
- Modify: `backend/lib/sta_connector_web/channels/logs_channel.ex`

Create an Elixir Logger backend that broadcasts logs to the channel.

---

### Task 2.5: Create admin:config Channel

**Files:**
- Create: `backend/lib/sta_connector_web/channels/config_channel.ex`

Real-time config updates, read/write operations.

---

### Task 2.6: Create notifications:alerts Channel

**Files:**
- Create: `backend/lib/sta_connector_web/channels/notifications_channel.ex`

Broadcast alerts for file failures, system issues.

---

## Wave 3: Frontend Real Integration

### Task 3.1: Remove Mocks from Dashboard

**Files:**
- Modify: `frontend/src/views/DashboardView.vue`
- Modify: `frontend/src/composables/useMetrics.ts`

**Step 1: Update DashboardView.vue**

Remove `loadMockData()` function and all mock data. Keep only real channel connections.

```typescript
// Remove entirely:
// const loadMockData = () => { ... }

// Remove mock data timeout:
// setTimeout(() => { if (isLoading.value ...) loadMockData() }, 1000)

// Add proper error handling:
onMounted(() => {
  onMetrics('metrics_update', handleMetricsUpdate)
  onHealth('health_update', handleHealthUpdate)

  // Show loading state until real data arrives
  isLoading.value = true
})
```

**Step 2: Update error display**

Add proper error states when channels fail to connect instead of falling back to mock data.

---

### Task 3.2: Remove Mocks from Files View

**Files:**
- Modify: `frontend/src/views/FilesView.vue`
- Modify: `frontend/src/composables/useFiles.ts`

Real file list from REST API + real-time updates from files:lobby channel.

---

### Task 3.3: Remove Mocks from Config View

**Files:**
- Modify: `frontend/src/views/ConfigView.vue`

Real config loading from REST API.

---

### Task 3.4: Fix Logs View (Complete the Work)

**Files:**
- Modify: `frontend/src/views/LogsView.vue`
- Modify: `frontend/src/composables/useLogs.ts`

Replace the mock log generator with real logs:stream channel.

```typescript
// In useLogs.ts, restore channel connection:
const { joined, on, push, offlineMode } = useChannel<LogChannelResponse>(
  'logs:stream',
  { level },
  {
    autoJoin: true,
    joinTimeout: 5000,
    onError: (err) => {
      error.value = `Failed to connect: ${err}`
      loading.value = false
    }
  }
)

// Remove mock data generation entirely
// Remove startLogSimulation()
// Remove generateMockLogs()
```

---

### Task 3.5: Add Toast Notification System

**Files:**
- Modify: `frontend/src/components/notifications/ToastContainer.vue`
- Modify: `frontend/src/composables/useNotifications.ts`
- Modify: `frontend/src/App.vue`

Connect to notifications:alerts channel. Show toasts for:
- File upload complete
- File processing failed
- System health changes

---

### Task 3.6: Add Sound Alerts and Badge Counters

**Files:**
- Modify: `frontend/src/utils/audio.ts`
- Modify: `frontend/src/App.vue` (sidebar badge counters)

Play sound on errors (configurable). Show unread notification count on sidebar.

---

## E2E Test Suite

### Task 4.1: Inbound Flow E2E Test

**Files:**
- Create: `backend/test/e2e/inbound_flow_test.exs`

Test complete flow: TestFileServer → Poller → Parser → Router → WebhookNotifier → Database.

```elixir
defmodule StaConnector.E2E.InboundFlowTest do
  use StaConnector.DataCase, async: false

  setup do
    # Start test infrastructure
    start_supervised!(StaConnector.Testing.FileServer)
    start_supervised!(StaConnector.Testing.WebhookReceiver)

    StaConnector.Testing.FileServer.reset()
    :ok
  end

  test "complete inbound flow" do
    # Subscribe to events
    Phoenix.PubSub.subscribe(StaConnector.PubSub, "inbound:events")

    # Trigger poll
    StaConnector.Inbound.Poller.poll_now()

    # Wait for file discovery
    assert_receive {:file_discovered, file}, 5000

    # Wait for download complete
    assert_receive {:download_completed, protocol, :success}, 10000

    # Verify file in database
    assert StaConnector.Files.protocol_exists?(protocol)

    # Verify webhook was called
    assert StaConnector.Testing.WebhookReceiver.received_count() > 0
  end
end
```

---

### Task 4.2: Outbound Flow E2E Test

**Files:**
- Create: `backend/test/e2e/outbound_flow_test.exs`

Test complete flow: API upload → Validator → MockSTAUploader → WebhookNotifier → Database.

---

## Summary

**Total Tasks:** 20 across 4 waves
**Total Files:** ~80 test files + ~15 Elixir modules + ~10 Vue components

**Wave 1 (8 tasks):** Test file creation and infrastructure
**Wave 2 (6 tasks):** Backend Phoenix channels with real data
**Wave 3 (6 tasks):** Frontend Vue integration with no mocks
**Wave 4 (2 tasks):** E2E test suites

---

**Plan complete and saved to `docs/plans/2026-02-01-test-files-and-real-integration.md`.**

Two execution options:

**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration

**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints

Which approach?
