Grand Diomande Research · Full HTML Reader

Operations Hub — iOS Implementation Spec

The web CRM already has two Operations Hub views consolidating all outreach intelligence for a market into a 7-tab surface. The iOS app has the foundation (InboundCommandView, MarketSweepCommandView, InboundService, FieldRoutes) — all connected to the same Supabase. This spec describes the **reorganization + two new hub views** needed to match the web architecture. This is not a ground-up build.

Protocol and Compute research note experiment writeup candidate score 32 .md

Full Public Reader

# Operations Hub — iOS Implementation Spec
Date: 2026-05-28
Status: Ready to implement — pass to Codex
Relates to: Web `PDXSEAOperationsHub.tsx`, `MiamiOperationsHub.tsx`
Supabase project: `wovknbcvxjvayfdflxkn`

---

Context

The web CRM already has two Operations Hub views consolidating all outreach intelligence for a market into a 7-tab surface. The iOS app has the foundation (InboundCommandView, MarketSweepCommandView, InboundService, FieldRoutes) — all connected to the same Supabase. This spec describes the reorganization + two new hub views needed to match the web architecture. This is not a ground-up build.

Real lead counts (verified 2026-05-28 via SQL):
- OR+WA (`inbound_leads` where state IN ('OR','Oregon','WA','Washington')): 1,052 leads, 100
- FL (`inbound_leads` where state IN ('FL','Florida')): 700 leads, 100
- All markets total: 5,270 leads, 100
- All `status = 'new'`, zero contacted — full pipeline untouched

---

What Currently Exists in iOS

ComponentFileStatus
Inbound lead board`Views/Growth/InboundCommandView.swift`✅ Built — flat list, market program selector
Market sweep command`Views/Growth/MarketSweepCommandView.swift`✅ Built — 7 modes: Sweeps/Markets/Prospects/Outreach/Responses/Sequences/Inbound
Response inbox`Views/Growth/ResponseInboxView.swift`✅ Built
Sequence manager`Views/Growth/SequenceManagerView.swift`✅ Built
Market detail`Views/Growth/MarketDetailView.swift`✅ Built
Inbound service`Services/InboundService.swift`✅ Built — Supabase bridge for `inbound_leads`
Field routes + map`Views/FieldRoutes/`✅ Built — cluster map, visit outcomes, day summary
Visits`Views/Visits/`✅ Built — check-in, revisit list

What's missing: Metro/region-grouped contact queues for OR+WA and FL, and a unified hub entry point per market combining map + Instagram + sequences + templates + showcase in one place.

---

Files to Create

Views/Growth/
├── OperationsHubView.swift        ← Generic hub container + tab router
├── PDXSEAHubView.swift            ← PDX/SEA config instantiation
├── MiamiHubView.swift             ← Miami/SFL config instantiation
├── HubContactQueueView.swift      ← Region → City → Leads grouped list
├── HubInstagramQueueView.swift    ← IG-only leads list
├── HubLocationsView.swift         ← Raw locations table with enrichment links
└── HubTemplatesView.swift         ← Template viewer (reads outreach_templates)

Files to modify:
- `Services/InboundService.swift` — add 3 new fetch methods
- `Views/Growth/MarketSweepCommandView.swift` — add hub callout cards + mode routing

---

Data Models

swift
// MARK: - Hub Config Models

struct OperationsHubConfig {
    let title: String
    let emoji: String
    let states: [String]           // ["OR", "Oregon", "WA", "Washington"] — handle both abbrev and full
    let regions: [HubRegion]
    let defaultMarket: String      // "portland" | "south-florida"
    let accentColor: Color
    let leadCountLabel: String     // "1,052 leads"
}

struct HubRegion: Identifiable {
    let id: String                 // "portland-metro", "seattle-metro", etc.
    let key: String
    let label: String
    let emoji: String
    let cities: [String]           // used for city-level grouping
}

// MARK: - Grouped Display Models

struct GroupedRegion: Identifiable {
    let id: String
    let region: HubRegion
    let leads: [InboundLeadWithScore]
    let cities: [CityGroup]
    var isExpanded: Bool = true
}

