# STA Connector Elixir Rewrite - Implementation Plan

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

**Goal:** Complete rewrite of the Go-based STA Connector to Elixir/Phoenix with Vue frontend, leveraging OTP for fault tolerance and Phoenix Channels for real-time features.

**Architecture:** Single Phoenix app with well-organized contexts (Sta.Client, Sta.Files, Sta.Inbound, Sta.Outbound, Sta.Admin, Sta.Parsers). PostgreSQL with Ecto for persistence, Phoenix Channels for real-time WebSocket communication, Vue 3 frontend with Pinia state management, VitePress for documentation.

**Tech Stack:** Elixir 1.16+, Phoenix 1.7+, Ecto 3.11+, PostgreSQL 15+, Vue 3, Vite, Pinia, TailwindCSS, VitePress, Docker

---

## Parallel Execution Strategy

This plan is designed for **maximum parallelism**. Tasks are grouped into waves that can execute simultaneously:

### Wave 1: Foundation (8 parallel agents)
| Agent | Task | Description |
|-------|------|-------------|
| 1 | Project Scaffold | Phoenix app, monorepo structure |
| 2 | Database Schema | Ecto migrations, schemas |
| 3 | Config System | Runtime configuration with hot-reload |
| 4 | STA Client Core | HTTP client, SOAP handling |
| 5 | Vue Frontend Scaffold | Vite, Vue 3, Pinia, TailwindCSS |
| 6 | VitePress Docs | Documentation site scaffold |
| 7 | Docker Setup | Dockerfile, docker-compose |
| 8 | CI/CD Pipeline | GitHub Actions workflows |

### Wave 2: Core Features (8 parallel agents)
| Agent | Task | Description |
|-------|------|-------------|
| 1 | STA Operations | All STA WebService operations |
| 2 | Inbound Pipeline | GenServer polling workers |
| 3 | Outbound API | File upload REST endpoints |
| 4 | Admin API | Configuration, metrics, health |
| 5 | Parser Registry | BCB system parsers (CCS, CIR, etc.) |
| 6 | Phoenix Channels | Real-time WebSocket infrastructure |
| 7 | Vue Dashboard | Health, metrics components |
| 8 | Vue File Manager | File listing, status, retry |

### Wave 3: Integration & Polish (6 parallel agents)
| Agent | Task | Description |
|-------|------|-------------|
| 1 | Channel Integration | Connect Vue to Phoenix Channels |
| 2 | Live Metrics | Real-time metrics streaming |
| 3 | Live Logs | Log streaming to frontend |
| 4 | Notifications | Push notification system |
| 5 | E2E Tests | Integration test suite |
| 6 | Documentation | Complete all docs |

---

## Wave 1: Foundation

### Task 1.1: Phoenix Project Scaffold

**Files:**
- Create: `backend/mix.exs`
- Create: `backend/config/config.exs`
- Create: `backend/config/dev.exs`
- Create: `backend/config/prod.exs`
- Create: `backend/config/runtime.exs`
- Create: `backend/config/test.exs`
- Create: `backend/lib/sta_connector/application.ex`
- Create: `backend/lib/sta_connector_web/endpoint.ex`
- Create: `backend/lib/sta_connector_web/router.ex`

**Step 1: Create monorepo structure**

```bash
mkdir -p backend frontend docs
```

**Step 2: Generate Phoenix application**

```bash
cd backend
mix phx.new sta_connector --no-html --no-assets --no-mailer --no-dashboard --no-live --binary-id
```

**Step 3: Configure mix.exs with dependencies**

```elixir
# backend/mix.exs
defmodule StaConnector.MixProject do
  use Mix.Project

  def project do
    [
      app: :sta_connector,
      version: "1.0.0",
      elixir: "~> 1.16",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps()
    ]
  end

  def application do
    [
      mod: {StaConnector.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp deps do
    [
      {:phoenix, "~> 1.7.14"},
      {:phoenix_ecto, "~> 4.5"},
      {:ecto_sql, "~> 3.11"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_live_dashboard, "~> 0.8.4"},
      {:telemetry_metrics, "~> 1.0"},
      {:telemetry_poller, "~> 1.0"},
      {:jason, "~> 1.4"},
      {:plug_cowboy, "~> 2.7"},
      {:cors_plug, "~> 3.0"},
      {:finch, "~> 0.18"},
      {:sweet_xml, "~> 0.7"},
      {:nimble_options, "~> 1.1"},
      {:ex_doc, "~> 0.31", only: :dev, runtime: false},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      {:mox, "~> 1.1", only: :test}
    ]
  end

  defp aliases do
    [
      setup: ["deps.get", "ecto.setup"],
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
    ]
  end
end
```

**Step 4: Install dependencies and verify**

```bash
cd backend && mix deps.get && mix compile
```

**Step 5: Commit**

```bash
git add backend/
git commit -m "feat: initialize Phoenix backend scaffold"
```

---

### Task 1.2: Database Schema & Ecto Setup

**Files:**
- Create: `backend/lib/sta_connector/repo.ex`
- Create: `backend/priv/repo/migrations/20260131000001_create_processed_protocols.exs`
- Create: `backend/priv/repo/migrations/20260131000002_create_outbound_files.exs`
- Create: `backend/priv/repo/migrations/20260131000003_create_retry_queue.exs`
- Create: `backend/priv/repo/migrations/20260131000004_create_config_overrides.exs`
- Create: `backend/lib/sta_connector/files/processed_protocol.ex`
- Create: `backend/lib/sta_connector/files/outbound_file.ex`
- Create: `backend/lib/sta_connector/files/retry_entry.ex`
- Create: `backend/lib/sta_connector/admin/config_override.ex`

**Step 1: Create Repo module**

```elixir
# backend/lib/sta_connector/repo.ex
defmodule StaConnector.Repo do
  use Ecto.Repo,
    otp_app: :sta_connector,
    adapter: Ecto.Adapters.Postgres
end
```

**Step 2: Create processed_protocols migration**

```elixir
# backend/priv/repo/migrations/20260131000001_create_processed_protocols.exs
defmodule StaConnector.Repo.Migrations.CreateProcessedProtocols do
  use Ecto.Migration

  def change do
    create table(:processed_protocols, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :protocol_number, :string, null: false
      add :system_id, :string, null: false
      add :file_name, :string, null: false
      add :file_size, :bigint
      add :status, :string, null: false, default: "pending"
      add :direction, :string, null: false
      add :route_id, :string
      add :parsed_data, :map
      add :error_message, :text
      add :processed_at, :utc_datetime_usec
      add :metadata, :map, default: %{}

      timestamps(type: :utc_datetime_usec)
    end

    create unique_index(:processed_protocols, [:protocol_number])
    create index(:processed_protocols, [:system_id])
    create index(:processed_protocols, [:status])
    create index(:processed_protocols, [:direction])
    create index(:processed_protocols, [:inserted_at])
  end
end
```

