The Lookout dashboard is the workspace landing page. It shows a KPI scoreboard (Revenue / ROAS / Running events / Running campaigns / Flagged), the user’s name in the hero, and a list of upcoming events the user can drill into.

What you’ll build

  • A 4-tile KPI strip.
  • A “Flagged” tile that only appears when count > 0.
  • An “Upcoming events” panel — paginated list, click-to-open drawer/detail.

Prerequisites

The call chain

On mount, fire these three in parallel:
#MethodPathPurpose
1GET/events/statisticsTenant-wide KPIs: { total, open, running, closed, revenue, spent, roas, purchases, currency }. Drives Revenue / ROAS / Running events.
2GET/campaigns/statistics{ total, running, open, closed, ignored }. Drives Running campaigns.
3GET/events/flagged/Flagged events. Only the count is rendered.
Then the Upcoming Events panel fires:
#MethodPathPurpose
4GET/events/?page=1&limit=50&until=<+1y>&since=<-3y>&frontend_status=DEFAULT&frontend_status=FLAGGED&exclude_products=falsePaged list for the table.
When the user opens a row:
#MethodPathPurpose
5GET/events/{eid}Event detail.
6GET/events/{eid}/series?limit=1000Sibling events.

Payload — /events/statistics

{
  "total":     412,
  "open":      87,
  "running":   42,
  "closed":    283,
  "revenue":   1825400,
  "spent":     112800,
  "roas":      16.18,
  "purchases": 41205,
  "currency":  "EUR"
}
Any field may be null — render defensively. The reference webapp uses Intl.NumberFormat with currency as the unit.

Payload — /campaigns/statistics

{ "total": 134, "running": 42, "open": 11, "closed": 76, "ignored": 5 }

Reference implementation

import { useEffect, useState } from "react";

export function LookoutDashboard() {
  const [stats, setStats]                 = useState<EventStats | null>(null);
  const [campaignStats, setCampaignStats] = useState<CampaignStats | null>(null);
  const [flaggedCount, setFlaggedCount]   = useState(0);

  useEffect(() => {
    Promise.all([
      fd.get<EventStats>("/events/statistics"),
      fd.get<CampaignStats>("/campaigns/statistics"),
      fd.get<{ total: number }>("/events/flagged/"),
    ]).then(([e, c, f]) => {
      setStats(e); setCampaignStats(c); setFlaggedCount(f.total);
    });
  }, []);

  if (!stats) return <Skeleton />;

  return (
    <div className="kpi-strip">
      <Kpi label="Revenue"          value={fmt(stats.revenue, stats.currency)} />
      <Kpi label="ROAS"             value={stats.roas?.toFixed(2) ?? "—"} />
      <Kpi label="Running events"    value={stats.running} />
      <Kpi label="Running campaigns" value={campaignStats?.running ?? 0} />
      {flaggedCount > 0 && <Kpi label="Flagged" value={flaggedCount} />}
    </div>
  );
}
For the upcoming-events panel, lean on the same fetchEvents you’ll build for the Events list — pass these default filters:
const upcoming = await fetchEvents({
  page:  1,
  limit: 50,
  since: minusYears(3),
  until: plusYears(1),
  frontend_status: ["DEFAULT", "FLAGGED"],   // repeated query param
  exclude_products: false,
});

Gotchas

/events/flagged/ keeps the trailing slash. Without it you’ll get a 404 on some deployments.
On freshly-provisioned partners or partners without any ingested sales, revenue, roas, and currency come back as null. Render .
The reference webapp hides it otherwise. Don’t show an empty tile.
The Dashboard sends false (include products). The Events page sends true (exclude). The Wave list sends nothing. Don’t copy-paste the wrong default.