struct CityGroup: Identifiable {
    let id: String
    let city: String
    let leads: [InboundLeadWithScore]
    var isExpanded: Bool = false
}

// MARK: - Hub Tab

enum HubTab: String, CaseIterable, Identifiable {
    case contactQueue  = "Contact Queue"
    case map           = "Map"
    case instagram     = "Instagram"
    case sequences     = "Sequences"
    case templates     = "Templates"
    case showcase      = "Showcase"
    case allLocations  = "All Locations"

    var id: String { rawValue }

    var icon: String {
        switch self {
        case .contactQueue: return "list.bullet.clipboard"
        case .map:          return "map"
        case .instagram:    return "camera"
        case .sequences:    return "arrow.triangle.2.circlepath"
        case .templates:    return "doc.text"
        case .showcase:     return "chart.bar"
        case .allLocations: return "building.2"
        }
    }
}

---

InboundService.swift — Additions

Add these three methods to the existing `InboundService` class:

swift
// MARK: - Hub Fetch Methods

/// Fetches all inbound_leads for a set of states, handling both abbreviated and full-name variants.
/// Uses pagination (.range) to bypass Supabase's 1000-row default cap.
func fetchLeadsForStates(_ states: [String]) async -> [InboundLeadWithScore] {
    var allLeads: [InboundLeadWithScore] = []
    let pageSize = 1000
    var from = 0

    repeat {
        let batch: [InboundLead] = (try? await supabase
            .from("inbound_leads")
            .select("*")
            .in("state", values: states)
            .range(from: from, to: from + pageSize - 1)
            .execute()
            .value) ?? []

        allLeads.append(contentsOf: batch.map { InboundLeadWithScore(lead: $0, score: nil) })
        from += pageSize

        if batch.count < pageSize { break }
    } while true

    return allLeads
}

/// Fetches raw locations for a set of states (for All Locations tab).
func fetchLocationsForStates(_ states: [String]) async -> [LocationRow] {
    var allLocations: [LocationRow] = []
    let pageSize = 1000
    var from = 0

    repeat {
        let batch: [LocationRow] = (try? await supabase
            .from("locations")
            .select("id, name, city, state, address, contact_email, instagram, phone, google_rating, is_partner")
            .in("state", values: states)
            .range(from: from, to: from + pageSize - 1)
            .execute()
            .value) ?? []

        allLocations.append(contentsOf: batch)
        from += pageSize

        if batch.count < pageSize { break }
    } while true

    return allLocations
}

/// Fetches leads that have an Instagram handle (for Instagram tab).
func fetchInstagramLeads(states: [String]) async -> [InboundLeadWithScore] {
    let allLeads = await fetchLeadsForStates(states)
    return allLeads.filter { $0.lead.instagram != nil && !($0.lead.instagram?.isEmpty ?? true) }
}

---

OperationsHubView.swift

swift
import SwiftUI

struct OperationsHubView: View {
    let config: OperationsHubConfig

    @State private var selectedTab: HubTab = .contactQueue
    @State private var leads: [InboundLeadWithScore] = []
    @State private var locations: [LocationRow] = []
    @State private var isLoading = true

    @StateObject private var inboundService = InboundService()

