// ----------------------------------------------------------------------------- // Home-Ka — US real-estate aggregator (Groupe KA) // Author: Simon-Pierre Boucher — contact@spboucher.ai // pages/Admin.tsx : back-office (/admin/sources, /admin/connectors, // /admin/brokerages) — reachable by URL only, not linked in the nav. // Optional token (X-Admin-Token) stored in localStorage (homeka_admin_token). // ----------------------------------------------------------------------------- import { useCallback, useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { AdminOverview, AdminSource, Brokerage, BrokerageFilters, ConnectorFamily, CustomConnector, PARTNERSHIP_STATUSES, fetchAdminBrokerages, fetchAdminConnectors, fetchAdminOverview, fetchAdminSources, getAdminToken, inspectBrokerage, runDiscovery, setAdminToken, setBrokerageStatus, sourceName, } from "../api"; import { Ico } from "../components/Icons"; const BROK_PAGE = 50; const n = (x: number | null | undefined) => x == null ? "—" : Math.round(x).toLocaleString("en-US"); function ago(ts: number | null | undefined): string { if (ts == null) return "never"; const s = Math.max(0, Math.floor(Date.now() / 1000 - (ts > 1e12 ? ts / 1000 : ts))); if (s < 3600) return `${Math.round(s / 60)} min ago`; if (s < 172800) return `${Math.round(s / 3600)} h ago`; return `${Math.round(s / 86400)} d ago`; } // --- Token bar ----------------------------------------------------------------- function TokenBar({ onChange }: { onChange: () => void }) { const [token, setToken] = useState(getAdminToken()); const [saved, setSaved] = useState(false); return (
{ setToken(e.target.value); setSaved(false); }} autoComplete="off" />
); } // --- Overview cards -------------------------------------------------------------- function Overview({ ov, error }: { ov: AdminOverview | null; error: string | null }) { if (error) return
Overview unavailable — {error}
; if (!ov) return
Loading overview…
; return (
{n(ov.active_listings)}
Active listings
{n(ov.properties)}
Properties
{n(ov.enabled_sources)}
Enabled sources
{n(ov.errors_24h)}
Errors (24 h)
{ago(ov.last_successful_sync)}
Last successful sync
Brokerages by partnership status
{ov.brokerages_by_status.length === 0 && —} {ov.brokerages_by_status.map((b) => ( {b.partnership_status || "unknown"} {n(b.n)} ))}
); } // --- Sources tab ------------------------------------------------------------------- function SourcesTab({ reloadKey }: { reloadKey: number }) { const [rows, setRows] = useState(null); const [error, setError] = useState(null); useEffect(() => { setRows(null); setError(null); fetchAdminSources().then((r) => setRows(r.sources)).catch((e) => setError(String(e))); }, [reloadKey]); if (error) return
Could not load sources — {error}
; if (!rows) return
Loading sources…
; return (
{rows.map((s) => ( ))}
SourceTypeStates ActivePublishedLast sync FreshnessErrors 7dLast message
{s.name || s.id} {!s.enabled && disabled}
{s.id}
{s.connector_type ?? "—"} {s.states?.length ? s.states.join(", ") : "—"} {n(s.active_listings)} {n(s.published)} {s.last_sync != null ? ago(s.last_sync) : "never"}{" "} {s.last_sync != null && ( s.last_ok ? OK : Error )} {s.freshness_hours != null ? `${s.freshness_hours} h` : "—"} {n(s.errors_7d)} {s.last_message ?? "—"}
); } // --- Connectors tab ------------------------------------------------------------------ function ConnectorsTab({ reloadKey }: { reloadKey: number }) { const [families, setFamilies] = useState(null); const [custom, setCustom] = useState([]); const [error, setError] = useState(null); useEffect(() => { setFamilies(null); setError(null); fetchAdminConnectors() .then((r) => { setFamilies(r.families); setCustom(r.custom); }) .catch((e) => setError(String(e))); }, [reloadKey]); if (error) return
Could not load connectors — {error}
; if (!families) return
Loading connectors…
; return ( <>
{families.map((f) => (
{f.family} {n(f.instances)} instance{f.instances !== 1 ? "s" : ""}
{f.class} · {f.module}

{f.doc || "—"}

))}
{custom.length > 0 && ( <>

Custom connectors

{custom.map((c) => ( ))}
SourceClassModule
{sourceName(c.source_id)} {c.source_id} {c.class} {c.module}
)} ); } // --- Brokerages tab --------------------------------------------------------------------- function BrokeragesTab({ reloadKey }: { reloadKey: number }) { const [rows, setRows] = useState(null); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [error, setError] = useState(null); const [busy, setBusy] = useState>({}); const [discovering, setDiscovering] = useState(false); // filters const [q, setQ] = useState(""); const [state, setState] = useState(""); const [status, setStatus] = useState(""); const [feedType, setFeedType] = useState(""); const [minPriority, setMinPriority] = useState(""); const [inspected, setInspected] = useState(""); const [sort, setSort] = useState("priority"); const filters: BrokerageFilters = { q, state, status, feed_type: feedType, min_priority: minPriority, inspected, sort, }; const filterKey = JSON.stringify(filters); const load = useCallback((p: number) => { setRows(null); setError(null); fetchAdminBrokerages(JSON.parse(filterKey) as BrokerageFilters, BROK_PAGE, (p - 1) * BROK_PAGE) .then((r) => { setRows(r.brokerages); setTotal(r.total); }) .catch((e) => setError(String(e))); }, [filterKey]); useEffect(() => { setPage(1); }, [filterKey]); useEffect(() => { load(page); }, [load, page, reloadKey]); const totalPages = Math.max(1, Math.ceil(total / BROK_PAGE)); const onStatus = async (b: Brokerage, next: string) => { setBusy((x) => ({ ...x, [b.id]: true })); try { await setBrokerageStatus(b.id, next); setRows((rs) => rs?.map((r) => r.id === b.id ? { ...r, partnership_status: next } : r) ?? null); } catch (e) { alert(`Status update failed: ${e}`); } finally { setBusy((x) => ({ ...x, [b.id]: false })); } }; const onInspect = async (b: Brokerage) => { setBusy((x) => ({ ...x, [b.id]: true })); try { await inspectBrokerage(b.id); // refresh the page to pick up the newly inspected row load(page); } catch (e) { alert(`Inspection failed: ${e}`); setBusy((x) => ({ ...x, [b.id]: false })); } }; const onDiscover = async () => { setDiscovering(true); try { await runDiscovery(); alert("Discovery batch started in the background."); } catch (e) { alert(`Discovery failed to start: ${e}`); } finally { setDiscovering(false); } }; const contacts = (b: Brokerage): { label: string; email: string }[] => { const out: { label: string; email: string }[] = []; if (b.partnership_contact && b.partnership_contact.includes("@")) out.push({ label: "partnership", email: b.partnership_contact }); if (b.technical_contact && b.technical_contact.includes("@")) out.push({ label: "technical", email: b.technical_contact }); return out; }; return ( <>
setQ(e.target.value)} aria-label="Search brokerages" /> setState(e.target.value.toUpperCase())} aria-label="State" /> setMinPriority(e.target.value)} aria-label="Minimum priority score" />
{error &&
Could not load brokerages — {error}
} {!rows && !error &&
Loading brokerages…
} {rows && ( <>

{n(total)} brokerage{total !== 1 ? "s" : ""} — page {page} / {totalPages}

{rows.map((b) => ( ))} {rows.length === 0 && ( )}
BrokerageStatesAgentsListings IDX providerFeedFeed %Priority StatusContactsInspected
{b.website ? {b.name} : {b.name}} {b.mls_affiliations?.length > 0 && (
{b.mls_affiliations.join(", ")}
)}
{b.states?.length ? b.states.join(", ") : "—"} {n(b.estimated_agents)} {n(b.estimated_listings)} {b.idx_provider ?? "—"} {b.reso_detected ? RESO : null} {b.possible_feed_type ?? "—"} {b.feed_probability_score != null ? Math.round(b.feed_probability_score) : "—"} {b.priority_score != null ? Math.round(b.priority_score) : "—"} {contacts(b).length === 0 && "—"} {contacts(b).map((c) => ( ))} {b.last_inspected != null ? ago(b.last_inspected) : "never"} {b.inspect_error && ( err )}
No brokerage matches these filters.
{totalPages > 1 && ( )} )} ); } // --- Page ------------------------------------------------------------------------ export type AdminTab = "sources" | "connectors" | "brokerages"; export default function AdminPage({ tab }: { tab: AdminTab }) { const [ov, setOv] = useState(null); const [ovError, setOvError] = useState(null); const [reloadKey, setReloadKey] = useState(0); const loadOverview = useCallback(() => { setOv(null); setOvError(null); fetchAdminOverview().then(setOv).catch((e) => setOvError(String(e))); }, []); useEffect(() => { loadOverview(); }, [loadOverview, reloadKey]); const tabs: { id: AdminTab; label: string }[] = [ { id: "sources", label: "Sources" }, { id: "connectors", label: "Connectors" }, { id: "brokerages", label: "Brokerages" }, ]; return (
Back office

Home-Ka admin

Operational view of the aggregation pipeline — sources, connector families and the brokerage partnership funnel.

setReloadKey((k) => k + 1)} /> {tab === "sources" && } {tab === "connectors" && } {tab === "brokerages" && }
); }