Skip to content

Call Debugger Dashboard Implementation Plan

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

Goal: Redesign the call debugger dashboard with multi-sidebar navigation, org/location hierarchy (prod), and agent grouping (dev).

Architecture: Multi-sidebar drill-down UI with separate backend paths for prod (Hyperdrive/MySQL) and dev (S3). Sidebars collapse when viewing a call, breadcrumb navigation to return.

Tech Stack: React 19, Hono, Hyperdrive, fuse.js (fuzzy search), Tailwind CSS

Linear Ticket: ARE-1557

Design Doc: docs/plans/2026-02-04-call-debugger-dashboard-design.md


Phase 1: Backend - Prod Routes

Task 1: Add Day Index Query

Files: - Create: src/worker/modules/debug/queries/dayIndex.ts

Step 1: Create the day index query file

import type { Connection } from "mysql2/promise"

export type OrgLocationIndex = {
  org_id: string
  org_name: string
  location_id: string
  location_name: string
  call_count: number
}

export async function getDayIndex(
  conn: Connection,
  date: string
): Promise<OrgLocationIndex[]> {
  const [rows] = await conn.query<OrgLocationIndex[]>(
    `SELECT
      o.uid AS org_id,
      o.name AS org_name,
      ol.uid AS location_id,
      ol.name AS location_name,
      COUNT(rc.retell_call_id) AS call_count
    FROM retell_calls rc
    JOIN calls c ON rc.arini_call_id = c.uid
    JOIN org_locations ol ON c.to_id = ol.uid
    JOIN orgs o ON ol.org_id = o.uid
    WHERE DATE(rc.created_at) = ?
    GROUP BY o.uid, o.name, ol.uid, ol.name
    ORDER BY call_count DESC`,
    [date]
  )
  return rows
}

Step 2: Verify file compiles

Run: devenv shell -- pnpm build Expected: Build succeeds with no TypeScript errors

Step 3: Commit

git add src/worker/modules/debug/queries/dayIndex.ts
git commit -m "feat(debug): add day index query for org/location hierarchy"

Task 2: Add Location Calls Query

Files: - Create: src/worker/modules/debug/queries/locationCalls.ts

Step 1: Create the location calls query file

import type { Connection, RowDataPacket } from "mysql2/promise"

export type LocationCall = {
  call_id: string
  arini_call_id: string
  created_at: string
  direction: "inbound" | "outbound" | null
}

type LocationCallRow = RowDataPacket & LocationCall

export async function getLocationCalls(
  conn: Connection,
  locationId: string,
  date: string,
  limit: number,
  offset: number
): Promise<{ calls: LocationCall[]; total: number }> {
  const [[countRow]] = await conn.query<RowDataPacket[]>(
    `SELECT COUNT(*) as total
     FROM retell_calls rc
     JOIN calls c ON rc.arini_call_id = c.uid
     WHERE c.to_id = ? AND DATE(rc.created_at) = ?`,
    [locationId, date]
  )

  const [rows] = await conn.query<LocationCallRow[]>(
    `SELECT
      rc.retell_call_id AS call_id,
      rc.arini_call_id,
      rc.created_at,
      JSON_UNQUOTE(JSON_EXTRACT(rc.retell_call_details, '$.call_type')) AS direction
    FROM retell_calls rc
    JOIN calls c ON rc.arini_call_id = c.uid
    WHERE c.to_id = ? AND DATE(rc.created_at) = ?
    ORDER BY rc.created_at DESC
    LIMIT ? OFFSET ?`,
    [locationId, date, limit, offset]
  )

  return {
    calls: rows,
    total: (countRow as { total: number }).total,
  }
}

Step 2: Verify file compiles

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/worker/modules/debug/queries/locationCalls.ts
git commit -m "feat(debug): add paginated location calls query"

Task 3: Add Day Index API Route

Files: - Modify: src/worker/modules/debug/router.ts

Step 1: Read current router structure

Read src/worker/modules/debug/router.ts to understand existing patterns.

Step 2: Add the /db/index route

Add import at top:

import { getDayIndex } from "./queries/dayIndex"

Add route handler (place near other db routes):

// GET /api/debug/db/index?date=YYYY-MM-DD
app.get("/db/index", async (c) => {
  const date = c.req.query("date")
  if (!date) {
    return c.json({ error: "date query parameter required" }, 400)
  }

  const result = await withConnection(c.env, c.executionCtx, async (conn) => {
    const rows = await getDayIndex(conn, date)

    // Transform flat rows into nested org -> locations structure
    const orgsMap = new Map<string, {
      org_id: string
      org_name: string
      call_count: number
      locations: { location_id: string; location_name: string; call_count: number }[]
    }>()

    for (const row of rows) {
      let org = orgsMap.get(row.org_id)
      if (!org) {
        org = {
          org_id: row.org_id,
          org_name: row.org_name,
          call_count: 0,
          locations: [],
        }
        orgsMap.set(row.org_id, org)
      }
      org.locations.push({
        location_id: row.location_id,
        location_name: row.location_name,
        call_count: row.call_count,
      })
      org.call_count += row.call_count
    }

    // Sort orgs by call_count descending
    const orgs = Array.from(orgsMap.values()).sort((a, b) => b.call_count - a.call_count)

    return { orgs }
  })

  return c.json(result)
})

Step 3: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 4: Test manually

Run: devenv shell -- pnpm dev Test: curl "http://localhost:5173/api/debug/db/index?date=2026-02-04" -u arini:ariniisgod Expected: JSON response with orgs array (may be empty if no calls for that date)

Step 5: Commit