    var body: some View {
        VStack(spacing: 0) {
            // Header
            hubHeader

            // Tab bar
            hubTabBar

            // Content
            TabView(selection: $selectedTab) {
                HubContactQueueView(config: config, leads: leads)
                    .tag(HubTab.contactQueue)

                HubMapView(config: config, leads: leads)
                    .tag(HubTab.map)

                HubInstagramQueueView(config: config, leads: leads.filter {
                    $0.lead.instagram != nil
                })
                    .tag(HubTab.instagram)

                SequenceManagerView()
                    .tag(HubTab.sequences)

                HubTemplatesView(config: config)
                    .tag(HubTab.templates)

                MarketDetailView(market: config.defaultMarket)
                    .tag(HubTab.showcase)

                HubLocationsView(config: config, locations: locations)
                    .tag(HubTab.allLocations)
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
        }
        .navigationBarHidden(true)
        .task { await loadData() }
    }

    private var hubHeader: some View {
        HStack {
            VStack(alignment: .leading, spacing: 2) {
                Text("\(config.emoji) \(config.title)")
                    .font(.title2.bold())
                Text(config.leadCountLabel)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            Spacer()
            if isLoading {
                ProgressView().scaleEffect(0.8)
            }
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 12)
        .background(Color(.systemBackground))
    }

    private var hubTabBar: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 4) {
                ForEach(HubTab.allCases) { tab in
                    Button {
                        withAnimation(.easeInOut(duration: 0.2)) {
                            selectedTab = tab
                        }
                    } label: {
                        HStack(spacing: 4) {
                            Image(systemName: tab.icon)
                                .font(.caption)
                            Text(tab.rawValue)
                                .font(.caption.weight(.medium))
                        }
                        .padding(.horizontal, 12)
                        .padding(.vertical, 7)
                        .background(
                            selectedTab == tab
                                ? config.accentColor
                                : Color(.secondarySystemBackground)
                        )
                        .foregroundStyle(
                            selectedTab == tab ? .white : .primary
                        )
                        .clipShape(Capsule())
                    }
                    .buttonStyle(.plain)
                }
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 8)
        }
        .background(Color(.systemBackground))
        .overlay(alignment: .bottom) {
            Divider()
        }
    }

    private func loadData() async {
        isLoading = true
        async let leadsTask = inboundService.fetchLeadsForStates(config.states)
        async let locationsTask = inboundService.fetchLocationsForStates(config.states)
        let (fetchedLeads, fetchedLocations) = await (leadsTask, locationsTask)
        leads = fetchedLeads
        locations = fetchedLocations
        isLoading = false
    }
}

---

HubContactQueueView.swift

swift
import SwiftUI

struct HubContactQueueView: View {
    let config: OperationsHubConfig
    let leads: [InboundLeadWithScore]

    @State private var statusFilter: LeadStatus? = nil
    @State private var groupedRegions: [GroupedRegion] = []
    @State private var expandedRegions: Set<String> = []
    @State private var expandedCities: Set<String> = []

    var body: some View {
        VStack(spacing: 0) {
            filterBar
            if groupedRegions.isEmpty {
                emptyState
            } else {
                leadList
            }
        }
        .onChange(of: leads) { _ in buildGroups() }
        .onChange(of: statusFilter) { _ in buildGroups() }
        .onAppear { buildGroups() }
    }

