Used tool: Agent

Searched for files matching `apps/gateway_web/lib/gateway_web_web/live/*.ex`

Searched for files matching `apps/infra_repo/lib/infra_repo/schemas/*.ex`

Searched for files matching `apps/infra_repo/priv/repo/migrations/*.exs`

Searched for files matching `apps/mw_kernel/lib/mw_kernel/*.ex`

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/live/dashboard_live.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/live/tenants_live.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/live/api_keys_live.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/live/monitoring_live.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/router.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/tenant.ex)

Searched for files matching `*api*`

Ran terminal command: find /home/prem/mercurypay/tms_new_version/mw-core/apps/mw_kernel/lib/mw_kernel -type f -name "*.ex" | head -20

Ran terminal command: find /home/prem/mercurypay/tms_new_version/mw-core -type f -name "*telemetry*" -o -name "*monitoring*" -o -name "*counter*" | grep -E "\.ex(s)?$"

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000002_create_api_keys.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000011_create_tenants.exs)

Ran terminal command:  find /home/prem/mercurypay/tms_new_version/mw-core -type f -name "*telemetry*" -o -name "*monitoring*" -o -name "*counter*" | grep -E "\.ex(s)?$"

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_telemetry/lib/infra_telemetry.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/telemetry_handler.ex)

Ran terminal command: ls -la /home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/ | grep -E "\.exs$"

Ran terminal command:  ls -la /home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/ | grep -E "\.exs$"

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000003_create_token_revocations.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000004_create_audit_events.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000005_create_dead_letter_queue.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000006_create_async_jobs.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000007_create_transactions.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000008_create_device_registrations.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000009_create_transform_rules.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000010_create_schema_registry.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000013_create_idempotency_records.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260427000001_create_admin_users.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260430000001_create_flows.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260501000001_create_adapter_configs.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260501000003_create_route_rules.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260501000006_create_webhook_tables.exs)

Ran terminal command: find /home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas -type f -name "*.ex" | head -20

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/flow.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/adapter_config.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/route_rule.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000001_create_schema_info.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000012_add_composite_idx_audit_events.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260430000002_add_composite_trace_to_audit_events.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260501000002_fix_auto_increment_all_tables.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/webhook_source.ex)

Ran terminal command: grep -r "schema.*api_key" /home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/ 2>/dev/null || echo "No API Key schema found"

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/admin_user.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/webhook_endpoint.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/webhook_delivery.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/idempotency_record.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/transaction.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/schema_registry.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/transform_rule.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260501000004_add_processing_mode_to_route_rules.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260501000005_add_version_to_route_rules.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/device_registration.ex)

Ran terminal command: find /home/prem/mercurypay/tms_new_version/mw-core/apps -type d -name "*telemetry*" 2>/dev/null

Ran terminal command: ls -la /home/prem/mercurypay/tms_new_version/mw-core/apps/infra_telemetry/lib/ 2>/dev/null

Ran terminal command: find /home/prem/mercurypay/tms_new_version/mw-core/apps/infra_telemetry -name "*.ex" | head -20

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_telemetry/lib/infra_telemetry/metrics.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_telemetry/lib/infra_telemetry/application.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_telemetry/lib/infra_telemetry/fanout_handler.ex)

Now let me read the key existing files before planning:

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/live/tenants_live.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/lib/infra_repo/schemas/tenant.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/infra_repo/priv/repo/migrations/20260426000002_create_api_keys.exs)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/live/dashboard_live.ex)