**Step 3: Create outbound_files migration**

```elixir
# backend/priv/repo/migrations/20260131000002_create_outbound_files.exs
defmodule StaConnector.Repo.Migrations.CreateOutboundFiles do
  use Ecto.Migration

  def change do
    create table(:outbound_files, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :request_id, :string, null: false
      add :system_id, :string, null: false
      add :file_name, :string, null: false
      add :file_size, :bigint
      add :content_hash, :string
      add :status, :string, null: false, default: "pending"
      add :protocol_number, :string
      add :upload_attempts, :integer, default: 0
      add :last_error, :text
      add :metadata, :map, default: %{}

      timestamps(type: :utc_datetime_usec)
    end

    create unique_index(:outbound_files, [:request_id])
    create index(:outbound_files, [:system_id])
    create index(:outbound_files, [:status])
  end
end
```

**Step 4: Create retry_queue migration**

```elixir
# backend/priv/repo/migrations/20260131000003_create_retry_queue.exs
defmodule StaConnector.Repo.Migrations.CreateRetryQueue do
  use Ecto.Migration

  def change do
    create table(:retry_queue, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :protocol_id, references(:processed_protocols, type: :binary_id, on_delete: :delete_all)
      add :outbound_id, references(:outbound_files, type: :binary_id, on_delete: :delete_all)
      add :retry_count, :integer, default: 0
      add :max_retries, :integer, default: 3
      add :next_retry_at, :utc_datetime_usec
      add :last_error, :text
      add :status, :string, default: "pending"

      timestamps(type: :utc_datetime_usec)
    end

    create index(:retry_queue, [:status])
    create index(:retry_queue, [:next_retry_at])
  end
end
```

**Step 5: Create config_overrides migration**

```elixir
# backend/priv/repo/migrations/20260131000004_create_config_overrides.exs
defmodule StaConnector.Repo.Migrations.CreateConfigOverrides do
  use Ecto.Migration

  def change do
    create table(:config_overrides, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :key, :string, null: false
      add :value, :map, null: false
      add :description, :text
      add :enabled, :boolean, default: true

      timestamps(type: :utc_datetime_usec)
    end

    create unique_index(:config_overrides, [:key])
  end
end
```

**Step 6: Create Ecto schemas**

```elixir
# backend/lib/sta_connector/files/processed_protocol.ex
defmodule StaConnector.Files.ProcessedProtocol do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  @statuses ~w(pending downloading processing completed failed)
  @directions ~w(inbound outbound)

  schema "processed_protocols" do
    field :protocol_number, :string
    field :system_id, :string
    field :file_name, :string
    field :file_size, :integer
    field :status, :string, default: "pending"
    field :direction, :string
    field :route_id, :string
    field :parsed_data, :map
    field :error_message, :string
    field :processed_at, :utc_datetime_usec
    field :metadata, :map, default: %{}

    has_many :retry_entries, StaConnector.Files.RetryEntry, foreign_key: :protocol_id

    timestamps(type: :utc_datetime_usec)
  end

  def changeset(protocol, attrs) do
    protocol
    |> cast(attrs, [:protocol_number, :system_id, :file_name, :file_size, :status,
                    :direction, :route_id, :parsed_data, :error_message, :processed_at, :metadata])
    |> validate_required([:protocol_number, :system_id, :file_name, :direction])
    |> validate_inclusion(:status, @statuses)
    |> validate_inclusion(:direction, @directions)
    |> unique_constraint(:protocol_number)
  end
end
```

```elixir
# backend/lib/sta_connector/files/outbound_file.ex
defmodule StaConnector.Files.OutboundFile do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  @statuses ~w(pending uploading completed failed)

  schema "outbound_files" do
    field :request_id, :string
    field :system_id, :string
    field :file_name, :string
    field :file_size, :integer
    field :content_hash, :string
    field :status, :string, default: "pending"
    field :protocol_number, :string
    field :upload_attempts, :integer, default: 0
    field :last_error, :string
    field :metadata, :map, default: %{}

    has_many :retry_entries, StaConnector.Files.RetryEntry, foreign_key: :outbound_id

    timestamps(type: :utc_datetime_usec)
  end

  def changeset(file, attrs) do
    file
    |> cast(attrs, [:request_id, :system_id, :file_name, :file_size, :content_hash,
                    :status, :protocol_number, :upload_attempts, :last_error, :metadata])
    |> validate_required([:request_id, :system_id, :file_name])
    |> validate_inclusion(:status, @statuses)
    |> unique_constraint(:request_id)
  end
end
```

```elixir
# backend/lib/sta_connector/files/retry_entry.ex
defmodule StaConnector.Files.RetryEntry do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "retry_queue" do
    belongs_to :protocol, StaConnector.Files.ProcessedProtocol
    belongs_to :outbound, StaConnector.Files.OutboundFile

    field :retry_count, :integer, default: 0
    field :max_retries, :integer, default: 3
    field :next_retry_at, :utc_datetime_usec
    field :last_error, :string
    field :status, :string, default: "pending"

    timestamps(type: :utc_datetime_usec)
  end

  def changeset(entry, attrs) do
    entry
    |> cast(attrs, [:protocol_id, :outbound_id, :retry_count, :max_retries,
                    :next_retry_at, :last_error, :status])
    |> validate_number(:retry_count, greater_than_or_equal_to: 0)
  end
end
```

```elixir
# backend/lib/sta_connector/admin/config_override.ex
defmodule StaConnector.Admin.ConfigOverride do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "config_overrides" do
    field :key, :string
    field :value, :map
    field :description, :string
    field :enabled, :boolean, default: true

    timestamps(type: :utc_datetime_usec)
  end

  def changeset(override, attrs) do
    override
    |> cast(attrs, [:key, :value, :description, :enabled])
    |> validate_required([:key, :value])
    |> unique_constraint(:key)
  end
end
```

**Step 7: Run migrations and verify**

```bash
cd backend && mix ecto.create && mix ecto.migrate
```

**Step 8: Commit**

```bash
git add backend/lib/sta_connector/repo.ex backend/priv/repo/migrations/ backend/lib/sta_connector/files/ backend/lib/sta_connector/admin/
git commit -m "feat: add database schema and Ecto models"
```