    private var filterBar: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                FilterChip(label: "All", isSelected: statusFilter == nil) {
                    statusFilter = nil
                }
                ForEach([LeadStatus.new, .contacted, .interested, .closed], id: \.self) { status in
                    FilterChip(label: status.rawValue, isSelected: statusFilter == status) {
                        statusFilter = statusFilter == status ? nil : status
                    }
                }
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 10)
        }
    }

    private var leadList: some View {
        List {
            ForEach(groupedRegions) { group in
                Section {
                    if expandedRegions.contains(group.id) {
                        ForEach(group.cities) { cityGroup in
                            CityRowView(
                                cityGroup: cityGroup,
                                isExpanded: expandedCities.contains(cityGroup.id),
                                onToggle: {
                                    toggleCity(cityGroup.id)
                                }
                            )
                        }
                    }
                } header: {
                    RegionHeaderView(
                        region: group.region,
                        leadCount: group.leads.count,
                        newCount: group.leads.filter { $0.lead.status == "new" }.count,
                        igCount: group.leads.filter { $0.lead.instagram != nil }.count,
                        isExpanded: expandedRegions.contains(group.id),
                        accentColor: config.accentColor,
                        onToggle: { toggleRegion(group.id) }
                    )
                }
            }
        }
        .listStyle(.plain)
    }

    private var emptyState: some View {
        VStack(spacing: 12) {
            Image(systemName: "tray")
                .font(.system(size: 40))
                .foregroundStyle(.tertiary)
            Text("No leads match this filter")
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private func buildGroups() {
        let filtered = statusFilter == nil
            ? leads
            : leads.filter { $0.lead.status == statusFilter?.rawValue }

        groupedRegions = config.regions.compactMap { region in
            let regionLeads = filtered.filter { lead in
                let city = lead.lead.city ?? ""
                return region.cities.contains(city)
            }
            guard !regionLeads.isEmpty else { return nil }

            let byCity = Dictionary(grouping: regionLeads) { $0.lead.city ?? "Unknown" }
            let cities = byCity.map { CityGroup(id: $0.key, city: $0.key, leads: $0.value) }
                               .sorted { $0.leads.count > $1.leads.count }

            return GroupedRegion(id: region.id, region: region, leads: regionLeads, cities: cities)
        }

        // Auto-expand all regions on first load
        if expandedRegions.isEmpty {
            expandedRegions = Set(groupedRegions.map { $0.id })
        }
    }

    private func toggleRegion(_ id: String) {
        if expandedRegions.contains(id) {
            expandedRegions.remove(id)
        } else {
            expandedRegions.insert(id)
        }
    }

    private func toggleCity(_ id: String) {
        if expandedCities.contains(id) {
            expandedCities.remove(id)
        } else {
            expandedCities.insert(id)
        }
    }
}

// MARK: - Supporting Views

struct RegionHeaderView: View {
    let region: HubRegion
    let leadCount: Int
    let newCount: Int
    let igCount: Int
    let isExpanded: Bool
    let accentColor: Color
    let onToggle: () -> Void

    var body: some View {
        Button(action: onToggle) {
            HStack {
                Text(region.emoji)
                Text(region.label)
                    .font(.subheadline.bold())
                    .foregroundStyle(.primary)
                Spacer()
                HStack(spacing: 6) {
                    StatBadge(value: leadCount, label: "total", color: .secondary)
                    StatBadge(value: newCount, label: "new", color: accentColor)
                    StatBadge(value: igCount, label: "IG", color: .purple)
                }
                Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            .padding(.vertical, 4)
        }
        .buttonStyle(.plain)
    }
}

struct CityRowView: View {
    let cityGroup: CityGroup
    let isExpanded: Bool
    let onToggle: () -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Button(action: onToggle) {
                HStack {
                    Text(cityGroup.city)
                        .font(.subheadline)
                    Spacer()
                    Text("\(cityGroup.leads.count)")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
                        .font(.caption2)
                        .foregroundStyle(.tertiary)
                }
                .padding(.vertical, 8)
                .padding(.horizontal, 12)
            }
            .buttonStyle(.plain)

            if isExpanded {
                ForEach(cityGroup.leads, id: \.lead.id) { lead in
                    NavigationLink(destination: InboundLeadDetailSheet(lead: lead.lead)) {
                        LeadRowView(lead: lead.lead)
                    }
                    .padding(.leading, 12)
                }
            }
        }
    }
}

struct LeadRowView: View {
    let lead: InboundLead

    var body: some View {
        HStack(spacing: 10) {
            // Priority dot
            Circle()
                .fill(priorityColor)
                .frame(width: 8, height: 8)

            VStack(alignment: .leading, spacing: 2) {
                Text(lead.businessName ?? "Unknown")
                    .font(.subheadline)
                    .lineLimit(1)
                HStack(spacing: 6) {
                    if lead.instagram != nil {
                        Label("IG", systemImage: "camera")
                            .font(.caption2)
                            .foregroundStyle(.purple)
                    }
                    Text(lead.email ?? "")
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                }
            }

            Spacer()

            StatusBadge(status: lead.status ?? "new")
        }
        .padding(.vertical, 6)
    }

    private var priorityColor: Color {
        switch lead.priorityScore ?? 0 {
        case 80...: return .red
        case 60..<80: return .orange
        case 40..<60: return .yellow
        default: return .gray
        }
    }
}

struct StatBadge: View {
    let value: Int
    let label: String
    let color: Color

    var body: some View {
        Text("\(value) \(label)")
            .font(.caption2)
            .foregroundStyle(color)
            .padding(.horizontal, 6)
            .padding(.vertical, 2)
            .background(color.opacity(0.12))
            .clipShape(Capsule())
    }
}