git add src/worker/modules/debug/router.ts
git commit -m "feat(debug): add /db/index route for day hierarchy"

Task 4: Add Location Calls API Route

Files: - Modify: src/worker/modules/debug/router.ts

Step 1: Add import

import { getLocationCalls } from "./queries/locationCalls"

Step 2: Add the /db/locations/:olid/calls route

// GET /api/debug/db/locations/:olid/calls?date=YYYY-MM-DD&offset=0&limit=50
app.get("/db/locations/:olid/calls", async (c) => {
  const olid = c.req.param("olid")
  const date = c.req.query("date")
  const offset = parseInt(c.req.query("offset") || "0", 10)
  const limit = parseInt(c.req.query("limit") || "50", 10)

  if (!date) {
    return c.json({ error: "date query parameter required" }, 400)
  }

  const result = await withConnection(c.env, c.executionCtx, async (conn) => {
    const { calls, total } = await getLocationCalls(conn, olid, date, limit, offset)
    return {
      calls,
      total,
      hasMore: offset + calls.length < total,
    }
  })

  return c.json(result)
})

Step 3: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 4: Commit

git add src/worker/modules/debug/router.ts
git commit -m "feat(debug): add /db/locations/:olid/calls route"

Phase 2: Backend - Dev Routes

Task 5: Add S3 Index Route

Files: - Modify: src/worker/modules/debug/router.ts

Step 1: Read existing S3 call listing code

Read the existing /api/calls route in the router to understand the S3 listing pattern.

Step 2: Add /s3/index route

// GET /api/debug/s3/index?date=YYYY-MM-DD
app.get("/s3/index", async (c) => {
  const date = c.req.query("date")
  if (!date) {
    return c.json({ error: "date query parameter required" }, 400)
  }

  // Reuse existing S3 listing logic
  const bucketName = "arini-call-artifacts-dev"
  const prefix = `${date}/`

  const s3 = new S3Client({
    region: c.env.AWS_REGION,
    credentials: {
      accessKeyId: c.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: c.env.AWS_SECRET_ACCESS_KEY,
    },
  })

  const command = new ListObjectsV2Command({
    Bucket: bucketName,
    Prefix: prefix,
    Delimiter: "/",
  })

  const response = await s3.send(command)
  const callDirs = response.CommonPrefixes || []

  // Extract call IDs and group by agent_id (from call metadata if available)
  // For now, return flat list - agent grouping can be done client-side
  const calls = callDirs.map((dir) => {
    const callId = dir.Prefix?.replace(prefix, "").replace("/", "") || ""
    return {
      call_id: callId,
      date,
    }
  })

  return c.json({ calls })
})

Step 3: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 4: Commit

git add src/worker/modules/debug/router.ts
git commit -m "feat(debug): add /s3/index route for dev environment"

Phase 3: Shared Types

Task 6: Add Response Types

Files: - Modify: src/ui/modules/debug/types/debugger.ts

Step 1: Add new types at end of file

// Day Index Types (Prod)
export type DayIndexOrg = {
  org_id: string
  org_name: string
  call_count: number
  locations: DayIndexLocation[]
}

export type DayIndexLocation = {
  location_id: string
  location_name: string
  call_count: number
}

export type DayIndexResponse = {
  orgs: DayIndexOrg[]
}

// Location Calls Types (Prod)
export type LocationCallsResponse = {
  calls: {
    call_id: string
    arini_call_id: string
    created_at: string
    direction: "inbound" | "outbound" | null
  }[]
  total: number
  hasMore: boolean
}

// Dev Index Types
export type DevDayIndexResponse = {
  calls: {
    call_id: string
    date: string
  }[]
}

// Agent metadata mapping (hardcoded for dev)
export type AgentMetadata = {
  agent_id: string
  agent_name: string
}

export const DEV_AGENT_METADATA: AgentMetadata[] = [
  // TODO: Add agent mappings when available
  // { agent_id: "agent_abc123", agent_name: "Main Scheduling Agent" },
]

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/types/debugger.ts
git commit -m "feat(debug): add types for day index and location calls"

Phase 4: Frontend - Data Hooks

Task 7: Add useDayIndex Hook

Files: - Create: src/ui/modules/debug/hooks/useDayIndex.ts

Step 1: Create the hook

import { useCallback, useEffect, useState } from "react"
import { fetchWithAuth } from "@/lib/api"
import type { DayIndexResponse } from "../types/debugger"