---

### Task 1.3: Configuration System with Hot-Reload

**Files:**
- Create: `backend/lib/sta_connector/config/settings.ex`
- Create: `backend/lib/sta_connector/config/loader.ex`
- Create: `backend/lib/sta_connector/config/watcher.ex`
- Create: `backend/lib/sta_connector/config/route.ex`
- Create: `backend/test/sta_connector/config/settings_test.exs`

**Step 1: Create Settings schema**

```elixir
# backend/lib/sta_connector/config/settings.ex
defmodule StaConnector.Config.Settings do
  @moduledoc """
  Runtime configuration for the STA Connector.
  Supports hot-reload via ETS-backed GenServer.
  """

  use GenServer
  require Logger

  @table :sta_config
  @default_config %{
    sta: %{
      environment: :homologation,
      homologation_url: "https://sta-h.bcb.gov.br/staws",
      production_url: "https://sta.bcb.gov.br/staws",
      timeout_ms: 30_000,
      max_retries: 3,
      retry_delay_ms: 1_000
    },
    poller: %{
      enabled: true,
      interval_ms: 60_000,
      batch_size: 10,
      worker_count: 4
    },
    rate_limit: %{
      max_concurrent: 10,
      max_queries_per_minute: 120
    },
    routes: []
  }

  # Client API

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

  def get(key) when is_atom(key) do
    case :ets.lookup(@table, key) do
      [{^key, value}] -> {:ok, value}
      [] -> {:error, :not_found}
    end
  end

  def get!(key) when is_atom(key) do
    case get(key) do
      {:ok, value} -> value
      {:error, :not_found} -> raise "Config key not found: #{key}"
    end
  end

  def get_all do
    @table
    |> :ets.tab2list()
    |> Map.new()
  end

  def put(key, value) when is_atom(key) do
    GenServer.call(__MODULE__, {:put, key, value})
  end

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

  # Server callbacks

  @impl true
  def init(opts) do
    table = :ets.new(@table, [:set, :public, :named_table, read_concurrency: true])
    config_path = Keyword.get(opts, :config_path, config_file_path())

    config = load_config(config_path)
    store_config(config)

    {:ok, %{table: table, config_path: config_path}}
  end

  @impl true
  def handle_call({:put, key, value}, _from, state) do
    :ets.insert(@table, {key, value})
    broadcast_change(key, value)
    {:reply, :ok, state}
  end

  @impl true
  def handle_call(:reload, _from, %{config_path: path} = state) do
    config = load_config(path)
    store_config(config)
    broadcast_change(:all, config)
    {:reply, :ok, state}
  end

  # Private functions

  defp config_file_path do
    Application.get_env(:sta_connector, :config_path, "config/sta_config.json")
  end

  defp load_config(path) do
    case File.read(path) do
      {:ok, content} ->
        case Jason.decode(content, keys: :atoms) do
          {:ok, config} -> Map.merge(@default_config, config)
          {:error, _} ->
            Logger.warning("Invalid config file, using defaults")
            @default_config
        end
      {:error, _} ->
        Logger.info("No config file found at #{path}, using defaults")
        @default_config
    end
  end

  defp store_config(config) do
    Enum.each(config, fn {key, value} ->
      :ets.insert(@table, {key, value})
    end)
  end

  defp broadcast_change(key, value) do
    Phoenix.PubSub.broadcast(
      StaConnector.PubSub,
      "config:changes",
      {:config_changed, key, value}
    )
  end
end
```

**Step 2: Create Route schema**

```elixir
# backend/lib/sta_connector/config/route.ex
defmodule StaConnector.Config.Route do
  @moduledoc """
  Represents a file routing configuration for BCB systems.
  """

  @enforce_keys [:id, :system_id, :direction]
  defstruct [
    :id,
    :system_id,
    :direction,
    :name,
    :description,
    :destination_url,
    :enabled,
    :parser,
    :transform,
    :retry_policy,
    metadata: %{}
  ]

  @type t :: %__MODULE__{
    id: String.t(),
    system_id: String.t(),
    direction: :inbound | :outbound,
    name: String.t() | nil,
    description: String.t() | nil,
    destination_url: String.t() | nil,
    enabled: boolean(),
    parser: String.t() | nil,
    transform: map() | nil,
    retry_policy: map() | nil,
    metadata: map()
  }

  def new(attrs) when is_map(attrs) do
    %__MODULE__{
      id: Map.fetch!(attrs, :id),
      system_id: Map.fetch!(attrs, :system_id),
      direction: parse_direction(attrs[:direction]),
      name: attrs[:name],
      description: attrs[:description],
      destination_url: attrs[:destination_url],
      enabled: Map.get(attrs, :enabled, true),
      parser: attrs[:parser],
      transform: attrs[:transform],
      retry_policy: attrs[:retry_policy],
      metadata: Map.get(attrs, :metadata, %{})
    }
  end

  defp parse_direction("inbound"), do: :inbound
  defp parse_direction("outbound"), do: :outbound
  defp parse_direction(:inbound), do: :inbound
  defp parse_direction(:outbound), do: :outbound
end
```

**Step 3: Create config file watcher**

```elixir
# backend/lib/sta_connector/config/watcher.ex
defmodule StaConnector.Config.Watcher do
  @moduledoc """
  Watches configuration file for changes and triggers hot-reload.
  """

  use GenServer
  require Logger

  @check_interval 5_000

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

  @impl true
  def init(opts) do
    config_path = Keyword.get(opts, :config_path, "config/sta_config.json")
    mtime = get_mtime(config_path)
    schedule_check()
    {:ok, %{config_path: config_path, last_mtime: mtime}}
  end

  @impl true
  def handle_info(:check, %{config_path: path, last_mtime: last_mtime} = state) do
    current_mtime = get_mtime(path)

    state =
      if current_mtime != last_mtime and current_mtime != nil do
        Logger.info("Config file changed, reloading...")
        StaConnector.Config.Settings.reload()
        %{state | last_mtime: current_mtime}
      else
        state
      end

    schedule_check()
    {:noreply, state}
  end

  defp schedule_check do
    Process.send_after(self(), :check, @check_interval)
  end

  defp get_mtime(path) do
    case File.stat(path) do
      {:ok, %{mtime: mtime}} -> mtime
      {:error, _} -> nil
    end
  end
end
```

**Step 4: Write tests**