struct FilterChip: View {
    let label: String
    let isSelected: Bool
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Text(label)
                .font(.caption.weight(.medium))
                .padding(.horizontal, 12)
                .padding(.vertical, 6)
                .background(isSelected ? Color.accentColor : Color(.secondarySystemBackground))
                .foregroundStyle(isSelected ? .white : .primary)
                .clipShape(Capsule())
        }
        .buttonStyle(.plain)
    }
}

---

HubInstagramQueueView.swift

swift
import SwiftUI

struct HubInstagramQueueView: View {
    let config: OperationsHubConfig
    let leads: [InboundLeadWithScore]

    var body: some View {
        List(leads, id: \.lead.id) { item in
            HStack(spacing: 12) {
                Image(systemName: "camera.fill")
                    .foregroundStyle(.purple)
                    .frame(width: 32, height: 32)
                    .background(Color.purple.opacity(0.12))
                    .clipShape(Circle())

                VStack(alignment: .leading, spacing: 2) {
                    Text(item.lead.businessName ?? "Unknown")
                        .font(.subheadline.weight(.medium))
                    Text("@\(item.lead.instagram ?? "")")
                        .font(.caption)
                        .foregroundStyle(.purple)
                    Text(item.lead.city ?? "")
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                }

                Spacer()

                VStack(alignment: .trailing, spacing: 4) {
                    // Email status
                    if item.lead.email != nil {
                        Label("Email", systemImage: "envelope.fill")
                            .font(.caption2)
                            .foregroundStyle(.green)
                    }
                    // Open in Instagram
                    Button {
                        openInstagram(handle: item.lead.instagram ?? "")
                    } label: {
                        Text("Open")
                            .font(.caption.weight(.medium))
                            .padding(.horizontal, 10)
                            .padding(.vertical, 4)
                            .background(Color.purple.opacity(0.15))
                            .foregroundStyle(.purple)
                            .clipShape(Capsule())
                    }
                    .buttonStyle(.plain)
                }
            }
            .padding(.vertical, 4)
        }
        .listStyle(.plain)
        .navigationTitle("Instagram (\(leads.count))")
    }

    private func openInstagram(handle: String) {
        let appURL = URL(string: "instagram://user?username=\(handle)")!
        let webURL = URL(string: "https://instagram.com/\(handle)")!
        if UIApplication.shared.canOpenURL(appURL) {
            UIApplication.shared.open(appURL)
        } else {
            UIApplication.shared.open(webURL)
        }
    }
}

---

HubLocationsView.swift

swift
import SwiftUI

struct HubLocationsView: View {
    let config: OperationsHubConfig
    let locations: [LocationRow]

    @State private var searchText = ""

    private var filtered: [LocationRow] {
        guard !searchText.isEmpty else { return locations }
        return locations.filter {
            ($0.name ?? "").localizedCaseInsensitiveContains(searchText) ||
            ($0.city ?? "").localizedCaseInsensitiveContains(searchText)
        }
    }

    var body: some View {
        VStack(spacing: 0) {
            SearchBar(text: $searchText, placeholder: "Search locations...")
            List(filtered, id: \.id) { location in
                LocationRowView(location: location)
            }
            .listStyle(.plain)
        }
        .navigationTitle("All Locations (\(locations.count))")
    }
}

struct LocationRowView: View {
    let location: LocationRow