export function useDayIndex(date: string | null) {
  const [data, setData] = useState<DayIndexResponse | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  const fetchIndex = useCallback(async (signal: AbortSignal) => {
    if (!date) return

    setIsLoading(true)
    setError(null)

    try {
      const response = await fetchWithAuth(
        `/api/debug/db/index?date=${date}`,
        { signal }
      )
      if (!response.ok) {
        throw new Error(`Failed to fetch day index: ${response.status}`)
      }
      const json = await response.json()
      if (!signal.aborted) {
        setData(json)
      }
    } catch (e) {
      if (!signal.aborted) {
        setError(e instanceof Error ? e : new Error(String(e)))
      }
    } finally {
      if (!signal.aborted) {
        setIsLoading(false)
      }
    }
  }, [date])

  useEffect(() => {
    const controller = new AbortController()
    fetchIndex(controller.signal)
    return () => controller.abort()
  }, [fetchIndex])

  const refetch = useCallback(() => {
    const controller = new AbortController()
    fetchIndex(controller.signal)
  }, [fetchIndex])

  return { data, isLoading, error, refetch }
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/hooks/useDayIndex.ts
git commit -m "feat(debug): add useDayIndex hook"

Task 8: Add useLocationCalls Hook

Files: - Create: src/ui/modules/debug/hooks/useLocationCalls.ts

Step 1: Create the hook

import { useCallback, useEffect, useState } from "react"
import { fetchWithAuth } from "@/lib/api"
import type { LocationCallsResponse } from "../types/debugger"

export function useLocationCalls(
  locationId: string | null,
  date: string | null
) {
  const [data, setData] = useState<LocationCallsResponse | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)
  const [offset, setOffset] = useState(0)
  const limit = 50

  const fetchCalls = useCallback(
    async (signal: AbortSignal, currentOffset: number, append: boolean) => {
      if (!locationId || !date) return

      setIsLoading(true)
      setError(null)

      try {
        const response = await fetchWithAuth(
          `/api/debug/db/locations/${locationId}/calls?date=${date}&offset=${currentOffset}&limit=${limit}`,
          { signal }
        )
        if (!response.ok) {
          throw new Error(`Failed to fetch calls: ${response.status}`)
        }
        const json: LocationCallsResponse = await response.json()
        if (!signal.aborted) {
          setData((prev) =>
            append && prev
              ? { ...json, calls: [...prev.calls, ...json.calls] }
              : json
          )
        }
      } catch (e) {
        if (!signal.aborted) {
          setError(e instanceof Error ? e : new Error(String(e)))
        }
      } finally {
        if (!signal.aborted) {
          setIsLoading(false)
        }
      }
    },
    [locationId, date]
  )

  // Reset and fetch when location/date changes
  useEffect(() => {
    setOffset(0)
    setData(null)
    const controller = new AbortController()
    fetchCalls(controller.signal, 0, false)
    return () => controller.abort()
  }, [fetchCalls])

  const loadMore = useCallback(() => {
    const newOffset = offset + limit
    setOffset(newOffset)
    const controller = new AbortController()
    fetchCalls(controller.signal, newOffset, true)
  }, [offset, fetchCalls])

  return {
    calls: data?.calls || [],
    total: data?.total || 0,
    hasMore: data?.hasMore || false,
    isLoading,
    error,
    loadMore,
  }
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/hooks/useLocationCalls.ts
git commit -m "feat(debug): add useLocationCalls hook with pagination"

Task 9: Add useDevDayIndex Hook

Files: - Create: src/ui/modules/debug/hooks/useDevDayIndex.ts

Step 1: Create the hook

import { useCallback, useEffect, useState } from "react"
import { fetchWithAuth } from "@/lib/api"
import type { DevDayIndexResponse } from "../types/debugger"

export function useDevDayIndex(date: string | null) {
  const [data, setData] = useState<DevDayIndexResponse | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  const fetchIndex = useCallback(async (signal: AbortSignal) => {
    if (!date) return

    setIsLoading(true)
    setError(null)

    try {
      const response = await fetchWithAuth(
        `/api/debug/s3/index?date=${date}`,
        { signal }
      )
      if (!response.ok) {
        throw new Error(`Failed to fetch dev index: ${response.status}`)
      }
      const json = await response.json()
      if (!signal.aborted) {
        setData(json)
      }
    } catch (e) {
      if (!signal.aborted) {
        setError(e instanceof Error ? e : new Error(String(e)))
      }
    } finally {
      if (!signal.aborted) {
        setIsLoading(false)
      }
    }
  }, [date])

  useEffect(() => {
    const controller = new AbortController()
    fetchIndex(controller.signal)
    return () => controller.abort()
  }, [fetchIndex])

  const refetch = useCallback(() => {
    const controller = new AbortController()
    fetchIndex(controller.signal)
  }, [fetchIndex])

  return { data, isLoading, error, refetch }
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/hooks/useDevDayIndex.ts
git commit -m "feat(debug): add useDevDayIndex hook"

Phase 5: Frontend - UI Components

Task 10: Add fuse.js Dependency

Files: - Modify: package.json

Step 1: Install fuse.js

Run: devenv shell -- pnpm add fuse.js

Step 2: Verify installation

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add package.json pnpm-lock.yaml
git commit -m "chore: add fuse.js for fuzzy search"

Task 11: Create DayNavigator Component

Files: - Create: src/ui/modules/debug/components/DayNavigator.tsx

Step 1: Create the component

import { ChevronLeft, ChevronRight } from "lucide-react"
import { Button } from "@/components/ui/button"

type DayNavigatorProps = {
  date: string // YYYY-MM-DD
  onDateChange: (date: string) => void
}

function formatDateDisplay(dateStr: string): string {
  const date = new Date(dateStr + "T00:00:00")
  return date.toLocaleDateString("en-US", {
    weekday: "long",
    year: "numeric",
    month: "long",
    day: "numeric",
  })
}

function addDays(dateStr: string, days: number): string {
  const date = new Date(dateStr + "T00:00:00")
  date.setDate(date.getDate() + days)
  return date.toISOString().split("T")[0]
}

export function DayNavigator({ date, onDateChange }: DayNavigatorProps) {
  const today = new Date().toISOString().split("T")[0]
  const isToday = date === today

  return (
    <div className="flex items-center justify-center gap-2 py-3 px-4 border-b border-border">
      <Button
        variant="ghost"
        size="icon"
        onClick={() => onDateChange(addDays(date, -1))}
        className="h-8 w-8"
      >
        <ChevronLeft className="h-4 w-4" />
      </Button>
      <span className="text-sm font-medium min-w-[240px] text-center">
        {formatDateDisplay(date)}
      </span>
      <Button
        variant="ghost"
        size="icon"
        onClick={() => onDateChange(addDays(date, 1))}
        disabled={isToday}
        className="h-8 w-8"
      >
        <ChevronRight className="h-4 w-4" />
      </Button>
    </div>
  )
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/components/DayNavigator.tsx
git commit -m "feat(debug): add DayNavigator component"

Task 12: Create OrgSidebar Component

Files: - Create: src/ui/modules/debug/components/OrgSidebar.tsx

Step 1: Create the component

import { useMemo, useState } from "react"
import Fuse from "fuse.js"
import { Building2, Search } from "lucide-react"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
import type { DayIndexOrg } from "../types/debugger"

type OrgSidebarProps = {
  orgs: DayIndexOrg[]
  selectedOrgId: string | null
  onSelectOrg: (orgId: string) => void
  isLoading?: boolean
}

export function OrgSidebar({
  orgs,
  selectedOrgId,
  onSelectOrg,
  isLoading,
}: OrgSidebarProps) {
  const [search, setSearch] = useState("")

  const fuse = useMemo(
    () => new Fuse(orgs, { keys: ["org_name"], threshold: 0.3 }),
    [orgs]
  )

  const filteredOrgs = useMemo(() => {
    if (!search.trim()) return orgs
    return fuse.search(search).map((result) => result.item)
  }, [orgs, fuse, search])

  if (isLoading) {
    return (
      <div className="w-48 border-r border-border flex flex-col">
        <div className="p-2 border-b border-border">
          <div className="h-8 bg-surface-2 rounded animate-pulse" />
        </div>
        <div className="flex-1 p-2 space-y-2">
          {[1, 2, 3].map((i) => (
            <div key={i} className="h-12 bg-surface-2 rounded animate-pulse" />
          ))}
        </div>
      </div>
    )
  }

  return (
    <div className="w-48 border-r border-border flex flex-col">
      <div className="p-2 border-b border-border">
        <div className="relative">
          <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
          <Input
            placeholder="Search orgs..."
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="h-8 pl-7 text-xs"
          />
        </div>
      </div>
      <div className="flex-1 overflow-y-auto">
        {filteredOrgs.length === 0 ? (
          <div className="p-4 text-center text-xs text-muted-foreground">
            {search ? "No matching orgs" : "No orgs with calls"}
          </div>
        ) : (
          filteredOrgs.map((org) => (
            <button
              key={org.org_id}
              type="button"
              onClick={() => onSelectOrg(org.org_id)}
              className={cn(
                "w-full p-2 text-left hover:bg-surface-2 transition-colors",
                "border-b border-border/50",
                selectedOrgId === org.org_id && "bg-surface-2 border-l-2 border-l-blue-500"
              )}
            >
              <div className="flex items-center gap-2">
                <Building2 className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
                <span className="text-xs font-medium truncate">{org.org_name}</span>
              </div>
              <div className="text-[10px] text-muted-foreground mt-0.5 pl-5">
                {org.call_count.toLocaleString()} calls
              </div>
            </button>
          ))
        )}
      </div>
    </div>
  )
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/components/OrgSidebar.tsx
git commit -m "feat(debug): add OrgSidebar component with fuzzy search"

Task 13: Create LocationSidebar Component

Files: - Create: src/ui/modules/debug/components/LocationSidebar.tsx

Step 1: Create the component

import { useMemo, useState } from "react"
import Fuse from "fuse.js"
import { MapPin, Search } from "lucide-react"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
import type { DayIndexLocation } from "../types/debugger"

type LocationSidebarProps = {
  locations: DayIndexLocation[]
  selectedLocationId: string | null
  onSelectLocation: (locationId: string) => void
  isLoading?: boolean
}

export function LocationSidebar({
  locations,
  selectedLocationId,
  onSelectLocation,
  isLoading,
}: LocationSidebarProps) {
  const [search, setSearch] = useState("")

  const fuse = useMemo(
    () => new Fuse(locations, { keys: ["location_name"], threshold: 0.3 }),
    [locations]
  )

  const filteredLocations = useMemo(() => {
    if (!search.trim()) return locations
    return fuse.search(search).map((result) => result.item)
  }, [locations, fuse, search])

  if (isLoading) {
    return (
      <div className="w-48 border-r border-border flex flex-col">
        <div className="p-2 border-b border-border">
          <div className="h-8 bg-surface-2 rounded animate-pulse" />
        </div>
        <div className="flex-1 p-2 space-y-2">
          {[1, 2, 3].map((i) => (
            <div key={i} className="h-12 bg-surface-2 rounded animate-pulse" />
          ))}
        </div>
      </div>
    )
  }

  return (
    <div className="w-48 border-r border-border flex flex-col">
      <div className="p-2 border-b border-border">
        <div className="relative">
          <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
          <Input
            placeholder="Search locations..."
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="h-8 pl-7 text-xs"
          />
        </div>
      </div>
      <div className="flex-1 overflow-y-auto">
        {filteredLocations.length === 0 ? (
          <div className="p-4 text-center text-xs text-muted-foreground">
            {search ? "No matching locations" : "Select an org"}
          </div>
        ) : (
          filteredLocations.map((loc) => (
            <button
              key={loc.location_id}
              type="button"
              onClick={() => onSelectLocation(loc.location_id)}
              className={cn(
                "w-full p-2 text-left hover:bg-surface-2 transition-colors",
                "border-b border-border/50",
                selectedLocationId === loc.location_id && "bg-surface-2 border-l-2 border-l-blue-500"
              )}
            >
              <div className="flex items-center gap-2">
                <MapPin className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
                <span className="text-xs font-medium truncate">{loc.location_name}</span>
              </div>
              <div className="text-[10px] text-muted-foreground mt-0.5 pl-5">
                {loc.call_count.toLocaleString()} calls
              </div>
            </button>
          ))
        )}
      </div>
    </div>
  )
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/components/LocationSidebar.tsx
git commit -m "feat(debug): add LocationSidebar component"

Task 14: Create CallsSidebar Component

Files: - Create: src/ui/modules/debug/components/CallsSidebar.tsx

Step 1: Create the component

import { useMemo, useState } from "react"
import Fuse from "fuse.js"
import { Phone, PhoneIncoming, PhoneOutgoing, Search } from "lucide-react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"

type Call = {
  call_id: string
  arini_call_id?: string
  created_at?: string
  direction?: "inbound" | "outbound" | null
}

type CallsSidebarProps = {
  calls: Call[]
  selectedCallId: string | null
  onSelectCall: (callId: string) => void
  hasMore?: boolean
  onLoadMore?: () => void
  isLoading?: boolean
}

function formatTime(dateStr?: string): string {
  if (!dateStr) return ""
  const date = new Date(dateStr)
  return date.toLocaleTimeString("en-US", {
    hour: "numeric",
    minute: "2-digit",
    hour12: true,
  })
}

function DirectionIcon({ direction }: { direction?: "inbound" | "outbound" | null }) {
  if (direction === "inbound") {
    return <PhoneIncoming className="h-3 w-3 text-green-500" />
  }
  if (direction === "outbound") {
    return <PhoneOutgoing className="h-3 w-3 text-blue-500" />
  }
  return <Phone className="h-3 w-3 text-muted-foreground" />
}

export function CallsSidebar({
  calls,
  selectedCallId,
  onSelectCall,
  hasMore,
  onLoadMore,
  isLoading,
}: CallsSidebarProps) {
  const [search, setSearch] = useState("")

  const fuse = useMemo(
    () => new Fuse(calls, { keys: ["call_id", "arini_call_id"], threshold: 0.3 }),
    [calls]
  )

  const filteredCalls = useMemo(() => {
    if (!search.trim()) return calls
    return fuse.search(search).map((result) => result.item)
  }, [calls, fuse, search])

  if (isLoading && calls.length === 0) {
    return (
      <div className="w-56 border-r border-border flex flex-col">
        <div className="p-2 border-b border-border">
          <div className="h-8 bg-surface-2 rounded animate-pulse" />
        </div>
        <div className="flex-1 p-2 space-y-2">
          {[1, 2, 3, 4, 5].map((i) => (
            <div key={i} className="h-10 bg-surface-2 rounded animate-pulse" />
          ))}
        </div>
      </div>
    )
  }

  return (
    <div className="w-56 border-r border-border flex flex-col">
      <div className="p-2 border-b border-border">
        <div className="relative">
          <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
          <Input
            placeholder="Search calls..."
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="h-8 pl-7 text-xs"
          />
        </div>
      </div>
      <div className="flex-1 overflow-y-auto">
        {filteredCalls.length === 0 ? (
          <div className="p-4 text-center text-xs text-muted-foreground">
            {search ? "No matching calls" : "Select a location"}
          </div>
        ) : (
          <>
            {filteredCalls.map((call) => (
              <button
                key={call.call_id}
                type="button"
                onClick={() => onSelectCall(call.call_id)}
                className={cn(
                  "w-full p-2 text-left hover:bg-surface-2 transition-colors",
                  "border-b border-border/50",
                  selectedCallId === call.call_id && "bg-surface-2 border-l-2 border-l-blue-500"
                )}
              >
                <div className="flex items-center gap-2">
                  <DirectionIcon direction={call.direction} />
                  <span className="text-xs font-mono truncate">
                    {call.call_id.slice(0, 16)}...
                  </span>
                </div>
                {call.created_at && (
                  <div className="text-[10px] text-muted-foreground mt-0.5 pl-5">
                    {formatTime(call.created_at)}
                  </div>
                )}
              </button>
            ))}
            {hasMore && !search && (
              <div className="p-2">
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={onLoadMore}
                  disabled={isLoading}
                  className="w-full text-xs"
                >
                  {isLoading ? "Loading..." : "Load more"}
                </Button>
              </div>
            )}
          </>
        )}
      </div>
    </div>
  )
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/components/CallsSidebar.tsx
git commit -m "feat(debug): add CallsSidebar component with pagination"

Task 15: Create AgentSidebar Component (Dev)

Files: - Create: src/ui/modules/debug/components/AgentSidebar.tsx

Step 1: Create the component

import { useMemo, useState } from "react"
import Fuse from "fuse.js"
import { Bot, Search } from "lucide-react"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
import { DEV_AGENT_METADATA } from "../types/debugger"

type Agent = {
  agent_id: string
  call_count: number
}

type AgentSidebarProps = {
  agents: Agent[]
  selectedAgentId: string | null
  onSelectAgent: (agentId: string) => void
  isLoading?: boolean
}

function getAgentName(agentId: string): string {
  const meta = DEV_AGENT_METADATA.find((a) => a.agent_id === agentId)
  return meta?.agent_name || `Unknown Agent`
}

export function AgentSidebar({
  agents,
  selectedAgentId,
  onSelectAgent,
  isLoading,
}: AgentSidebarProps) {
  const [search, setSearch] = useState("")

  const agentsWithNames = useMemo(
    () => agents.map((a) => ({ ...a, agent_name: getAgentName(a.agent_id) })),
    [agents]
  )

  const fuse = useMemo(
    () => new Fuse(agentsWithNames, { keys: ["agent_name", "agent_id"], threshold: 0.3 }),
    [agentsWithNames]
  )

  const filteredAgents = useMemo(() => {
    if (!search.trim()) return agentsWithNames
    return fuse.search(search).map((result) => result.item)
  }, [agentsWithNames, fuse, search])

  if (isLoading) {
    return (
      <div className="w-48 border-r border-border flex flex-col">
        <div className="p-2 border-b border-border">
          <div className="h-8 bg-surface-2 rounded animate-pulse" />
        </div>
        <div className="flex-1 p-2 space-y-2">
          {[1, 2, 3].map((i) => (
            <div key={i} className="h-12 bg-surface-2 rounded animate-pulse" />
          ))}
        </div>
      </div>
    )
  }

  return (
    <div className="w-48 border-r border-border flex flex-col">
      <div className="p-2 border-b border-border">
        <div className="relative">
          <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
          <Input
            placeholder="Search agents..."
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="h-8 pl-7 text-xs"
          />
        </div>
      </div>
      <div className="flex-1 overflow-y-auto">
        {filteredAgents.length === 0 ? (
          <div className="p-4 text-center text-xs text-muted-foreground">
            {search ? "No matching agents" : "No agents with calls"}
          </div>
        ) : (
          filteredAgents.map((agent) => (
            <button
              key={agent.agent_id}
              type="button"
              onClick={() => onSelectAgent(agent.agent_id)}
              className={cn(
                "w-full p-2 text-left hover:bg-surface-2 transition-colors",
                "border-b border-border/50",
                selectedAgentId === agent.agent_id && "bg-surface-2 border-l-2 border-l-blue-500"
              )}
            >
              <div className="flex items-center gap-2">
                <Bot className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
                <span className="text-xs font-medium truncate">{agent.agent_name}</span>
              </div>
              <div className="text-[10px] text-muted-foreground mt-0.5 pl-5 font-mono">
                {agent.agent_id.slice(0, 12)}... · {agent.call_count} calls
              </div>
            </button>
          ))
        )}
      </div>
    </div>
  )
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/components/AgentSidebar.tsx
git commit -m "feat(debug): add AgentSidebar component for dev env"

Phase 6: Frontend - Main Page Refactor

Task 16: Create DebugBreadcrumb Component

Files: - Create: src/ui/modules/debug/components/DebugBreadcrumb.tsx

Step 1: Create the component

import { ChevronRight } from "lucide-react"
import { Button } from "@/components/ui/button"

type DebugBreadcrumbProps = {
  orgName?: string
  locationName?: string
  agentName?: string
  callId: string
  onBack: () => void
  env: "prod" | "dev"
}

export function DebugBreadcrumb({
  orgName,
  locationName,
  agentName,
  callId,
  onBack,
  env,
}: DebugBreadcrumbProps) {
  return (
    <div className="flex items-center gap-1 text-sm">
      <Button variant="ghost" size="sm" onClick={onBack} className="h-7 px-2">
        
      </Button>
      {env === "prod" ? (
        <>
          <span className="text-muted-foreground">{orgName || "Unknown Org"}</span>
          <ChevronRight className="h-3 w-3 text-muted-foreground" />
          <span className="text-muted-foreground">{locationName || "Unknown Location"}</span>
        </>
      ) : (
        <span className="text-muted-foreground">{agentName || "Unknown Agent"}</span>
      )}
      <ChevronRight className="h-3 w-3 text-muted-foreground" />
      <span className="font-mono text-xs">{callId.slice(0, 20)}...</span>
    </div>
  )
}

Step 2: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 3: Commit

git add src/ui/modules/debug/components/DebugBreadcrumb.tsx
git commit -m "feat(debug): add DebugBreadcrumb component"

Task 17: Refactor CallsLandingPage - Part 1 (State & Layout)

Files: - Modify: src/ui/modules/debug/pages/CallsLandingPage.tsx

Step 1: Read current implementation

Read the entire src/ui/modules/debug/pages/CallsLandingPage.tsx file to understand existing structure.

Step 2: Replace with new implementation - imports and state

Replace the file content with the new multi-sidebar implementation. This is a large change, so we'll do it in one step with careful testing.

import { useCallback, useEffect, useMemo, useState } from "react"
import { useNavigate, useSearchParams } from "react-router-dom"
import { DayNavigator } from "../components/DayNavigator"
import { OrgSidebar } from "../components/OrgSidebar"
import { LocationSidebar } from "../components/LocationSidebar"
import { CallsSidebar } from "../components/CallsSidebar"
import { AgentSidebar } from "../components/AgentSidebar"
import { DebugBreadcrumb } from "../components/DebugBreadcrumb"
import { useDayIndex } from "../hooks/useDayIndex"
import { useLocationCalls } from "../hooks/useLocationCalls"
import { useDevDayIndex } from "../hooks/useDevDayIndex"
import { CallDebugger } from "./CallDebugger"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import type { DayIndexOrg } from "../types/debugger"

type Environment = "prod" | "dev"

function getToday(): string {
  return new Date().toISOString().split("T")[0]
}

export function CallsLandingPage() {
  const navigate = useNavigate()
  const [searchParams, setSearchParams] = useSearchParams()

  // URL state
  const date = searchParams.get("date") || getToday()
  const env: Environment = (searchParams.get("env") as Environment) || "prod"

  // Selection state
  const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null)
  const [selectedLocationId, setSelectedLocationId] = useState<string | null>(null)
  const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null)
  const [selectedCallId, setSelectedCallId] = useState<string | null>(null)

  // Debounce timer for env switch
  const [envSwitchTimer, setEnvSwitchTimer] = useState<NodeJS.Timeout | null>(null)

  // Data hooks (prod)
  const { data: dayIndex, isLoading: isLoadingIndex } = useDayIndex(
    env === "prod" ? date : null
  )
  const selectedOrg = useMemo(
    () => dayIndex?.orgs.find((o) => o.org_id === selectedOrgId) || null,
    [dayIndex, selectedOrgId]
  )
  const {
    calls: locationCalls,
    hasMore,
    isLoading: isLoadingCalls,
    loadMore,
  } = useLocationCalls(
    env === "prod" ? selectedLocationId : null,
    env === "prod" ? date : null
  )

  // Data hooks (dev)
  const { data: devIndex, isLoading: isLoadingDevIndex } = useDevDayIndex(
    env === "dev" ? date : null
  )

  // Group dev calls by agent_id
  const devAgents = useMemo(() => {
    if (!devIndex?.calls) return []
    const agentMap = new Map<string, { agent_id: string; call_count: number; calls: typeof devIndex.calls }>()
    for (const call of devIndex.calls) {
      // Extract agent_id from call_id pattern or use "unknown"
      // This is a placeholder - actual implementation depends on call_id format
      const agentId = "agent_unknown" // TODO: Extract from call metadata
      let agent = agentMap.get(agentId)
      if (!agent) {
        agent = { agent_id: agentId, call_count: 0, calls: [] }
        agentMap.set(agentId, agent)
      }
      agent.calls.push(call)
      agent.call_count++
    }
    return Array.from(agentMap.values()).sort((a, b) => b.call_count - a.call_count)
  }, [devIndex])

  const selectedDevAgent = useMemo(
    () => devAgents.find((a) => a.agent_id === selectedAgentId) || null,
    [devAgents, selectedAgentId]
  )

  // Auto-select first org/agent on load
  useEffect(() => {
    if (env === "prod" && dayIndex?.orgs.length && !selectedOrgId) {
      setSelectedOrgId(dayIndex.orgs[0].org_id)
    }
  }, [env, dayIndex, selectedOrgId])

  useEffect(() => {
    if (env === "dev" && devAgents.length && !selectedAgentId) {
      setSelectedAgentId(devAgents[0].agent_id)
    }
  }, [env, devAgents, selectedAgentId])

  // Handlers
  const handleDateChange = useCallback(
    (newDate: string) => {
      setSearchParams({ date: newDate, env })
      setSelectedOrgId(null)
      setSelectedLocationId(null)
      setSelectedAgentId(null)
      setSelectedCallId(null)
    },
    [env, setSearchParams]
  )

  const handleEnvChange = useCallback(
    (newEnv: Environment) => {
      // Clear any pending timer
      if (envSwitchTimer) {
        clearTimeout(envSwitchTimer)
      }
      // Debounce the actual switch
      const timer = setTimeout(() => {
        setSearchParams({ date, env: newEnv })
        setSelectedOrgId(null)
        setSelectedLocationId(null)
        setSelectedAgentId(null)
        setSelectedCallId(null)
      }, 500)
      setEnvSwitchTimer(timer)
    },
    [date, setSearchParams, envSwitchTimer]
  )

  const handleSelectOrg = useCallback((orgId: string) => {
    setSelectedOrgId(orgId)
    setSelectedLocationId(null)
    setSelectedCallId(null)
  }, [])

  const handleSelectLocation = useCallback((locationId: string) => {
    setSelectedLocationId(locationId)
    setSelectedCallId(null)
  }, [])

  const handleSelectAgent = useCallback((agentId: string) => {
    setSelectedAgentId(agentId)
    setSelectedCallId(null)
  }, [])

  const handleSelectCall = useCallback((callId: string) => {
    setSelectedCallId(callId)
  }, [])

  const handleBackFromDebugger = useCallback(() => {
    setSelectedCallId(null)
  }, [])

  // Get names for breadcrumb
  const selectedOrgName = selectedOrg?.org_name
  const selectedLocationName = selectedOrg?.locations.find(
    (l) => l.location_id === selectedLocationId
  )?.location_name
  const selectedAgentName = selectedDevAgent?.agent_id // TODO: Use actual name from metadata

  // Render debugger view
  if (selectedCallId) {
    return (
      <div className="h-full flex flex-col">
        <div className="flex items-center justify-between px-4 py-2 border-b border-border">
          <DebugBreadcrumb
            orgName={selectedOrgName}
            locationName={selectedLocationName}
            agentName={selectedAgentName}
            callId={selectedCallId}
            onBack={handleBackFromDebugger}
            env={env}
          />
          <div className="flex items-center gap-2 text-xs text-muted-foreground">
            {date}
          </div>
        </div>
        <div className="flex-1 overflow-hidden">
          <CallDebugger
            callId={selectedCallId}
            date={date}
            env={env}
          />
        </div>
      </div>
    )
  }

  // Render browse view
  return (
    <div className="h-full flex flex-col">
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-2 border-b border-border">
        <DayNavigator date={date} onDateChange={handleDateChange} />
        <div className="flex items-center gap-2">
          <Button
            variant={env === "prod" ? "default" : "outline"}
            size="sm"
            onClick={() => handleEnvChange("prod")}
            className="h-7 text-xs"
          >
            Prod
          </Button>
          <Button
            variant={env === "dev" ? "default" : "outline"}
            size="sm"
            onClick={() => handleEnvChange("dev")}
            className="h-7 text-xs"
          >
            Dev
          </Button>
        </div>
      </div>

      {/* Main content */}
      <div className="flex-1 flex overflow-hidden">
        {env === "prod" ? (
          <>
            <OrgSidebar
              orgs={dayIndex?.orgs || []}
              selectedOrgId={selectedOrgId}
              onSelectOrg={handleSelectOrg}
              isLoading={isLoadingIndex}
            />
            <LocationSidebar
              locations={selectedOrg?.locations || []}
              selectedLocationId={selectedLocationId}
              onSelectLocation={handleSelectLocation}
              isLoading={isLoadingIndex && !!selectedOrgId}
            />
            <CallsSidebar
              calls={locationCalls}
              selectedCallId={selectedCallId}
              onSelectCall={handleSelectCall}
              hasMore={hasMore}
              onLoadMore={loadMore}
              isLoading={isLoadingCalls}
            />
          </>
        ) : (
          <>
            <AgentSidebar
              agents={devAgents}
              selectedAgentId={selectedAgentId}
              onSelectAgent={handleSelectAgent}
              isLoading={isLoadingDevIndex}
            />
            <CallsSidebar
              calls={selectedDevAgent?.calls.map((c) => ({ ...c, call_id: c.call_id })) || []}
              selectedCallId={selectedCallId}
              onSelectCall={handleSelectCall}
              isLoading={isLoadingDevIndex && !!selectedAgentId}
            />
          </>
        )}

        {/* Empty state / instructions */}
        <div className="flex-1 flex items-center justify-center text-muted-foreground text-sm">
          {env === "prod" ? (
            !selectedLocationId ? (
              "Select an org and location to view calls"
            ) : locationCalls.length === 0 && !isLoadingCalls ? (
              "No calls for this location"
            ) : (
              "Select a call to view details"
            )
          ) : (
            !selectedAgentId ? (
              "Select an agent to view calls"
            ) : (selectedDevAgent?.calls.length || 0) === 0 && !isLoadingDevIndex ? (
              "No calls for this agent"
            ) : (
              "Select a call to view details"
            )
          )}
        </div>
      </div>
    </div>
  )
}

Step 3: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds (may have warnings about CallDebugger props that we'll fix next)

Step 4: Commit

git add src/ui/modules/debug/pages/CallsLandingPage.tsx
git commit -m "feat(debug): refactor CallsLandingPage with multi-sidebar layout"

Task 18: Update CallDebugger Props

Files: - Modify: src/ui/modules/debug/pages/CallDebugger.tsx

Step 1: Read current CallDebugger implementation

Read src/ui/modules/debug/pages/CallDebugger.tsx to understand current props.

Step 2: Update to accept props from parent

The CallDebugger currently reads from URL params. We need to allow it to accept props directly when embedded in CallsLandingPage. Add prop types and use props when provided, falling back to URL params.

Add to the component:

type CallDebuggerProps = {
  callId?: string
  date?: string
  env?: string
}

export function CallDebugger({ callId: propCallId, date: propDate, env: propEnv }: CallDebuggerProps = {}) {
  // Use props if provided, otherwise fall back to URL params
  const params = useParams()
  const callId = propCallId || params.callId || ""
  const date = propDate || params.date || ""
  const env = propEnv || params.env || "prod"

  // ... rest of component
}

Step 3: Verify build

Run: devenv shell -- pnpm build Expected: Build succeeds

Step 4: Test the UI

Run: devenv shell -- pnpm dev Navigate to http://localhost:5173/debug Expected: Multi-sidebar layout appears, can navigate between orgs/locations

Step 5: Commit

git add src/ui/modules/debug/pages/CallDebugger.tsx
git commit -m "feat(debug): update CallDebugger to accept props"

Phase 7: Testing & Polish

Task 19: Manual Integration Test

Step 1: Start dev server

Run: devenv shell -- pnpm dev

Step 2: Test prod environment

  1. Navigate to http://localhost:5173/debug
  2. Verify prod is selected by default
  3. Verify orgs sidebar loads with fuzzy search
  4. Click an org, verify locations sidebar populates
  5. Click a location, verify calls sidebar populates with pagination
  6. Click a call, verify debugger opens with breadcrumb
  7. Click back button, verify return to browse view
  8. Test day navigation arrows

Step 3: Test dev environment

  1. Click "Dev" button
  2. Verify agents sidebar loads (may be empty or show "agent_unknown")
  3. Click an agent, verify calls sidebar populates
  4. Test search functionality

Step 4: Test edge cases

  1. Navigate to a date with no calls - verify empty state
  2. Rapidly switch between prod/dev - verify debounce works
  3. Test fuzzy search in each sidebar

Step 5: Document any issues found

Note any bugs or issues for follow-up tasks.


Task 20: Run Full Check

Step 1: Run pnpm check

Run: devenv shell -- pnpm check Expected: All checks pass (biome, tsc, build, deploy --dry-run)

Step 2: Fix any issues

If any issues found, fix them and re-run check.

Step 3: Final commit

git add -A
git commit -m "chore(debug): fix lint and type issues"

Phase 8: Cleanup & PR

Task 21: Create PR

Step 1: Push branch

Run: git push -u origin debugger-dashboard

Step 2: Create PR

Run: gh pr create --fill

Step 3: Link to Linear ticket

Add "Fixes ARE-1557" to PR description if not already included.


Summary

Phase Tasks Description
1 1-4 Backend prod routes (day index, location calls)
2 5 Backend dev routes (S3 index)
3 6 Shared TypeScript types
4 7-9 Data fetching hooks
5 10-15 UI components (sidebars, nav)
6 16-18 Main page refactor
7 19-20 Testing and polish
8 21 PR creation

Total tasks: 21 Estimated commits: ~18