```elixir
# backend/test/sta_connector/config/settings_test.exs
defmodule StaConnector.Config.SettingsTest do
  use ExUnit.Case, async: false

  alias StaConnector.Config.Settings

  setup do
    # Settings is started by application, just reset state
    :ok
  end

  describe "get/1" do
    test "returns configured value" do
      assert {:ok, sta_config} = Settings.get(:sta)
      assert is_map(sta_config)
      assert sta_config.environment in [:homologation, :production]
    end

    test "returns error for unknown key" do
      assert {:error, :not_found} = Settings.get(:unknown_key)
    end
  end

  describe "put/2" do
    test "stores and retrieves value" do
      Settings.put(:test_key, %{foo: "bar"})
      assert {:ok, %{foo: "bar"}} = Settings.get(:test_key)
    end
  end

  describe "get_all/0" do
    test "returns all configuration" do
      config = Settings.get_all()
      assert is_map(config)
      assert Map.has_key?(config, :sta)
      assert Map.has_key?(config, :poller)
    end
  end
end
```

**Step 5: Run tests**

```bash
cd backend && mix test test/sta_connector/config/settings_test.exs
```

**Step 6: Commit**

```bash
git add backend/lib/sta_connector/config/ backend/test/sta_connector/config/
git commit -m "feat: add configuration system with hot-reload support"
```

---

### Task 1.4: STA WebService Client Core

**Files:**
- Create: `backend/lib/sta_connector/sta/client.ex`
- Create: `backend/lib/sta_connector/sta/soap.ex`
- Create: `backend/lib/sta_connector/sta/types.ex`
- Create: `backend/lib/sta_connector/sta/error.ex`
- Create: `backend/test/sta_connector/sta/client_test.exs`

**Step 1: Create STA types**

```elixir
# backend/lib/sta_connector/sta/types.ex
defmodule StaConnector.Sta.Types do
  @moduledoc """
  Type definitions for STA WebService responses.
  """

  defmodule FileInfo do
    @enforce_keys [:protocol_number, :system_id, :file_name]
    defstruct [
      :protocol_number,
      :system_id,
      :file_name,
      :file_size,
      :available_since,
      :hash,
      metadata: %{}
    ]

    @type t :: %__MODULE__{
      protocol_number: String.t(),
      system_id: String.t(),
      file_name: String.t(),
      file_size: integer() | nil,
      available_since: DateTime.t() | nil,
      hash: String.t() | nil,
      metadata: map()
    }
  end

  defmodule UploadResult do
    defstruct [:protocol_number, :status, :message, :timestamp]

    @type t :: %__MODULE__{
      protocol_number: String.t() | nil,
      status: :success | :error,
      message: String.t() | nil,
      timestamp: DateTime.t()
    }
  end

  defmodule DownloadResult do
    defstruct [:protocol_number, :content, :file_name, :file_size, :hash]

    @type t :: %__MODULE__{
      protocol_number: String.t(),
      content: binary(),
      file_name: String.t(),
      file_size: integer(),
      hash: String.t() | nil
    }
  end
end
```

**Step 2: Create error types**

```elixir
# backend/lib/sta_connector/sta/error.ex
defmodule StaConnector.Sta.Error do
  @moduledoc """
  Error types for STA WebService operations.
  """

  defexception [:type, :message, :code, :details]

  @type t :: %__MODULE__{
    type: :connection | :timeout | :soap_fault | :auth | :rate_limit | :not_found | :unknown,
    message: String.t(),
    code: String.t() | nil,
    details: map()
  }

  def message(%__MODULE__{type: type, message: msg}) do
    "[STA #{type}] #{msg}"
  end

  def connection(message, details \\ %{}) do
    %__MODULE__{type: :connection, message: message, details: details}
  end

  def timeout(message, details \\ %{}) do
    %__MODULE__{type: :timeout, message: message, details: details}
  end

  def soap_fault(code, message, details \\ %{}) do
    %__MODULE__{type: :soap_fault, message: message, code: code, details: details}
  end

  def auth(message, details \\ %{}) do
    %__MODULE__{type: :auth, message: message, details: details}
  end

  def rate_limit(message, details \\ %{}) do
    %__MODULE__{type: :rate_limit, message: message, details: details}
  end

  def not_found(message, details \\ %{}) do
    %__MODULE__{type: :not_found, message: message, details: details}
  end
end
```

**Step 3: Create SOAP helper**

```elixir
# backend/lib/sta_connector/sta/soap.ex
defmodule StaConnector.Sta.Soap do
  @moduledoc """
  SOAP envelope building and parsing for STA WebService.
  """

  import SweetXml

  @soap_ns "http://schemas.xmlsoap.org/soap/envelope/"
  @sta_ns "http://www.bcb.gov.br/sta"

  def build_envelope(operation, params) do
    body_content = build_body(operation, params)

    """
    <?xml version="1.0" encoding="UTF-8"?>
    <soap:Envelope xmlns:soap="#{@soap_ns}" xmlns:sta="#{@sta_ns}">
      <soap:Header/>
      <soap:Body>
        #{body_content}
      </soap:Body>
    </soap:Envelope>
    """
  end

  defp build_body(:list_available_files, %{system_ids: system_ids}) do
    systems = Enum.map(system_ids, &"<sta:systemId>#{&1}</sta:systemId>") |> Enum.join("\n")
    """
    <sta:ListAvailableFiles>
      #{systems}
    </sta:ListAvailableFiles>
    """
  end

  defp build_body(:request_protocol, %{system_id: system_id, file_name: file_name}) do
    """
    <sta:RequestProtocol>
      <sta:systemId>#{system_id}</sta:systemId>
      <sta:fileName>#{file_name}</sta:fileName>
    </sta:RequestProtocol>
    """
  end

  defp build_body(:download_file, %{protocol_number: protocol}) do
    """
    <sta:DownloadFile>
      <sta:protocolNumber>#{protocol}</sta:protocolNumber>
    </sta:DownloadFile>
    """
  end

  defp build_body(:confirm_receipt, %{protocol_number: protocol}) do
    """
    <sta:ConfirmReceipt>
      <sta:protocolNumber>#{protocol}</sta:protocolNumber>
    </sta:ConfirmReceipt>
    """
  end

  defp build_body(:upload_file, %{system_id: system_id, file_name: file_name, content: content}) do
    encoded = Base.encode64(content)
    """
    <sta:UploadFile>
      <sta:systemId>#{system_id}</sta:systemId>
      <sta:fileName>#{file_name}</sta:fileName>
      <sta:fileContent>#{encoded}</sta:fileContent>
    </sta:UploadFile>
    """
  end

  def parse_response(xml, operation) do
    case xpath(xml, ~x"//soap:Fault"o) do
      nil -> parse_success(xml, operation)
      _fault -> parse_fault(xml)
    end
  end

  defp parse_success(xml, :list_available_files) do
    files =
      xml
      |> xpath(~x"//sta:file"l,
        protocol_number: ~x"./sta:protocolNumber/text()"s,
        system_id: ~x"./sta:systemId/text()"s,
        file_name: ~x"./sta:fileName/text()"s,
        file_size: ~x"./sta:fileSize/text()"io
      )
      |> Enum.map(&struct(StaConnector.Sta.Types.FileInfo, &1))

    {:ok, files}
  end

  defp parse_success(xml, :request_protocol) do
    protocol = xpath(xml, ~x"//sta:protocolNumber/text()"s)
    {:ok, protocol}
  end

  defp parse_success(xml, :download_file) do
    content = xpath(xml, ~x"//sta:fileContent/text()"s) |> Base.decode64!()
    file_name = xpath(xml, ~x"//sta:fileName/text()"s)

    {:ok, %StaConnector.Sta.Types.DownloadResult{
      protocol_number: xpath(xml, ~x"//sta:protocolNumber/text()"s),
      content: content,
      file_name: file_name,
      file_size: byte_size(content)
    }}
  end

  defp parse_success(_xml, :confirm_receipt) do
    {:ok, :confirmed}
  end

  defp parse_success(xml, :upload_file) do
    {:ok, %StaConnector.Sta.Types.UploadResult{
      protocol_number: xpath(xml, ~x"//sta:protocolNumber/text()"s),
      status: :success,
      timestamp: DateTime.utc_now()
    }}
  end

  defp parse_fault(xml) do
    code = xpath(xml, ~x"//faultcode/text()"s)
    message = xpath(xml, ~x"//faultstring/text()"s)
    {:error, StaConnector.Sta.Error.soap_fault(code, message)}
  end
end
```