Read [](file:///home/prem/mercurypay/tms_new_version/mw-core/apps/gateway_web/lib/gateway_web_web/live/monitoring_live.ex)

Now I have everything I need. Let me enter plan mode:

Used tool: EnterPlanMode

Used tool: AskUserQuestion

Analyzing your answers...

Now let me write the implementation plan:

Read [](file:///home/prem/.claude/plans/cosmic-discovering-papert.md)

Here is Claude's plan:

# Plan: Phase 13 — Tenants CRUD, API Keys, Monitoring Enhancements, Dashboard Fixes

## Context
Four admin portal features are incomplete (confirmed by screenshots):
1. **Tenants** — read-only table; needs full create/edit/archive CRUD. Schema missing `plan` field shown in UI.
2. **API Keys** — stubbed "Coming Soon". `api_keys` table exists in DB but no Ecto schema, no management UI.
3. **Monitoring** — purely in-memory counters reset on restart. Needs DB snapshot on load from `audit_events`.
4. **Dashboard** — stat cards live-only (reset to 0). Needs DB-backed baseline + 4 new stat cards.

User requested all 3 API key scoping modes: superadmin manages any tenant's keys, tenant-admin manages own, per-user context.

---

## Files to Modify / Create

| File | Action |
|---|---|
| `apps/infra_repo/lib/infra_repo/schemas/tenant.ex` | Add `plan` field |
| `apps/infra_repo/lib/infra_repo/schemas/api_key.ex` | **CREATE** new Ecto schema |
| `apps/infra_repo/priv/repo/migrations/20260501000007_add_plan_to_tenants.exs` | **CREATE** add `plan` column |
| `apps/gateway_web/lib/gateway_web_web/live/tenants_live.ex` | Full CRUD (create/edit/archive) |
| `apps/gateway_web/lib/gateway_web_web/live/api_keys_live.ex` | Full implementation replacing "Coming Soon" |
| `apps/gateway_web/lib/gateway_web_web/live/monitoring_live.ex` | Add DB snapshot on mount |
| `apps/gateway_web/lib/gateway_web_web/live/dashboard_live.ex` | DB-backed initial counts + 4 new stat cards |

---

## Change 1 — Migration: add `plan` to tenants

**File**: `apps/infra_repo/priv/repo/migrations/20260501000007_add_plan_to_tenants.exs`

Use `information_schema.columns` guard (MySQL doesn't support `IF NOT EXISTS` on `ALTER TABLE`):
```elixir
def up do
  unless plan_exists?() do
    alter table(:tenants) do
      add :plan, :string, null: true, default: "standard"
    end
  end
end
defp plan_exists? do
  {:ok, %{rows: [[n]]}} =
    Repo.query("SELECT COUNT(*) FROM information_schema.columns
                WHERE table_schema = DATABASE() AND table_name = 'tenants' AND column_name = 'plan'")
  n > 0
end
```

---

## Change 2 — Tenant schema: add `plan` field

**File**: `apps/infra_repo/lib/infra_repo/schemas/tenant.ex`

Add `field :plan, :string, default: "standard"` and `:plan` to `@optional`.
Add `validate_inclusion(:plan, ~w(standard professional enterprise))`.

---

## Change 3 — Tenants LiveView: full CRUD

**File**: `apps/gateway_web/lib/gateway_web_web/live/tenants_live.ex`

New state assigns:
```elixir
modal: nil,          # nil | :new | :edit
draft: %{},
draft_errors: %{},
confirm_archive: nil,
flash_msg: nil,
search: ""
```

New events: `"new"`, `"edit"`, `"close_modal"`, `"update_draft"`, `"save"`, `"confirm_archive"`, `"cancel_archive"`, `"archive"`, `"search"`.

**Save logic**:
- Create: `id` is user-supplied slug (required, immutable). Validate `~r/^[a-z0-9][a-z0-9_-]{2,63}$/`.
- Edit: look up by `draft["id"]`, update name/display_name/plan/status.
- Archive: set `status: "archived"` (add to `validate_inclusion` list).

**UI additions**:
- "+ New Tenant" button in header
- Search input filters by name/id
- Actions column: Edit | Archive buttons per row
- Inline confirm: "Archive?" Yes / No inline (same pattern as webhook endpoints)
- Modal: id (disabled on edit), Name, Display Name, Plan dropdown, Status dropdown

---

## Change 4 — ApiKey schema (new file)

**File**: `apps/infra_repo/lib/infra_repo/schemas/api_key.ex`

```elixir
defmodule InfraRepo.Schemas.ApiKey do
  use Ecto.Schema
  import Ecto.Changeset

  schema "api_keys" do
    field :name,       :string
    field :key_prefix, :string       # e.g. "mpk_a1b2c3d4"
    field :key_hash,   :string       # SHA-256 hex of full key
    field :tenant_id,  :string
    field :roles,      :string       # comma-separated: "read,write,admin"
    field :active,     :boolean, default: true
    field :expires_at, :utc_datetime
    timestamps(type: :utc_datetime)
  end

  def changeset(key, attrs) do
    key
    |> cast(attrs, [:name, :key_prefix, :key_hash, :tenant_id, :roles, :active, :expires_at])
    |> validate_required([:name, :key_prefix, :key_hash, :tenant_id])
    |> unique_constraint(:key_prefix)
  end

  # Returns {full_plaintext_key, insert_attrs}
  def generate(name, tenant_id, roles \\ "read") do
    raw    = :crypto.strong_rand_bytes(24) |> Base.encode64(padding: false)
             |> String.replace(~r/[^A-Za-z0-9]/, "x")
    prefix = "mpk_#{String.slice(raw, 0, 8)}"
    full   = "#{prefix}_#{String.slice(raw, 8, 24)}"
    hash   = :crypto.hash(:sha256, full) |> Base.encode16(case: :lower)
    {full, %{name: name, key_prefix: prefix, key_hash: hash, tenant_id: tenant_id, roles: roles}}
  end
end
```

---

## Change 5 — API Keys LiveView (full replacement)

**File**: `apps/gateway_web/lib/gateway_web_web/live/api_keys_live.ex`

State:
```elixir
api_keys: [],
tenants: [],
modal: nil,           # nil | :new | :show_key
new_key_plaintext: nil,
draft: %{},
draft_errors: %{},
confirm_revoke: nil,
flash_msg: nil,
filter_tenant: ""
```

Events:
- `"new"` → modal: :new, load tenants list
- `"update_draft"` / `"update_draft_roles"` → field updates
- `"save"` → call `ApiKey.generate/3`, `Repo.insert`, transition to `:show_key` modal with plaintext
- `"dismiss_key"` → clear plaintext, close modal, reload list
- `"toggle_active"` → flip `active` boolean
- `"confirm_revoke"` / `"cancel_revoke"` / `"revoke"` → set `active: false`
- `"filter_tenant"` → filter list

Key reveal modal (shown once):
- Warning banner: "Copy this key now — it will not be shown again."
- Key displayed in monospace code block with JS copy-to-clipboard button
- Dismiss button only available (no accidental close)

Table columns: Name | Key Prefix | Tenant | Roles | Expires | Status | Actions

---

## Change 6 — Monitoring LiveView: DB snapshot on mount

**File**: `apps/gateway_web/lib/gateway_web_web/live/monitoring_live.ex`

Add on mount:
```elixir
db_snapshot = load_db_snapshot()
assign(socket, db_snapshot: db_snapshot)
```

`load_db_snapshot/0` queries:
- `Repo.aggregate(AuditEvent, :count)` → total_requests
- `Repo.aggregate(from(a in AuditEvent, where: a.status == "error"), :count)` → total_errors
- `Repo.aggregate(DeadLetter, :count)` → dlq_total
- Min `inserted_at` from AuditEvent → data_since

Render a new card below the existing counters:
```
┌─────────────────────────────────────────────────────┐
│  Database Snapshot (all-time)                       │
│  Total Requests: 12,453  Errors: 89  DLQ: 14       │
│  Data since: 2026-04-26                             │
└─────────────────────────────────────────────────────┘
```

Also add 5 more counter rows from DB:
- Active tenants, Active API keys, Active routes, Published flows, Webhook deliveries (24h)

---

## Change 7 — Dashboard LiveView: DB stats + 2nd row of cards

**File**: `apps/gateway_web/lib/gateway_web_web/live/dashboard_live.ex`

Add `db_stats` to initial state loaded from DB on mount:
```elixir
db_stats: load_db_stats()
```

`load_db_stats/0`:
- `tenant_count`: `Repo.aggregate(Tenant, :count)`
- `api_key_count`: `Repo.aggregate(from(k in ApiKey, where: k.active), :count)`
- `webhook_24h`: count `WebhookDelivery` where `inserted_at >= now - 86400s`
- `flow_count`: count `Flow` where `status == "published"`

Stat cards layout — 2 rows of 4:

**Row 1** (live session, PubSub counters — same as today):
Pipeline Requests | Errors | Batch Rows | DLQ Items

**Row 2** (DB-backed, loaded on mount):
Tenants | Active API Keys | Webhooks (24h) | Active Flows

Each row 2 card links to its admin page (click → navigate).

**Quick links grid** — update to 9 links (add Flows, Webhooks, API Keys, Monitoring):
```
Route Config | Adapter Health | Adapter Configs
Audit Stream | Idempotency    | Dead Letters
Tenants      | Flows          | Webhooks
```

---

## Verification

1. `mix compile` — no errors
2. `mix ecto.migrate` — `20260501000007` runs cleanly
3. `/admin/tenants` — "+ New Tenant" → create `acme-corp` → save → appears in list → Edit → change plan → save → Archive → status shows archived
4. `/admin/api-keys` — "Generate Key" → fill name + tenant + roles → key revealed once → dismiss → appears in list with prefix only → Revoke → status goes inactive
5. `/admin/monitoring` — Database Snapshot section shows all-time counts; in-memory counters still increment per pipeline event
6. `/admin` (Dashboard) — Row 2 cards show DB counts; quick links include Flows/Webhooks/API Keyski