    var body: some View {
        HStack(spacing: 10) {
            // Email badge
            Circle()
                .fill(location.contactEmail != nil ? Color.green : Color.orange)
                .frame(width: 8, height: 8)

            VStack(alignment: .leading, spacing: 2) {
                Text(location.name ?? "Unknown")
                    .font(.subheadline)
                    .lineLimit(1)
                Text(location.city ?? "")
                    .font(.caption2)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            HStack(spacing: 8) {
                if location.instagram != nil {
                    Image(systemName: "camera")
                        .font(.caption)
                        .foregroundStyle(.purple)
                }
                if location.isPartner == true {
                    Text("Partner")
                        .font(.caption2)
                        .padding(.horizontal, 6)
                        .padding(.vertical, 2)
                        .background(Color.blue.opacity(0.12))
                        .foregroundStyle(.blue)
                        .clipShape(Capsule())
                }
            }
        }
        .padding(.vertical, 4)
    }
}

---

HubMapView.swift

Reuses existing `MapKit` infrastructure. Create a thin adapter that loads `inbound_leads` for the hub's states and plots them as `MKAnnotation` pins on Apple Maps.

swift
import SwiftUI
import MapKit

struct HubMapView: View {
    let config: OperationsHubConfig
    let leads: [InboundLeadWithScore]

    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 45.5, longitude: -122.6), // default Portland
        span: MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
    )
    @State private var selectedLead: InboundLead? = nil

    private var mappableLeads: [InboundLeadWithScore] {
        leads.filter { $0.lead.latitude != nil && $0.lead.longitude != nil }
    }

    var body: some View {
        ZStack(alignment: .bottom) {
            Map(coordinateRegion: $region, annotationItems: mappableLeads) { item in
                MapAnnotation(coordinate: CLLocationCoordinate2D(
                    latitude: item.lead.latitude ?? 0,
                    longitude: item.lead.longitude ?? 0
                )) {
                    LeadMapPin(lead: item.lead) {
                        selectedLead = item.lead
                    }
                }
            }
            .ignoresSafeArea(edges: .top)

            // Stats bar
            HStack(spacing: 16) {
                MapStat(value: leads.count, label: "total", color: .primary)
                MapStat(value: mappableLeads.count, label: "mapped", color: config.accentColor)
                MapStat(value: leads.count - mappableLeads.count, label: "no coords", color: .secondary)
            }
            .padding()
            .background(.ultraThinMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .padding()
        }
        .sheet(item: $selectedLead) { lead in
            InboundLeadDetailSheet(lead: lead)
        }
        .onAppear { centerMap() }
    }

    private func centerMap() {
        // Center based on config states
        if config.states.contains("OR") || config.states.contains("Oregon") {
            region.center = CLLocationCoordinate2D(latitude: 45.9, longitude: -122.4)
        } else if config.states.contains("FL") || config.states.contains("Florida") {
            region.center = CLLocationCoordinate2D(latitude: 26.1, longitude: -80.2)
        }
    }
}

struct LeadMapPin: View {
    let lead: InboundLead
    let onTap: () -> Void

    var body: some View {
        Button(action: onTap) {
            Circle()
                .fill(pinColor)
                .frame(width: 10, height: 10)
                .overlay(Circle().stroke(Color.white, lineWidth: 1.5))
        }
        .buttonStyle(.plain)
    }

    private var pinColor: Color {
        switch lead.status {
        case "contacted":  return .blue
        case "interested": return .green
        case "closed":     return .purple
        default:
            return lead.instagram != nil ? Color.orange : Color.gray
        }
    }
}

struct MapStat: View {
    let value: Int
    let label: String
    let color: Color
    var body: some View {
        VStack(spacing: 2) {
            Text("\(value)").font(.headline.bold()).foregroundStyle(color)
            Text(label).font(.caption2).foregroundStyle(.secondary)
        }
    }
}

---

PDXSEAHubView.swift

swift
import SwiftUI

struct PDXSEAHubView: View {
    static let config = OperationsHubConfig(
        title: "Portland + Seattle",
        emoji: "🌲⛰️",
        states: ["OR", "Oregon", "WA", "Washington"],
        regions: [
            HubRegion(
                id: "portland-metro",
                key: "portland",
                label: "Portland Metro",
                emoji: "🌲",
                cities: ["Portland", "Beaverton", "Hillsboro", "Lake Oswego", "Eugene", "Bend",
                         "Gresham", "Tigard", "Tualatin", "Milwaukie", "Oregon City"]
            ),
            HubRegion(
                id: "seattle-metro",
                key: "seattle",
                label: "Seattle Metro",
                emoji: "⛰️",
                cities: ["Seattle", "Tacoma", "Bellevue", "Redmond", "Kirkland", "Spokane",
                         "Renton", "Bothell", "Shoreline", "Edmonds", "Burien", "Kent",
                         "Federal Way", "Lynnwood", "Issaquah", "Sammamish"]
            )
        ],
        defaultMarket: "portland",
        accentColor: .green,
        leadCountLabel: "1,052 leads"
    )