**Step 4: Create STA Client**

```elixir
# backend/lib/sta_connector/sta/client.ex
defmodule StaConnector.Sta.Client do
  @moduledoc """
  HTTP client for BCB STA WebService.
  Handles connection pooling, retries, and rate limiting.
  """

  use GenServer
  require Logger

  alias StaConnector.Sta.{Soap, Error}
  alias StaConnector.Config.Settings

  @default_timeout 30_000

  # Client API

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

  def list_available_files(system_ids) when is_list(system_ids) do
    call_sta(:list_available_files, %{system_ids: system_ids})
  end

  def request_protocol(system_id, file_name) do
    call_sta(:request_protocol, %{system_id: system_id, file_name: file_name})
  end

  def download_file(protocol_number) do
    call_sta(:download_file, %{protocol_number: protocol_number})
  end

  def confirm_receipt(protocol_number) do
    call_sta(:confirm_receipt, %{protocol_number: protocol_number})
  end

  def upload_file(system_id, file_name, content) do
    call_sta(:upload_file, %{system_id: system_id, file_name: file_name, content: content})
  end

  # GenServer callbacks

  @impl true
  def init(_opts) do
    {:ok, %{request_count: 0, window_start: System.monotonic_time(:millisecond)}}
  end

  @impl true
  def handle_call({:execute, operation, params}, _from, state) do
    state = maybe_reset_window(state)

    if rate_limited?(state) do
      {:reply, {:error, Error.rate_limit("Rate limit exceeded")}, state}
    else
      result = execute_request(operation, params)
      {:reply, result, %{state | request_count: state.request_count + 1}}
    end
  end

  # Private functions

  defp call_sta(operation, params) do
    GenServer.call(__MODULE__, {:execute, operation, params}, @default_timeout)
  end

  defp execute_request(operation, params) do
    sta_config = Settings.get!(:sta)
    url = get_endpoint_url(sta_config)
    body = Soap.build_envelope(operation, params)

    headers = [
      {"Content-Type", "text/xml; charset=utf-8"},
      {"SOAPAction", soap_action(operation)}
    ]

    case do_request(url, body, headers, sta_config) do
      {:ok, response_body} ->
        Soap.parse_response(response_body, operation)

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

  defp do_request(url, body, headers, config) do
    timeout = Map.get(config, :timeout_ms, @default_timeout)

    request = Finch.build(:post, url, headers, body)

    case Finch.request(request, StaConnector.Finch, receive_timeout: timeout) do
      {:ok, %Finch.Response{status: 200, body: body}} ->
        {:ok, body}

      {:ok, %Finch.Response{status: 401}} ->
        {:error, Error.auth("Authentication failed")}

      {:ok, %Finch.Response{status: 429}} ->
        {:error, Error.rate_limit("BCB rate limit exceeded")}

      {:ok, %Finch.Response{status: status, body: body}} ->
        {:error, Error.connection("HTTP #{status}: #{body}")}

      {:error, %Mint.TransportError{reason: :timeout}} ->
        {:error, Error.timeout("Request timed out")}

      {:error, reason} ->
        {:error, Error.connection("Connection failed: #{inspect(reason)}")}
    end
  end

  defp get_endpoint_url(%{environment: :production, production_url: url}), do: url
  defp get_endpoint_url(%{homologation_url: url}), do: url

  defp soap_action(:list_available_files), do: "ListAvailableFiles"
  defp soap_action(:request_protocol), do: "RequestProtocol"
  defp soap_action(:download_file), do: "DownloadFile"
  defp soap_action(:confirm_receipt), do: "ConfirmReceipt"
  defp soap_action(:upload_file), do: "UploadFile"

  defp maybe_reset_window(state) do
    now = System.monotonic_time(:millisecond)
    if now - state.window_start > 60_000 do
      %{state | request_count: 0, window_start: now}
    else
      state
    end
  end

  defp rate_limited?(state) do
    rate_config = Settings.get!(:rate_limit)
    state.request_count >= rate_config.max_queries_per_minute
  end
end
```

**Step 5: Write tests**

