upgrade navigation and route prefetch responsiveness

This commit is contained in:
2026-03-01 20:45:08 -05:00
parent d6895f185f
commit dc84f34fe9
17 changed files with 1208 additions and 142 deletions

View File

@@ -1,5 +1,6 @@
'use client';
import { useQueryClient } from '@tanstack/react-query';
import Link from 'next/link';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Activity, Bot, RefreshCw, Sparkles } from 'lucide-react';
@@ -10,19 +11,23 @@ import { MetricCard } from '@/components/dashboard/metric-card';
import { TaskFeed } from '@/components/dashboard/task-feed';
import { StatusPill } from '@/components/ui/status-pill';
import { useAuthGuard } from '@/hooks/use-auth-guard';
import { useLinkPrefetch } from '@/hooks/use-link-prefetch';
import { useTaskPoller } from '@/hooks/use-task-poller';
import {
getLatestPortfolioInsight,
getPortfolioSummary,
getTask,
listFilings,
listRecentTasks,
listWatchlist,
queuePortfolioInsights,
queuePriceRefresh
} from '@/lib/api';
import type { PortfolioInsight, PortfolioSummary, Task } from '@/lib/types';
import { formatCompactCurrency, formatCurrency, formatPercent } from '@/lib/format';
import { queryKeys } from '@/lib/query/keys';
import {
filingsQueryOptions,
latestPortfolioInsightQueryOptions,
portfolioSummaryQueryOptions,
recentTasksQueryOptions,
taskQueryOptions,
watchlistQueryOptions
} from '@/lib/query/options';
type DashboardState = {
summary: PortfolioSummary;
@@ -48,22 +53,33 @@ const EMPTY_STATE: DashboardState = {
export default function CommandCenterPage() {
const { isPending, isAuthenticated, session } = useAuthGuard();
const queryClient = useQueryClient();
const { prefetchPortfolioSurfaces } = useLinkPrefetch();
const [state, setState] = useState<DashboardState>(EMPTY_STATE);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const loadData = useCallback(async () => {
setLoading(true);
const summaryOptions = portfolioSummaryQueryOptions();
const filingsOptions = filingsQueryOptions({ limit: 200 });
const watchlistOptions = watchlistQueryOptions();
const tasksOptions = recentTasksQueryOptions(20);
const insightOptions = latestPortfolioInsightQueryOptions();
if (!queryClient.getQueryData(summaryOptions.queryKey)) {
setLoading(true);
}
setError(null);
try {
const [summaryRes, filingsRes, watchlistRes, tasksRes, insightRes] = await Promise.all([
getPortfolioSummary(),
listFilings({ limit: 200 }),
listWatchlist(),
listRecentTasks(20),
getLatestPortfolioInsight()
queryClient.ensureQueryData(summaryOptions),
queryClient.ensureQueryData(filingsOptions),
queryClient.ensureQueryData(watchlistOptions),
queryClient.ensureQueryData(tasksOptions),
queryClient.ensureQueryData(insightOptions)
]);
setState({
@@ -78,7 +94,7 @@ export default function CommandCenterPage() {
} finally {
setLoading(false);
}
}, []);
}, [queryClient]);
useEffect(() => {
if (!isPending && isAuthenticated) {
@@ -90,6 +106,10 @@ export default function CommandCenterPage() {
taskId: activeTaskId,
onTerminalState: () => {
setActiveTaskId(null);
void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() });
void queryClient.invalidateQueries({ queryKey: queryKeys.latestPortfolioInsight() });
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: ['filings'] });
void loadData();
}
});
@@ -102,8 +122,10 @@ export default function CommandCenterPage() {
try {
const { task } = await queuePriceRefresh();
setActiveTaskId(task.id);
const latest = await getTask(task.id);
const latest = await queryClient.fetchQuery(taskQueryOptions(task.id));
setState((prev) => ({ ...prev, tasks: [latest.task, ...prev.tasks] }));
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to queue price refresh');
}
@@ -117,8 +139,10 @@ export default function CommandCenterPage() {
try {
const { task } = await queuePortfolioInsights();
setActiveTaskId(task.id);
const latest = await getTask(task.id);
const latest = await queryClient.fetchQuery(taskQueryOptions(task.id));
setState((prev) => ({ ...prev, tasks: [latest.task, ...prev.tasks] }));
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: queryKeys.latestPortfolioInsight() });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to queue AI insight');
}
@@ -211,15 +235,34 @@ export default function CommandCenterPage() {
<p className="panel-heading text-xs uppercase text-[color:var(--terminal-muted)]">Financials</p>
<p className="mt-2 text-sm text-[color:var(--terminal-bright)]">Focus on multi-period filing metrics, margins, leverage, and balance sheet composition.</p>
</Link>
<Link className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4 transition hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel-bright)]" href="/filings">
<Link
className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4 transition hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel-bright)]"
href="/filings"
onMouseEnter={() => {
void queryClient.prefetchQuery(filingsQueryOptions({ limit: 120 }));
}}
onFocus={() => {
void queryClient.prefetchQuery(filingsQueryOptions({ limit: 120 }));
}}
>
<p className="panel-heading text-xs uppercase text-[color:var(--terminal-muted)]">Filings</p>
<p className="mt-2 text-sm text-[color:var(--terminal-bright)]">Sync SEC filings and trigger AI memo analysis.</p>
</Link>
<Link className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4 transition hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel-bright)]" href="/portfolio">
<Link
className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4 transition hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel-bright)]"
href="/portfolio"
onMouseEnter={() => prefetchPortfolioSurfaces()}
onFocus={() => prefetchPortfolioSurfaces()}
>
<p className="panel-heading text-xs uppercase text-[color:var(--terminal-muted)]">Portfolio</p>
<p className="mt-2 text-sm text-[color:var(--terminal-bright)]">Manage holdings and mark to market in real time.</p>
</Link>
<Link className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4 transition hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel-bright)]" href="/watchlist">
<Link
className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4 transition hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel-bright)]"
href="/watchlist"
onMouseEnter={() => prefetchPortfolioSurfaces()}
onFocus={() => prefetchPortfolioSurfaces()}
>
<p className="panel-heading text-xs uppercase text-[color:var(--terminal-muted)]">Watchlist</p>
<p className="mt-2 text-sm text-[color:var(--terminal-bright)]">Track priority tickers for monitoring and ingestion.</p>
</Link>