    var body: some View {
        OperationsHubView(config: Self.config)
    }
}

---

MiamiHubView.swift

swift
import SwiftUI

struct MiamiHubView: View {
    static let config = OperationsHubConfig(
        title: "Miami + South Florida",
        emoji: "🌴☀️",
        states: ["FL", "Florida"],
        regions: [
            HubRegion(
                id: "miami",
                key: "miami",
                label: "Miami",
                emoji: "🌴",
                cities: ["Miami", "Brickell", "Wynwood", "Coconut Grove", "Coral Gables",
                         "Design District", "Downtown Miami", "Little Havana", "Midtown",
                         "North Miami", "Miami Beach", "South Beach", "Mid-Beach", "Surfside", "Bal Harbour"]
            ),
            HubRegion(
                id: "fort-lauderdale",
                key: "fort-lauderdale",
                label: "Fort Lauderdale",
                emoji: "⛵",
                cities: ["Fort Lauderdale", "Wilton Manors", "Hollywood", "Pompano Beach",
                         "Coral Springs", "Deerfield Beach", "Hallandale Beach"]
            ),
            HubRegion(
                id: "boca-delray",
                key: "boca-delray",
                label: "Boca / Delray",
                emoji: "☀️",
                cities: ["Boca Raton", "Delray Beach", "Boynton Beach"]
            ),
            HubRegion(
                id: "west-palm",
                key: "west-palm",
                label: "West Palm",
                emoji: "🌊",
                cities: ["West Palm Beach", "Palm Beach Gardens", "Jupiter", "Lake Worth",
                         "Riviera Beach", "Wellington"]
            ),
            HubRegion(
                id: "naples",
                key: "naples",
                label: "Naples",
                emoji: "🌅",
                cities: ["Naples", "Marco Island", "Immokalee"]
            ),
            HubRegion(
                id: "swfl",
                key: "swfl",
                label: "Southwest Florida",
                emoji: "🦩",
                cities: ["Fort Myers", "Bonita Springs", "Estero", "Cape Coral",
                         "Sanibel", "Lehigh Acres"]
            )
        ],
        defaultMarket: "south-florida",
        accentColor: .orange,
        leadCountLabel: "700 leads"
    )

    var body: some View {
        OperationsHubView(config: Self.config)
    }
}

---

HubTemplatesView.swift

swift
import SwiftUI

struct OutreachTemplate: Identifiable, Decodable {
    let id: String
    let name: String
    let subject: String?
    let body: String?
    let channel: String?    // "email", "instagram", "sms"
    let market: String?
}

struct HubTemplatesView: View {
    let config: OperationsHubConfig

    @State private var templates: [OutreachTemplate] = []
    @State private var isLoading = true