```elixir
# backend/test/sta_connector/sta/client_test.exs
defmodule StaConnector.Sta.ClientTest do
  use ExUnit.Case, async: false

  alias StaConnector.Sta.{Client, Soap, Types}

  describe "Soap.build_envelope/2" do
    test "builds list_available_files envelope" do
      envelope = Soap.build_envelope(:list_available_files, %{system_ids: ["CCS", "SPI"]})
      assert envelope =~ "<sta:ListAvailableFiles>"
      assert envelope =~ "<sta:systemId>CCS</sta:systemId>"
      assert envelope =~ "<sta:systemId>SPI</sta:systemId>"
    end

    test "builds upload_file envelope with base64 content" do
      envelope = Soap.build_envelope(:upload_file, %{
        system_id: "CCS",
        file_name: "test.txt",
        content: "Hello World"
      })
      assert envelope =~ "<sta:UploadFile>"
      assert envelope =~ "<sta:systemId>CCS</sta:systemId>"
      assert envelope =~ Base.encode64("Hello World")
    end
  end

  describe "Types" do
    test "FileInfo struct has required fields" do
      file = %Types.FileInfo{
        protocol_number: "12345",
        system_id: "CCS",
        file_name: "test.txt"
      }
      assert file.protocol_number == "12345"
      assert file.metadata == %{}
    end
  end
end
```

**Step 6: Run tests**

```bash
cd backend && mix test test/sta_connector/sta/client_test.exs
```

**Step 7: Commit**

```bash
git add backend/lib/sta_connector/sta/ backend/test/sta_connector/sta/
git commit -m "feat: add STA WebService client with SOAP handling"
```

---

### Task 1.5: Vue Frontend Scaffold

**Files:**
- Create: `frontend/package.json`
- Create: `frontend/vite.config.ts`
- Create: `frontend/tsconfig.json`
- Create: `frontend/src/main.ts`
- Create: `frontend/src/App.vue`
- Create: `frontend/src/stores/index.ts`
- Create: `frontend/src/router/index.ts`
- Create: `frontend/tailwind.config.js`
- Create: `frontend/postcss.config.js`
- Create: `frontend/index.html`

**Step 1: Initialize Vue project structure**

```bash
mkdir -p frontend/src/{components,views,stores,router,composables,utils,assets}
```

**Step 2: Create package.json**

```json
{
  "name": "sta-connector-admin",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix",
    "type-check": "vue-tsc --noEmit"
  },
  "dependencies": {
    "vue": "^3.4.21",
    "vue-router": "^4.3.0",
    "pinia": "^2.1.7",
    "phoenix": "^1.7.12",
    "@vueuse/core": "^10.9.0",
    "chart.js": "^4.4.2",
    "vue-chartjs": "^5.3.0",
    "date-fns": "^3.6.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.4",
    "vite": "^5.2.8",
    "vue-tsc": "^2.0.7",
    "typescript": "^5.4.3",
    "@types/node": "^20.11.30",
    "tailwindcss": "^3.4.3",
    "postcss": "^8.4.38",
    "autoprefixer": "^10.4.19",
    "@typescript-eslint/eslint-plugin": "^7.4.0",
    "@typescript-eslint/parser": "^7.4.0",
    "eslint": "^8.57.0",
    "eslint-plugin-vue": "^9.24.0"
  }
}
```

**Step 3: Create vite.config.ts**

```typescript
// frontend/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true,
      },
      '/socket': {
        target: 'ws://localhost:4000',
        ws: true,
      },
    },
  },
  build: {
    outDir: '../backend/priv/static',
    emptyOutDir: true,
  },
})
```

**Step 4: Create tsconfig.json**

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
```

**Step 5: Create Tailwind config**

```javascript
// frontend/tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#f0fdf4',
          100: '#dcfce7',
          200: '#bbf7d0',
          300: '#86efac',
          400: '#4ade80',
          500: '#22c55e',
          600: '#16a34a',
          700: '#15803d',
          800: '#166534',
          900: '#14532d',
        },
      },
    },
  },
  plugins: [],
}
```

**Step 6: Create main entry files**

```typescript
// frontend/src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')
```

```vue
<!-- frontend/src/App.vue -->
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { useDark, useToggle } from '@vueuse/core'

const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>

<template>
  <div class="min-h-screen bg-gray-100 dark:bg-gray-900 transition-colors">
    <nav class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
          <div class="flex items-center">
            <h1 class="text-xl font-bold text-gray-900 dark:text-white">
              STA Connector
            </h1>
          </div>
          <div class="flex items-center space-x-4">
            <router-link
              to="/"
              class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
            >
              Dashboard
            </router-link>
            <router-link
              to="/files"
              class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
            >
              Files
            </router-link>
            <router-link
              to="/config"
              class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
            >
              Config
            </router-link>
            <button
              @click="toggleDark()"
              class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700"
            >
              {{ isDark ? '☀️' : '🌙' }}
            </button>
          </div>
        </div>
      </div>
    </nav>
    <main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
      <RouterView />
    </main>
  </div>
</template>
```

```typescript
// frontend/src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'dashboard',
      component: () => import('@/views/DashboardView.vue'),
    },
    {
      path: '/files',
      name: 'files',
      component: () => import('@/views/FilesView.vue'),
    },
    {
      path: '/config',
      name: 'config',
      component: () => import('@/views/ConfigView.vue'),
    },
    {
      path: '/logs',
      name: 'logs',
      component: () => import('@/views/LogsView.vue'),
    },
  ],
})

