# Phase 14 — Flow Audit Log

**Goal:** Per-flow execution audit page at `/admin/flows/:id/audit` — stats strip, searchable event table, right-side slide panel rendering the full `composite_trace` node timeline.

---

## Context

`audit_events` already stores `composite_trace` (full per-node execution data) for every DAG flow run, but has no `flow_id` column. There is no way to query "all executions of Flow 3" today. This phase wires `flow_id` end-to-end and builds the audit UI on top of that data.

---

## Phases

### Phase 0 — Wire `flow_id` through the pipeline (foundation)
*All later phases depend on this.*

| # | What | File(s) |
|---|---|---|
| 0a | Migration: `add :flow_id, :integer` + index `(flow_id, inserted_at)` on `audit_events` | new migration |
| 0b | Add `flow_id` field to `AuditEvent` schema + `@optional` | `apps/mw_audit/lib/mw_audit/event.ex` |
| 0c | `upsert_dag_rule` includes `flow_id` in ETS entry | `apps/mw_router/lib/mw_router/route_table.ex` |
| 0d | `publish_flow` passes `flow_id: socket.assigns.flow_id` | `apps/gateway_web/.../flow_builder_live.ex` |
| 0e | `write_audit` extracts `flow_id` from `ctx.route_spec` and writes it | `apps/mw_router/lib/mw_router/pipeline.ex` |

**How `flow_id` flows:**
```
FlowBuilderLive.publish_flow
  └─ upsert_dag_rule(%{..., flow_id: 3})
       └─ ETS entry: %{type: :dag, dag_route: ..., flow_id: 3}
            └─ Pipeline.route_lookup → ctx.route_spec = %{..., flow_id: 3}
                 └─ write_audit → audit_events.flow_id = 3
```

---

### Phase 1 — Query layer
*New context module. No UI yet.*

**File:** `apps/infra_repo/lib/infra_repo/flow_audit.ex`

```elixir
FlowAudit.flow_summary(flow_id)
  # → %{total: 142, success: 138, error: 3, pending: 1, avg_ms: 312}

FlowAudit.list_events(flow_id, %{status: nil, search: "", page: 1})
  # → {events, total_count}  — paginated, 25/page

FlowAudit.search_by_idempotency(flow_id, key)
  # → [AuditEvent, ...]
```

Queries `audit_events` WHERE `flow_id = ?` (AND optional filters).
`pending` = rows where `status = "success"` AND `idempotency_replayed = true` (replayed = in-flight proxy).

---

### Phase 2 — `FlowAuditLive`
*The main deliverable.*

**New files:**
- `apps/gateway_web/lib/gateway_web_web/live/flow_audit_live.ex`
- `apps/gateway_web/lib/gateway_web_web/live/flow_audit_live.html.heex`

**Route added to router:**
```elixir
live "/flows/:flow_id/audit", FlowAuditLive, :index
```

#### Layout

```
┌─ Tutu Flow — Audit ─────────────────────── [← Back to Flow] ──┐
│                                                                 │
│  [Total: 142]  [Success: 138]  [Failed: 3]  [Replayed: 1]     │
│  [Avg: 312ms]                                                   │
│                                                                 │
│  [🔍 idempotency key…]  [All] [Success] [Failed]               │
│                                                                 │
│  TRACE ID        STATUS   DURATION  TENANT   KEY        WHEN   │
│  abc-123…        ● ok     312ms     acme     key-013  2m ago → │
│  def-456…        ✗ error   45ms     acme     key-014  5m ago → │
│                                                                 │
│                    ← click any row →                            │
│                                                                 │
│                 ┌─ Slide Panel ──────────────────────────────┐ │
│                 │ Trace: abc-123  ·  ✓ ok  ·  312ms          │ │
│                 │ Tenant: acme  ·  Key: key-013               │ │
│                 │                                             │ │
│                 │ NODE TIMELINE                               │ │
│                 │  ✓  request           2ms                   │ │
│                 │  ○  profile (slot)    —                     │ │
│                 │  ✓  CrmAdapter        287ms                 │ │
│                 │  ○  balance (slot)    —                     │ │
│                 │  ✗  BankingAdapter    timeout  [▼ expand]   │ │
│                 │       error: upstream timeout 5000ms         │ │
│                 │  ✓  merge             1ms                   │ │
│                 │  ✓  response          2ms                   │ │
│                 │                                             │ │
│                 │  [Request ▼]  [Response ▼]                  │ │
│                 └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```

#### Assigns
```elixir
flow:           %Flow{}
summary:        %{total, success, error, replayed, avg_ms}
events:         [%AuditEvent{}, ...]
total_count:    integer
page:           integer
filter_status:  "all" | "success" | "error"
search:         String.t()
selected_event: %AuditEvent{} | nil
panel_open:     boolean
expanded_nodes: MapSet.t()     # which trace nodes are expanded
```

#### Key interactions
| User action | LiveView event | Result |
|---|---|---|
| Type in search | `search` (debounce 300ms) | Re-queries, resets to page 1 |
| Click status pill | `filter_status` | Re-queries, resets to page 1 |
| Click table row | `select_event` | Decodes `composite_trace`, opens panel |
| Click node row in panel | `toggle_node` | Expands/collapses error + payload detail |
| Click `×` | `close_panel` | Clears `selected_event` |
| Click page arrow | `paginate` | Changes page |

#### `composite_trace` rendering
The stored JSON is decoded on `select_event`:
```elixir
trace = Jason.decode!(event.composite_trace)
nodes = trace["nodes"]   # [%{"node_id", "type", "status", "duration_ms", "reason"}, ...]
```
Each node row shows: status icon · node_id · type chip · duration · expand toggle (if error/reason present).

---

### Phase 3 — Navigation & polish

| # | What | File |
|---|---|---|
| 3a | Add "Audit" link per row in flows list | `flows_live.html.heex` |
| 3b | Add "View Audit" button in flow builder toolbar | `flow_builder_live.html.heex` |
| 3c | Rebuild Tailwind CSS | `mix tailwind gateway_web` |

---

## File change summary

| Action | File |
|---|---|
| NEW | `apps/infra_repo/priv/repo/migrations/20260502000001_add_flow_id_to_audit_events.exs` |
| NEW | `apps/infra_repo/lib/infra_repo/flow_audit.ex` |
| NEW | `apps/gateway_web/lib/gateway_web_web/live/flow_audit_live.ex` |
| NEW | `apps/gateway_web/lib/gateway_web_web/live/flow_audit_live.html.heex` |
| MOD | `apps/mw_audit/lib/mw_audit/event.ex` |
| MOD | `apps/mw_router/lib/mw_router/route_table.ex` |
| MOD | `apps/mw_router/lib/mw_router/pipeline.ex` |
| MOD | `apps/gateway_web/lib/gateway_web_web/router.ex` |
| MOD | `apps/gateway_web/lib/gateway_web_web/live/flow_builder_live.ex` |
| MOD | `apps/gateway_web/lib/gateway_web_web/live/flows_live.html.heex` |
| MOD | `apps/gateway_web/lib/gateway_web_web/live/flow_builder_live.html.heex` |

---

## Out of scope (Plan A — future)

Visual canvas overlay with per-node badges is tracked separately as Phase 14b. It builds on top of the `FlowAudit.node_stats/1` query added here.