    var body: some View {
        Group {
            if isLoading {
                ProgressView("Loading templates...")
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else if templates.isEmpty {
                VStack(spacing: 12) {
                    Image(systemName: "doc.text")
                        .font(.system(size: 40))
                        .foregroundStyle(.tertiary)
                    Text("No templates yet")
                        .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else {
                List(templates) { template in
                    VStack(alignment: .leading, spacing: 6) {
                        HStack {
                            Text(template.name)
                                .font(.subheadline.weight(.medium))
                            Spacer()
                            if let channel = template.channel {
                                Text(channel)
                                    .font(.caption2)
                                    .padding(.horizontal, 8)
                                    .padding(.vertical, 3)
                                    .background(Color.accentColor.opacity(0.12))
                                    .foregroundStyle(.accentColor)
                                    .clipShape(Capsule())
                            }
                        }
                        if let subject = template.subject {
                            Text("Subject: \(subject)")
                                .font(.caption)
                                .foregroundStyle(.secondary)
                        }
                        if let body = template.body {
                            Text(body)
                                .font(.caption)
                                .foregroundStyle(.tertiary)
                                .lineLimit(2)
                        }
                    }
                    .padding(.vertical, 4)
                }
                .listStyle(.plain)
            }
        }
        .task {
            await loadTemplates()
        }
    }

    private func loadTemplates() async {
        isLoading = true
        // Fetch from outreach_templates table — filter by market if possible
        templates = (try? await SupabaseClient.shared
            .from("outreach_templates")
            .select("*")
            .execute()
            .value) ?? []
        isLoading = false
    }
}

---

MarketSweepCommandView.swift — Entry Point Changes

Add two hub callout cards above the mode picker. Find the section where `GrowthMode` cases are listed and add:

swift
// In GrowthMode enum — add two new cases:
case pdxseaHub = "PDX/SEA Hub"
case miamiHub  = "Miami Hub"

// In MarketSweepCommandView, above the mode picker ScrollView, add:
VStack(spacing: 12) {
    Text("Operations Hubs")
        .font(.caption.weight(.semibold))
        .foregroundStyle(.secondary)
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.horizontal, 16)

    HStack(spacing: 12) {
        HubCalloutCard(
            emoji: "🌲⛰️",
            title: "Portland + Seattle",
            subtitle: "1,052 leads",
            accentColor: .green
        ) {
            selectedMode = .pdxseaHub
        }

        HubCalloutCard(
            emoji: "🌴☀️",
            title: "Miami + SFL",
            subtitle: "700 leads",
            accentColor: .orange
        ) {
            selectedMode = .miamiHub
        }
    }
    .padding(.horizontal, 16)
}
.padding(.top, 12)

// HubCalloutCard component:
struct HubCalloutCard: View {
    let emoji: String
    let title: String
    let subtitle: String
    let accentColor: Color
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            VStack(alignment: .leading, spacing: 6) {
                Text(emoji)
                    .font(.title2)
                Text(title)
                    .font(.subheadline.bold())
                    .foregroundStyle(.primary)
                Text(subtitle)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            .padding(14)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(accentColor.opacity(0.08))
            .overlay(
                RoundedRectangle(cornerRadius: 12)
                    .stroke(accentColor.opacity(0.3), lineWidth: 1)
            )
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
        .buttonStyle(.plain)
    }
}

// In the mode switch/content area, add cases:
case .pdxseaHub:
    PDXSEAHubView()
case .miamiHub:
    MiamiHubView()

---

Key Constraints

1. No outreach sending from iOS. The mark-as-contacted button updates `inbound_leads.status = 'contacted'` in Supabase only. No email sends, no DMs. This is a tracking + planning surface.
2. Supabase 1000-row cap. All fetch methods must use `.range()` pagination loops. DO NOT use `.limit(1000)` as a ceiling — loop until batch < pageSize.
3. State normalization. The `inbound_leads` table has both abbreviated (`OR`, `WA`, `FL`) and full-name (`Oregon`, `Washington`, `Florida`) state values. Always query with both forms: `IN ('OR','Oregon')`.
4. Reuse existing components. `SequenceManagerView`, `MarketDetailView`, `InboundLeadDetailSheet`, `InboundService` all exist and work — wrap, don't rewrite.

---

Implementation Order

1. `InboundService` — add `fetchLeadsForStates`, `fetchLocationsForStates`, `fetchInstagramLeads`
2. Data models — `OperationsHubConfig`, `HubRegion`, `HubTab`, `GroupedRegion`, `CityGroup`
3. `OperationsHubView` — generic container + tab bar + data loading
4. `HubContactQueueView` — region grouping logic + lead rows (this is the most complex)
5. `PDXSEAHubView` + `MiamiHubView` — config declarations
6. `HubMapView` — MapKit adapter (reuse existing infrastructure)
7. `HubInstagramQueueView` — simple list with deep link
8. `HubLocationsView` — simple filtered list
9. `HubTemplatesView` — reads `outreach_templates` table
10. `MarketSweepCommandView` — callout cards + mode routing (entry point, last)

Promotion Decision

Attach run IDs, datasets, metrics, and reproduction commands.

Source Anchor

Milk Men/Documentation/Implementation/OPERATIONS-HUB-IOS-SPEC.md

Detected Structure

Method · Evaluation · Code Anchors · Architecture