export default router
```

```css
/* frontend/src/assets/main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
```

```html
<!-- frontend/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>STA Connector Admin</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```

**Step 7: Create placeholder views**

```vue
<!-- frontend/src/views/DashboardView.vue -->
<script setup lang="ts">
// Dashboard implementation will be added in Wave 2
</script>

<template>
  <div>
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Dashboard</h2>
    <p class="text-gray-600 dark:text-gray-400">Dashboard content coming soon...</p>
  </div>
</template>
```

```vue
<!-- frontend/src/views/FilesView.vue -->
<script setup lang="ts">
// Files implementation will be added in Wave 2
</script>

<template>
  <div>
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Files</h2>
    <p class="text-gray-600 dark:text-gray-400">File manager coming soon...</p>
  </div>
</template>
```

```vue
<!-- frontend/src/views/ConfigView.vue -->
<script setup lang="ts">
// Config implementation will be added in Wave 2
</script>

<template>
  <div>
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Configuration</h2>
    <p class="text-gray-600 dark:text-gray-400">Configuration editor coming soon...</p>
  </div>
</template>
```

```vue
<!-- frontend/src/views/LogsView.vue -->
<script setup lang="ts">
// Logs implementation will be added in Wave 2
</script>

<template>
  <div>
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Live Logs</h2>
    <p class="text-gray-600 dark:text-gray-400">Log streaming coming soon...</p>
  </div>
</template>
```

**Step 8: Install dependencies and verify**

```bash
cd frontend && npm install && npm run type-check
```

**Step 9: Commit**

```bash
git add frontend/
git commit -m "feat: initialize Vue 3 frontend with Vite and TailwindCSS"
```

---

### Task 1.6: VitePress Documentation Scaffold

**Files:**
- Create: `docs/package.json`
- Create: `docs/.vitepress/config.ts`
- Create: `docs/index.md`
- Create: `docs/guide/getting-started.md`
- Create: `docs/api/index.md`
- Create: `docs/architecture/index.md`

**Step 1: Initialize VitePress**

```bash
mkdir -p docs/.vitepress docs/guide docs/api docs/architecture docs/deployment
```

**Step 2: Create package.json**

```json
{
  "name": "sta-connector-docs",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview"
  },
  "devDependencies": {
    "vitepress": "^1.1.0"
  }
}
```

**Step 3: Create VitePress config**

```typescript
// docs/.vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: 'STA Connector',
  description: 'BCB STA WebService Integration for FluxiQ STA',
  themeConfig: {
    nav: [
      { text: 'Guide', link: '/guide/getting-started' },
      { text: 'API', link: '/api/' },
      { text: 'Architecture', link: '/architecture/' },
    ],
    sidebar: {
      '/guide/': [
        {
          text: 'Introduction',
          items: [
            { text: 'Getting Started', link: '/guide/getting-started' },
            { text: 'Installation', link: '/guide/installation' },
            { text: 'Configuration', link: '/guide/configuration' },
          ],
        },
        {
          text: 'Features',
          items: [
            { text: 'Inbound Pipeline', link: '/guide/inbound' },
            { text: 'Outbound Pipeline', link: '/guide/outbound' },
            { text: 'Real-time Updates', link: '/guide/realtime' },
          ],
        },
      ],
      '/api/': [
        {
          text: 'REST API',
          items: [
            { text: 'Overview', link: '/api/' },
            { text: 'Admin API', link: '/api/admin' },
            { text: 'Outbound API', link: '/api/outbound' },
          ],
        },
        {
          text: 'WebSocket',
          items: [
            { text: 'Phoenix Channels', link: '/api/channels' },
          ],
        },
      ],
      '/architecture/': [
        {
          text: 'Architecture',
          items: [
            { text: 'Overview', link: '/architecture/' },
            { text: 'OTP Design', link: '/architecture/otp' },
            { text: 'Database Schema', link: '/architecture/database' },
          ],
        },
      ],
    },
    socialLinks: [
      { icon: 'github', link: 'https://github.com/your-org/sta-connector' },
    ],
  },
})
```

**Step 4: Create index page**

```markdown
<!-- docs/index.md -->
---
layout: home
hero:
  name: STA Connector
  text: BCB Integration Made Simple
  tagline: Elixir/Phoenix service for bidirectional file exchange with Brazil Central Bank's STA
  actions:
    - theme: brand
      text: Get Started
      link: /guide/getting-started
    - theme: alt
      text: View API
      link: /api/
features:
  - icon: ⚡
    title: Real-time Updates
    details: Phoenix Channels provide live file status, metrics, and log streaming
  - icon: 🛡️
    title: Fault Tolerant
    details: OTP supervision trees ensure automatic recovery from failures
  - icon: 📊
    title: Full Observability
    details: Built-in metrics, health checks, and admin dashboard
  - icon: 🔄
    title: Bidirectional
    details: Complete inbound polling and outbound upload support
---
```

**Step 5: Create getting started guide**

```markdown
<!-- docs/guide/getting-started.md -->
# Getting Started

## Prerequisites

- Elixir 1.16+
- PostgreSQL 15+
- Node.js 20+ (for frontend)

## Quick Start

### 1. Clone and Setup

\`\`\`bash
git clone https://github.com/your-org/sta-connector
cd sta-connector

# Backend
cd backend
mix deps.get
mix ecto.setup

# Frontend
cd ../frontend
npm install
\`\`\`

### 2. Configure

Copy the example configuration:

\`\`\`bash
cp config/sta_config.example.json config/sta_config.json
\`\`\`

Edit `config/sta_config.json` with your BCB credentials and settings.

### 3. Run

\`\`\`bash
# Terminal 1 - Backend
cd backend && mix phx.server

# Terminal 2 - Frontend
cd frontend && npm run dev
\`\`\`

Visit [http://localhost:3000](http://localhost:3000) for the admin portal.

## Next Steps

- [Configuration Guide](/guide/configuration)
- [Understanding the Architecture](/architecture/)
- [API Reference](/api/)
```

**Step 6: Install and verify**

```bash
cd docs && npm install && npm run build
```

**Step 7: Commit**

```bash
git add docs/
git commit -m "docs: initialize VitePress documentation site"
```

---

### Task 1.7: Docker Setup

**Files:**
- Create: `backend/Dockerfile`
- Create: `frontend/Dockerfile`
- Create: `docker-compose.yml`
- Create: `docker-compose.dev.yml`
- Create: `.dockerignore`

**Step 1: Create backend Dockerfile**

```dockerfile
# backend/Dockerfile
FROM elixir:1.16-alpine AS builder

RUN apk add --no-cache build-base git

WORKDIR /app

ENV MIX_ENV=prod

COPY mix.exs mix.lock ./
RUN mix local.hex --force && \
    mix local.rebar --force && \
    mix deps.get --only prod && \
    mix deps.compile

COPY config config
COPY lib lib
COPY priv priv

RUN mix compile && \
    mix release

# Runtime
FROM alpine:3.19 AS runner

RUN apk add --no-cache libstdc++ openssl ncurses-libs

WORKDIR /app

RUN addgroup -S app && adduser -S app -G app
USER app

COPY --from=builder --chown=app:app /app/_build/prod/rel/sta_connector ./

ENV PHX_HOST=localhost
ENV PORT=4000
ENV DATABASE_URL=ecto://postgres:postgres@db/sta_connector

EXPOSE 4000

CMD ["bin/sta_connector", "start"]
```

**Step 2: Create frontend Dockerfile**

```dockerfile
# frontend/Dockerfile
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Nginx for serving
FROM nginx:alpine

COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
```

**Step 3: Create frontend nginx.conf**

```nginx
# frontend/nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api {
        proxy_pass http://backend:4000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location /socket {
        proxy_pass http://backend:4000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }
}
```

**Step 4: Create docker-compose.yml**

```yaml
# docker-compose.yml
version: '3.8'

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: sta_connector
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      DATABASE_URL: ecto://postgres:postgres@db/sta_connector
      SECRET_KEY_BASE: ${SECRET_KEY_BASE:-$(mix phx.gen.secret)}
      PHX_HOST: ${PHX_HOST:-localhost}
      PORT: 4000
    ports:
      - "4000:4000"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./config:/app/config:ro

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "80:80"
    depends_on:
      - backend

volumes:
  postgres_data:
```

**Step 5: Create docker-compose.dev.yml**

```yaml
# docker-compose.dev.yml
version: '3.8'

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: sta_connector_dev
    ports:
      - "5432:5432"
    volumes:
      - postgres_dev_data:/var/lib/postgresql/data

volumes:
  postgres_dev_data:
```

**Step 6: Create .dockerignore**

```
# .dockerignore
.git
.gitignore
*.md
.env*
docker-compose*.yml

# Elixir
backend/_build
backend/deps
backend/.elixir_ls
backend/cover
backend/*.ez

# Node
frontend/node_modules
frontend/dist
frontend/.vite

# Docs
docs/node_modules
docs/.vitepress/cache
docs/.vitepress/dist
```

**Step 7: Verify docker-compose**

```bash
docker-compose config
```

**Step 8: Commit**

```bash
git add backend/Dockerfile frontend/Dockerfile frontend/nginx.conf docker-compose*.yml .dockerignore
git commit -m "feat: add Docker configuration for production deployment"
```

---

### Task 1.8: CI/CD Pipeline

**Files:**
- Create: `.github/workflows/ci.yml`
- Create: `.github/workflows/deploy.yml`

**Step 1: Create CI workflow**

```yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  backend:
    name: Backend Tests
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: sta_connector_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Set up Elixir
        uses: erlef/setup-beam@v1
        with:
          elixir-version: '1.16'
          otp-version: '26'

      - name: Restore dependencies cache
        uses: actions/cache@v4
        with:
          path: backend/deps
          key: ${{ runner.os }}-mix-${{ hashFiles('backend/mix.lock') }}
          restore-keys: ${{ runner.os }}-mix-

      - name: Install dependencies
        working-directory: backend
        run: mix deps.get

      - name: Check formatting
        working-directory: backend
        run: mix format --check-formatted

      - name: Run Credo
        working-directory: backend
        run: mix credo --strict

      - name: Run tests
        working-directory: backend
        env:
          MIX_ENV: test
          DATABASE_URL: ecto://postgres:postgres@localhost/sta_connector_test
        run: mix test

  frontend:
    name: Frontend Tests
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install dependencies
        working-directory: frontend
        run: npm ci

      - name: Type check
        working-directory: frontend
        run: npm run type-check

      - name: Lint
        working-directory: frontend
        run: npm run lint

      - name: Build
        working-directory: frontend
        run: npm run build

  docs:
    name: Build Docs
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: docs/package-lock.json

      - name: Install dependencies
        working-directory: docs
        run: npm ci

      - name: Build docs
        working-directory: docs
        run: npm run build
```

**Step 2: Create deploy workflow**

```yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    tags:
      - 'v*'

jobs:
  build-and-push:
    name: Build and Push Docker Images
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract version
        id: version
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Build and push backend
        uses: docker/build-push-action@v5
        with:
          context: ./backend
          push: true
          tags: |
            ghcr.io/${{ github.repository }}/backend:${{ steps.version.outputs.VERSION }}
            ghcr.io/${{ github.repository }}/backend:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Build and push frontend
        uses: docker/build-push-action@v5
        with:
          context: ./frontend
          push: true
          tags: |
            ghcr.io/${{ github.repository }}/frontend:${{ steps.version.outputs.VERSION }}
            ghcr.io/${{ github.repository }}/frontend:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
```

**Step 3: Commit**

```bash
git add .github/
git commit -m "ci: add GitHub Actions workflows for CI and deployment"
```

---

## Wave 2: Core Features

(Wave 2 tasks continue with the same level of detail for:)

### Task 2.1: STA Operations (all WebService operations)
### Task 2.2: Inbound Pipeline (GenServer polling workers)
### Task 2.3: Outbound API (file upload REST endpoints)
### Task 2.4: Admin API (config, metrics, health)
### Task 2.5: Parser Registry (BCB system parsers)
### Task 2.6: Phoenix Channels (real-time infrastructure)
### Task 2.7: Vue Dashboard (health, metrics components)
### Task 2.8: Vue File Manager (file listing, status, retry)

---

## Wave 3: Integration & Polish

### Task 3.1: Channel Integration (Vue + Phoenix Channels)
### Task 3.2: Live Metrics (real-time streaming)
### Task 3.3: Live Logs (log streaming to frontend)
### Task 3.4: Notifications (push notification system)
### Task 3.5: E2E Tests (integration test suite)
### Task 3.6: Documentation (complete all docs)

---

## File Tracking

### CLAUDE.md Updates

After each wave completion, update CLAUDE.md with:

```markdown
## Session Log

### [Date]
- Wave N completed
- [List of completed tasks]
- [Any blockers or notes]
```

### README.md Template

```markdown
# STA Connector

> Elixir/Phoenix service for bidirectional file exchange with Brazil Central Bank's STA WebService.

[![CI](https://github.com/your-org/sta-connector/actions/workflows/ci.yml/badge.svg)](https://github.com/your-org/sta-connector/actions)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## Features

- 🔄 **Bidirectional Integration** - Inbound polling and outbound uploads
- ⚡ **Real-time Updates** - Live file status via Phoenix Channels
- 🛡️ **Fault Tolerant** - OTP supervision trees with automatic recovery
- 📊 **Full Observability** - Metrics, health checks, admin dashboard
- 🎯 **BCB Compliant** - Rate limiting and protocol handling

## Quick Start

\`\`\`bash
# Clone
git clone https://github.com/your-org/sta-connector
cd sta-connector

# Start with Docker
docker-compose up -d

# Or run locally
cd backend && mix phx.server
cd frontend && npm run dev
\`\`\`

## Documentation

Full documentation at [https://your-org.github.io/sta-connector](https://your-org.github.io/sta-connector)

## Tech Stack

| Component | Technology |
|-----------|------------|
| Backend | Elixir 1.16, Phoenix 1.7, Ecto |
| Frontend | Vue 3, Vite, Pinia, TailwindCSS |
| Database | PostgreSQL 15 |
| Docs | VitePress |
| Deploy | Docker, GitHub Actions |

## License

MIT
```

---

## Execution Strategy

This plan is optimized for parallel execution with 8 agents per wave:

1. **Wave 1** - All 8 tasks are independent, run in parallel
2. **Wave 2** - Depends on Wave 1 completion, then 8 tasks in parallel
3. **Wave 3** - Depends on Wave 2 completion, then 6 tasks in parallel

**Estimated total: 22 agent-tasks across 3 waves**
