/**
* Upstream Command Center — React SPA
* Hash-based routing, no react-router required.
*
* Pages: Overview, Dashboard, Intelligence, Batch Review, Ads, Pipeline,
* Newsletter, Digest, Analytics, Activity Log, + Sales/Contact placeholders
*
* Security note: Blog tab renders pipeline-generated HTML. DOMPurify.sanitize()
* is applied before any DOM mutation (defense-in-depth per PREV-04 spec).
*
* Sales page components (OverviewPage, AnalyticsPage, ActivityLogPage, etc.)
* are defined in sales_app.jsx which loads before this file.
*/
const { useState, useEffect, useCallback, useRef } = React;
// ---------------------------------------------------------------------------
// API base URL — empty string for same-origin (local dev), set for Cloud Run
// ---------------------------------------------------------------------------
const API_BASE = window.__UPSTREAM_API_BASE || "";
function apiFetch(path, options = {}) {
return fetch(API_BASE + path, {
...options,
credentials: "include",
headers: { ...options.headers, "Content-Type": "application/json" },
});
}
// ---------------------------------------------------------------------------
// Shared components
// ---------------------------------------------------------------------------
function Badge({ status }) {
const cls = {
active: "badge badge-ok",
idle: "badge badge-pending",
ok: "badge badge-ok",
error: "badge badge-error",
}[status] || "badge";
return React.createElement("span", { className: cls }, status);
}
function QualityBadge({ score }) {
if (score == null) return null;
const n = parseFloat(score);
const cls = n >= 8 ? "badge badge-ok" : n >= 6 ? "badge badge-pending" : "badge badge-error";
return React.createElement("span", { className: cls }, n.toFixed(1));
}
function PillarBadge({ pillar }) {
const label = (pillar || "").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
return React.createElement("span", { className: "pillar-badge" }, label);
}
function CopyButton({ text }) {
const [copied, setCopied] = useState(false);
function handleCopy() {
navigator.clipboard.writeText(text || "").then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
}
return (
);
}
function Table({ columns, rows, emptyMessage }) {
if (!rows || rows.length === 0) {
return
{emptyMessage || "No data"}
;
}
return (
{columns.map(c => | {c.label} | )}
{rows.map((row, i) => (
{columns.map(c => (
| {c.render ? c.render(row) : row[c.key]} |
))}
))}
);
}
// ---------------------------------------------------------------------------
// Dashboard page
// ---------------------------------------------------------------------------
function TrackCard({ track }) {
const accentColors = {
"Intelligence": "#2D7DD2",
"Digest": "#4CAF7D",
"Blog": "#9B59B6",
};
const accent = accentColors[track.name] || "#2D7DD2";
return (
{track.brief_count != null && (
Briefs
{track.brief_count}
)}
{track.articles_published != null && (
Published
{track.articles_published}
)}
{track.articles_pending != null && (
Pending
{track.articles_pending}
)}
{track.last_run ? `Last run: ${track.last_run}` : "No runs yet"}
);
}
function DashboardPage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
apiFetch("/api/dashboard")
.then(r => r.json())
.then(d => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
if (loading) return Loading dashboard...
;
if (error) return Error: {error}
;
const { tracks = [], pipeline_health = [], analytics = {} } = data;
const healthColumns = [
{ key: "module", label: "Module" },
{ key: "last_success", label: "Last Success", render: r => r.last_success || "—" },
{ key: "next_scheduled", label: "Next Scheduled" },
{ key: "status", label: "Status", render: r => },
];
return (
Dashboard
Pipeline health and 30-day activity at a glance
{tracks.map(track => )}
Monthly Cost
${(analytics.total_cost_month || 0).toFixed(2)}
This month (USD)
Articles Published
{analytics.articles_this_month || 0}
This month
Social Posts Queued
{analytics.social_posts_queued || 0}
Typefully queue
);
}
// ---------------------------------------------------------------------------
// Intelligence page
// ---------------------------------------------------------------------------
function IntelligencePage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [sortField, setSortField] = useState("novelty_score");
const [sortDir, setSortDir] = useState("desc");
useEffect(() => {
apiFetch("/api/intelligence")
.then(r => r.json())
.then(d => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
if (loading) return Loading intelligence brief...
;
if (error) return Error: {error}
;
const { brief = {}, signals = [], topic_ideas = [] } = data;
const sortedSignals = [...signals].sort((a, b) => {
const av = a[sortField] ?? 0;
const bv = b[sortField] ?? 0;
return sortDir === "desc" ? bv - av : av - bv;
});
function toggleSort(field) {
if (sortField === field) {
setSortDir(d => d === "desc" ? "asc" : "desc");
} else {
setSortField(field);
setSortDir("desc");
}
}
const sortIcon = (field) => sortField === field ? (sortDir === "desc" ? " ↓" : " ↑") : "";
return (
Intelligence
Latest weekly brief, signal table, and topic ideas
{brief && (brief.community_pulse || brief.policy_and_industry || brief.connecting_thread) && (
Weekly Brief
{brief.week_id &&
Week: {brief.week_id}
}
{brief.community_pulse && (
Community Pulse
{brief.community_pulse}
)}
{brief.policy_and_industry && (
Policy and Industry
{brief.policy_and_industry}
)}
{brief.connecting_thread && (
Connecting Thread
{brief.connecting_thread}
)}
)}
Signals ({signals.length})
{signals.length === 0 ? (
No signals yet — run uv run pipeline/run.py monitor
) : (
| Source |
Title |
Category |
toggleSort("novelty_score")}>
Novelty{sortIcon("novelty_score")}
|
{sortedSignals.map((s, i) => (
| {s.source || "—"} |
{s.title || s.headline || "—"} |
{s.category || s.pillar || "—"} |
|
))}
)}
{topic_ideas && topic_ideas.length > 0 && (
Topic Suggestions ({topic_ideas.length})
{topic_ideas.map((t, i) => (
{t.title || t.topic || "Untitled"}
{t.pillar &&
}
{t.seo_angle &&
{t.seo_angle}
}
{t.novelty_score != null && (
Novelty:
)}
))}
)}
);
}
// ---------------------------------------------------------------------------
// Batch page — master-detail with 8-tab article preview
// ---------------------------------------------------------------------------
const TAB_LABELS = [
{ id: "blog", label: "Blog" },
{ id: "intelligence_edition", label: "Intelligence Edition" },
{ id: "executive_briefing", label: "Executive Briefing" },
{ id: "linkedin_post", label: "LinkedIn Post" },
{ id: "linkedin_article", label: "LinkedIn Article" },
{ id: "substack", label: "Substack" },
{ id: "video_script", label: "Video Script" },
{ id: "ads", label: "Ads" },
];
/**
* Renders blog HTML in a sandboxed preview div.
* All content is pipeline-generated (not user input).
* DOMPurify sanitization applied before DOM update — defense-in-depth per PREV-04.
*/
function BlogTab({ article }) {
const containerRef = useRef(null);
const rawHtml = article.blog_post || article.blog_html || "";
useEffect(() => {
if (!containerRef.current || !rawHtml) return;
// Sanitize pipeline-generated HTML before rendering (defense-in-depth)
const sanitized = typeof DOMPurify !== "undefined"
? DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } })
: rawHtml;
// Safe: sanitized content has been processed by DOMPurify
containerRef.current.textContent = "";
const wrapper = document.createElement("div");
wrapper.className = "blog-preview prose";
// DOMParser approach: parse sanitized HTML into a doc, then append childNodes
const parsed = new DOMParser().parseFromString(sanitized, "text/html");
Array.from(parsed.body.childNodes).forEach(node => wrapper.appendChild(node.cloneNode(true)));
containerRef.current.appendChild(wrapper);
}, [rawHtml]);
if (!rawHtml) {
return No blog content — run generate first.
;
}
return ;
}
function TextTab({ content, label }) {
if (!content) return No {label} content yet.
;
if (Array.isArray(content)) {
return (
{content.map((item, i) => (
{i + 1}.
{typeof item === "string" ? item : JSON.stringify(item, null, 2)}
))}
);
}
return (
);
}
function VideoScriptTab({ article }) {
const script = article.video_script || article.video_narration || "";
if (!script) return No video script yet.
;
return (
);
}
function AdsTab({ article, batchId }) {
const [adData, setAdData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (article.ad_brief) {
setAdData(article.ad_brief);
setLoading(false);
return;
}
const month = batchId || new Date().toISOString().slice(0, 7);
apiFetch(`/api/ads/${month}`)
.then(r => r.json())
.then(d => {
const briefs = d.briefs || [];
const match = briefs.find(b => b.article_id === article.article_id);
setAdData(match || null);
setLoading(false);
})
.catch(() => { setAdData(null); setLoading(false); });
}, [article.article_id, batchId]);
if (loading) return Loading ad variants...
;
if (!adData) {
return (
No ad briefs yet — run: uv run pipeline/run.py ads --month {batchId}
);
}
const variants = adData.variants || [];
const targeting = adData.targeting || {};
return (
{adData.boost_candidate &&
Boost Candidate
}
{adData.recommended_image && (
Social card:
{adData.recommended_image}
)}
{Object.keys(targeting).length > 0 && (
Targeting
{targeting.job_titles && (
Titles:
{targeting.job_titles.join(", ")}
)}
{targeting.industries && (
Industries:
{targeting.industries.join(", ")}
)}
{targeting.company_size && (
Company size:
{targeting.company_size}
)}
)}
{variants.map((v, i) => (
))}
{variants.length === 0 &&
No ad variants in this brief.
}
);
}
function ArticleDetail({ article, batchId, articleIndex, onArticleUpdate }) {
const [activeTab, setActiveTab] = useState("blog");
const [editTitle, setEditTitle] = useState(article.title || "");
const [editSummary, setEditSummary] = useState(article.summary || "");
const [editCta, setEditCta] = useState(article.cta_text || "");
const [saving, setSaving] = useState(false);
useEffect(() => {
setEditTitle(article.title || "");
setEditSummary(article.summary || "");
setEditCta(article.cta_text || "");
}, [article.article_id]);
function saveEdits(overrides) {
if (saving) return;
setSaving(true);
const resolvedId = batchId || "latest";
apiFetch(`/api/batch/${resolvedId}/article/${articleIndex}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Object.assign({ title: editTitle, summary: editSummary, cta_text: editCta }, overrides || {})),
})
.then(r => r.json())
.then(updated => { onArticleUpdate(articleIndex, updated); setSaving(false); })
.catch(() => setSaving(false));
}
return (
setEditTitle(e.target.value)}
onBlur={() => saveEdits()}
placeholder="Article title"
/>
{article.quality_score != null &&
}
{TAB_LABELS.map(t => (
))}
{activeTab === "blog" &&
}
{activeTab === "intelligence_edition" && (
)}
{activeTab === "executive_briefing" && (
)}
{activeTab === "linkedin_post" && (
)}
{activeTab === "linkedin_article" && (
)}
{activeTab === "substack" && (
)}
{activeTab === "video_script" &&
}
{activeTab === "ads" && (
)}
);
}
// ---------------------------------------------------------------------------
// Publish workflow components — Plan 03 (PREV-07)
// ---------------------------------------------------------------------------
function PublishLogPanel({ publishId, onComplete }) {
const [logs, setLogs] = useState([]);
const [progress, setProgress] = useState(0);
const logsEndRef = useRef(null);
useEffect(() => {
if (!publishId) return;
const es = new EventSource("/api/status/" + publishId);
es.onmessage = (e) => {
const event = JSON.parse(e.data);
setLogs(prev => [...prev, event]);
if (event.step === "publishing_blog" && event.status === "success") {
setProgress(prev => prev + 1);
}
if (event.step === "complete") {
es.close();
onComplete(event.summary);
}
};
es.onerror = () => es.close();
return () => es.close();
}, [publishId]);
useEffect(() => {
if (logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [logs]);
return (
Publish Log
{logs.filter(l => l.step !== "complete").map((log, i) => (
{log.status === "success" ? "✓" : log.status === "error" ? "✗" : "…"}
{(log.step || "").replace(/_/g, " ")}
{log.article &&
{log.article}}
{log.detail}
{log.url && (
{log.url}
)}
))}
);
}
function PostPublishSummary({ summary, onClose }) {
const blogUrls = (summary.blog_urls || []).filter(Boolean);
const typefullyIds = (summary.typefully_ids || []).filter(Boolean);
const videoUrls = (summary.video_urls || []).filter(Boolean);
return (
All approved articles published successfully
{blogUrls.length > 0 && (
Ghost Blog Posts
{blogUrls.map((url, i) => (
{url}
))}
)}
{typefullyIds.length > 0 && (
)}
{videoUrls.length > 0 && (
R2 Video URLs
{videoUrls.map((url, i) => (
{url}
))}
)}
Total cost this run: ${(summary.total_cost || 0).toFixed(2)}
· Summary also sent to Telegram
);
}
function BatchPage() {
const [batchData, setBatchData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const [publishing, setPublishing] = useState(false);
const [publishId, setPublishId] = useState(null);
const [publishSummary, setPublishSummary] = useState(null);
useEffect(() => {
apiFetch("/api/batch/latest")
.then(r => r.json())
.then(d => { setBatchData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
function handleApproveToggle(index) {
const article = batchData.articles[index];
const newApproved = !article.approved;
const resolvedId = batchData.batch_id || "latest";
apiFetch("/api/batch/" + resolvedId + "/article/" + index, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ approved: newApproved }),
})
.then(r => r.json())
.then(updated => {
setBatchData(prev => {
const articles = [...prev.articles];
articles[index] = Object.assign({}, articles[index], updated);
return Object.assign({}, prev, { articles });
});
})
.catch(() => {});
}
function handleArticleUpdate(index, updatedArticle) {
setBatchData(prev => {
const articles = [...prev.articles];
articles[index] = Object.assign({}, articles[index], updatedArticle);
return Object.assign({}, prev, { articles });
});
}
async function handlePublish() {
if (!batchData) return;
setPublishing(true);
setPublishSummary(null);
setPublishId(null);
try {
const r = await apiFetch("/api/publish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ batch_id: batchData.batch_id || "latest" }),
});
const { publish_id } = await r.json();
setPublishId(publish_id);
} catch (e) {
console.error("Publish failed:", e);
setPublishing(false);
}
}
if (loading) return Loading batch...
;
if (error) return Error: {error}
;
if (!batchData || !batchData.articles || batchData.articles.length === 0) {
return (
Batch Review
Review and approve articles before publishing
No batch found. Run: uv run pipeline/run.py generate
);
}
const { articles, batch_id } = batchData;
const approvedCount = articles.filter(a => a.approved).length;
const selectedArticle = articles[selectedIndex] || articles[0];
return (
Batch Review — {batch_id}
{approvedCount} of {articles.length} articles approved
{approvedCount} approved
{/* Publish log panel slides in when publish starts */}
{publishId && !publishSummary && (
{ setPublishSummary(s); setPublishing(false); }}
/>
)}
{/* Post-publish summary card */}
{publishSummary && (
{ setPublishSummary(null); setPublishId(null); }}
/>
)}
{articles.map((article, i) => (
setSelectedIndex(i)}
>
{article.title || ("Article " + (i + 1))}
{article.quality_score != null &&
}
e.stopPropagation()}>
))}
);
}
// ---------------------------------------------------------------------------
// Ads standalone page (route: #ads)
// ---------------------------------------------------------------------------
function AdsPage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const now = new Date();
const month = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
apiFetch("/api/ads/" + month)
.then(r => r.json())
.then(d => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
if (loading) return Loading ad briefs...
;
if (error) return Error: {error}
;
const briefs = (data && data.briefs) || [];
const month = data && data.month;
return (
Ad Briefs
LinkedIn ad copy variants and social card pairings — {month || "current month"}
{briefs.length === 0 ? (
No ad briefs yet — run: uv run pipeline/run.py ads --month {month}
) : (
{briefs.map((brief, i) => (
{brief.article_title}
{brief.boost_candidate &&
Boost Candidate}
{brief.recommended_image && (
Social card:
{brief.recommended_image}
)}
{(brief.variants || []).map((v, j) => (
))}
))}
)}
);
}
// ---------------------------------------------------------------------------
// NewsletterPage — Plan 03 (PREV-05)
// ---------------------------------------------------------------------------
function NewsletterPage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
apiFetch("/api/newsletter/latest")
.then(r => r.json())
.then(d => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
if (loading) return Loading newsletter previews...
;
if (error) return Error: {error}
;
const { intelligence_html = "", briefing_html = "" } = data || {};
const fallback = "No newsletter HTML — run generate to build a batch first.
";
return (
Newsletter
Intelligence Edition and Executive Briefing preview — rendered in sandboxed iframes
);
}
// ---------------------------------------------------------------------------
// DigestPage — Plan 03 (PREV-06)
// ---------------------------------------------------------------------------
function DigestPage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
apiFetch("/api/digest/preview")
.then(r => r.json())
.then(d => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
if (loading) return Loading digest preview...
;
if (error) return Error: {error}
;
const { html = "", customer_name = "Sample Corp" } = data || {};
return (
Customer Digest
Track 2: Customer Digest Preview — rendered with dummy segment data
Preview customer: {customer_name} · Uses dummy metrics (denial_rate: 12%, drift_score: 0.63)
Track 2: Customer Digest Preview (Sample Data)
);
}
// ---------------------------------------------------------------------------
// Pipeline visualization page
// ---------------------------------------------------------------------------
function PipelineNode({ node, onClick }) {
const statusColors = { green: "#4CAF7D", yellow: "#F2994A", red: "#E74C3C" };
const borderColor = statusColors[node.status] || "#6B7A8D";
return (
onClick && onClick(node)}
>
{node.label}
{node.item_count}
{node.detail}
{node.last_run && (
{node.last_run}
)}
);
}
function PipelinePage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
apiFetch("/api/pipeline")
.then(r => r.json())
.then(d => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
if (loading) return Loading pipeline...
;
if (error) return Error: {error}
;
const { nodes = [], channels = [], summary = {} } = data;
return (
Pipeline
Visual data flow — Sources through Publish
{nodes.map((node, i) => (
{i < nodes.length - 1 && →
}
))}
Distribution Channels
{channels.map(ch => (
))}
Total Articles
{summary.total_articles || 0}
Approved
{summary.approved || 0}
Published
{summary.published || 0}
);
}
// ---------------------------------------------------------------------------
// Login page (password gate for deployed dashboard)
// ---------------------------------------------------------------------------
function LoginPage({ onLogin }) {
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
setError(null);
try {
const r = await apiFetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ password }),
});
if (r.ok) {
onLogin();
} else {
setError("Wrong password");
}
} catch (exc) {
setError("Connection failed");
}
setLoading(false);
}
return (
Upstream Command Center
Private dashboard — enter password to continue
);
}
// ---------------------------------------------------------------------------
// CommandBar — floating command palette activated by "/" keypress
// ---------------------------------------------------------------------------
function CommandBar({ onClose }) {
const [query, setQuery] = React.useState("");
const [result, setResult] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const inputRef = React.useRef(null);
React.useEffect(() => { inputRef.current?.focus(); }, []);
React.useEffect(() => {
function handler(e) { if (e.key === "Escape") onClose(); }
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [onClose]);
const QUICK_COMMANDS = [
"Score leads", "Run monitor", "Status", "Find prospects", "Show pipeline"
];
async function handleSubmit(e) {
e.preventDefault();
if (!query.trim()) return;
setLoading(true);
setResult(null);
try {
const resp = await fetch((window.__UPSTREAM_API_BASE || "") + "/api/ask", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query })
});
const data = await resp.json();
setResult(data);
} catch (e) {
setResult({ error: e.message });
} finally {
setLoading(false);
}
}
return (
e.stopPropagation()}>
{!query && (
{QUICK_COMMANDS.map(cmd => (
setQuery(cmd)}>
{cmd}
))}
)}
{result && (
{result.error
?
{result.error}
:
{typeof result.output === "string" ? result.output : JSON.stringify(result, null, 2)}
}
)}
);
}
// ---------------------------------------------------------------------------
// Navigation config
// ---------------------------------------------------------------------------
const NAV_SECTIONS = [
{ label: "OVERVIEW", items: [
{ hash: "#overview", label: "Overview", component: OverviewPage },
]},
{ label: "CONTENT", items: [
{ hash: "#dashboard", label: "Dashboard", component: DashboardPage },
{ hash: "#pipeline", label: "Pipeline", component: PipelinePage },
{ hash: "#intelligence", label: "Intelligence", component: IntelligencePage },
{ hash: "#batch", label: "Batch Review", component: BatchPage },
{ hash: "#newsletter", label: "Newsletter", component: NewsletterPage },
{ hash: "#digest", label: "Customer Digest", component: DigestPage },
{ hash: "#ads", label: "Ad Briefs", component: AdsPage },
]},
{ label: "SALES", items: [
{ hash: "#prospects", label: "Prospect Feed", component: ProspectFeedPage },
{ hash: "#sequences", label: "Active Sequences", component: SequencesPage },
{ hash: "#deals", label: "Deal Board", component: DealBoardPage },
{ hash: "#replies", label: "Reply Inbox", component: ReplyInboxPage },
]},
{ label: "CREATIVE", items: [
{ hash: "#create", label: "Freestyle Creator", component: FreestyleCreatorPage },
]},
{ label: "CONTACTS", items: [
{ hash: "#contacts", label: "All Contacts", component: ContactsPage },
{ hash: "#segments", label: "Segments", component: SegmentsPage },
]},
{ label: "SYSTEM", items: [
{ hash: "#analytics", label: "Analytics", component: AnalyticsPage },
{ hash: "#activity", label: "Activity Log", component: ActivityLogPage },
]},
];
const NAV_ITEMS = NAV_SECTIONS.flatMap(s => s.items);
function resolveCurrentHash() {
const hash = window.location.hash || "#overview";
// Handle #contact/UUID pattern — route to ContactDetailPage
if (hash.startsWith("#contact/")) {
return { hash: hash, label: "Contact Detail", component: ContactDetailPage };
}
return NAV_ITEMS.find(item => item.hash === hash) || NAV_ITEMS[0];
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function Sidebar({ activeHash, onNavigate }) {
return (
);
}
// ---------------------------------------------------------------------------
// App root
// ---------------------------------------------------------------------------
function App() {
const [activeHash, setActiveHash] = useState(resolveCurrentHash().hash);
const [authed, setAuthed] = useState(false);
const [checkingAuth, setCheckingAuth] = useState(true);
const [commandBarOpen, setCommandBarOpen] = useState(false);
useEffect(() => {
function handleKeyDown(e) {
if (e.key === "/" && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
e.preventDefault();
setCommandBarOpen(true);
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
// Check if already authenticated (cookie-based)
useEffect(() => {
apiFetch("/api/health")
.then(r => {
setAuthed(r.ok);
setCheckingAuth(false);
})
.catch(() => {
// No backend or auth required — assume local dev
setAuthed(true);
setCheckingAuth(false);
});
}, []);
useEffect(() => {
function handleHashChange() {
const hash = window.location.hash || "#overview";
// For #contact/UUID, preserve full hash; otherwise resolve from nav
if (hash.startsWith("#contact/")) {
setActiveHash(hash);
} else {
setActiveHash(resolveCurrentHash().hash);
}
}
window.addEventListener("hashchange", handleHashChange);
return () => window.removeEventListener("hashchange", handleHashChange);
}, []);
function navigate(hash) {
window.location.hash = hash;
setActiveHash(hash);
}
if (checkingAuth) return Loading...
;
if (!authed) return setAuthed(true)} />;
const resolvedPage = activeHash.startsWith("#contact/")
? { hash: activeHash, component: ContactDetailPage }
: (NAV_ITEMS.find(item => item.hash === activeHash) || NAV_ITEMS[0]);
const ActivePage = resolvedPage.component;
return (
{commandBarOpen &&
setCommandBarOpen(false)} />}
);
}
// ---------------------------------------------------------------------------
// Mount
// ---------------------------------------------------------------------------
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render();