+
Impressions
-
+
{fmt(c.impression_count)}
- / {fmt(c.budget_imps)}
+
+ {" "}/ {fmt(c.budget_imps)}
+
-
+
Spend
-
+
{fmtDollars(c.spend_dollars)}
- / {fmtDollars(c.budget_dollars)}
+
+ {" "}/ {fmtDollars(c.budget_dollars)}
+
@@ -112,11 +139,13 @@ function CampaignCard({ c }: { c: Campaign }) {
);
}
-// ---------------------------------------------------------------------------
-// Dashboard
-// ---------------------------------------------------------------------------
+interface CampaignDashboardProps {
+ onNavigateToArchitecture?: () => void;
+}
-export default function CampaignDashboard() {
+export default function CampaignDashboard({
+ onNavigateToArchitecture,
+}: CampaignDashboardProps = {}) {
const queryClient = useQueryClient();
const [resetting, setResetting] = useState(false);
@@ -140,7 +169,10 @@ export default function CampaignDashboard() {
return (
{[...Array(5)].map((_, i) => (
-
+
))}
);
@@ -148,7 +180,7 @@ export default function CampaignDashboard() {
if (isError || !campaigns) {
return (
-
+
Failed to load campaign data — is the API reachable?
);
@@ -156,30 +188,64 @@ export default function CampaignDashboard() {
const active = campaigns.filter((c) => c.status !== "STOPPED").length;
const stopped = campaigns.filter((c) => c.status === "STOPPED").length;
-
- // Already sorted by pacing_pct desc from API, but sort here too for safety
const sorted = [...campaigns].sort((a, b) => b.pacing_pct - a.pacing_pct);
return (
- {/* Summary bar */}
+
+
+
+ Imagine you are a campaign manager
+ {" "}
+ who cares about whether your live campaigns are burning budget at
+ the right rate and staying within their contractual frequency caps.
+ This app leverages{" "}
+
+ Lakebase + Spark RTM
+ {" "}
+ to surface pacing decisions within ~2 seconds of an impression
+ being served — fast enough to pause an over-pacing campaign before
+ the money is wasted, instead of finding out hours later.
+
+
+ To learn more about what's going on under the hood, visit the{" "}
+ {onNavigateToArchitecture ? (
+
+ Architecture page
+
+ ) : (
+ Architecture page
+ )}
+ .
+
+
+
- {active} Active
- ·
- {stopped} Stopped
+
+ {active} Active
+
+ ·
+
+ {stopped} Stopped
+
{resetting ? "Resetting..." : "Reset Pacing"}
- Refreshes every 2s
+
+ Refreshes every 2s
+
- {/* Campaign cards */}
{sorted.map((c) => (
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/LeftNav.tsx b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/LeftNav.tsx
new file mode 100644
index 0000000..1aef811
--- /dev/null
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/LeftNav.tsx
@@ -0,0 +1,142 @@
+import React from "react";
+
+export type NavPage = "monitoring" | "architecture" | "settings";
+
+export interface LeftNavProps {
+ currentPage: NavPage;
+ onNavigate: (page: NavPage) => void;
+ expanded: boolean;
+ onExpandedChange: (expanded: boolean) => void;
+}
+
+const ChevronLeft = () => (
+
+
+
+);
+
+const ChevronRight = () => (
+
+
+
+);
+
+const PulseIcon = () => (
+
+
+
+);
+
+const ArchitectureIcon = () => (
+
+
+
+
+
+
+
+);
+
+const GearIcon = () => (
+
+
+
+);
+
+const NAV_ITEMS: { id: NavPage; label: string; icon: React.FC }[] = [
+ { id: "monitoring", label: "Realtime Monitoring", icon: PulseIcon },
+ { id: "architecture", label: "Architecture", icon: ArchitectureIcon },
+];
+
+export const LeftNav: React.FC
= ({
+ currentPage,
+ onNavigate,
+ expanded,
+ onExpandedChange,
+}) => {
+ return (
+ onExpandedChange(true)}
+ onMouseLeave={() => onExpandedChange(false)}
+ >
+ onExpandedChange(!expanded)}
+ role="button"
+ aria-label={expanded ? "Collapse sidebar" : "Expand sidebar"}
+ >
+
+ {expanded ? : }
+
+ {expanded && (
+
+ Menu
+
+ )}
+
+
+
+ {NAV_ITEMS.map(({ id, label, icon: Icon }) => (
+
+ onNavigate(id)}
+ className={`w-full flex items-center gap-3 px-3 py-2.5 text-left text-sm transition-colors ${
+ expanded ? "min-w-0" : "justify-center"
+ } ${
+ currentPage === id
+ ? "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100"
+ : "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
+ }`}
+ >
+
+
+
+ {expanded && {label} }
+
+
+ ))}
+
+
+
+ onNavigate("settings")}
+ className={`w-full flex items-center gap-3 px-3 text-left text-sm transition-colors ${
+ expanded ? "min-w-0" : "justify-center"
+ } ${
+ currentPage === "settings"
+ ? "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100"
+ : "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
+ }`}
+ >
+
+
+
+ {expanded && Settings }
+
+
+
+ );
+};
+
+export default LeftNav;
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/PageHeader.tsx b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/PageHeader.tsx
new file mode 100644
index 0000000..9f9bc22
--- /dev/null
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/PageHeader.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+
+const LOGO_SRC = "/MegaCorp_Logo_-_Transparent.png";
+
+export interface PageHeaderProps {
+ title: string;
+ description?: string;
+ children?: React.ReactNode;
+}
+
+export const PageHeader: React.FC = ({
+ title,
+ description,
+ children,
+}) => {
+ return (
+
+
+
+
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {children != null && {children}
}
+
+ );
+};
+
+export default PageHeader;
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/SettingsPage.tsx b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/SettingsPage.tsx
new file mode 100644
index 0000000..12c36f3
--- /dev/null
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/components/SettingsPage.tsx
@@ -0,0 +1,187 @@
+import React from "react";
+import { PageHeader } from "./PageHeader";
+import type { Theme } from "../lib/theme";
+
+const WORKSPACE_HOST = "https://e2-demo-field-eng.cloud.databricks.com";
+const LAKEBASE_INSTANCE_NAME = "campaign-pacing";
+
+interface ResourceLink {
+ label: string;
+ description: string;
+ href: string;
+ detail?: string;
+}
+
+const LAKEBASE_RESOURCES: ResourceLink[] = [
+ {
+ label: "Lakebase instance",
+ description: `PostgreSQL state store — ${LAKEBASE_INSTANCE_NAME}`,
+ href: `${WORKSPACE_HOST}/compute/database-instances/${LAKEBASE_INSTANCE_NAME}`,
+ detail: "databricks_postgres",
+ },
+ {
+ label: "campaigns table",
+ description: "Per-campaign pacing state (impressions, budget, status)",
+ href: `${WORKSPACE_HOST}/compute/database-instances/${LAKEBASE_INSTANCE_NAME}/databases/databricks_postgres/schemas/public/tables/campaigns`,
+ detail: "databricks_postgres.public.campaigns",
+ },
+ {
+ label: "segment_definitions (Delta / UC)",
+ description: "Source of segment_definition text shown on each card",
+ href: `${WORKSPACE_HOST}/explore/data/media_advertising/segments/megacorp_segment_definitions`,
+ detail: "media_advertising.segments.megacorp_segment_definitions",
+ },
+];
+
+const PIPELINE_RESOURCES: ResourceLink[] = [
+ {
+ label: "kafka_producer_job",
+ description: "Generates synthetic impression events into Kafka",
+ href: `${WORKSPACE_HOST}/jobs?searchTerm=kafka_producer_job`,
+ detail: "Topic: tanner_wendland_adtech_impressions_realtime_v3",
+ },
+ {
+ label: "campaign_pacing_job",
+ description: "Real-Time Mode stream — Kafka → Lakebase via jdbcStreaming",
+ href: `${WORKSPACE_HOST}/jobs?searchTerm=campaign_pacing_job`,
+ detail: "Sink: campaign-pacing (Lakebase)",
+ },
+];
+
+const ExternalLinkIcon = () => (
+
+
+
+);
+
+const SunIcon = () => (
+
+
+
+);
+
+const MoonIcon = () => (
+
+
+
+);
+
+const ResourceRow: React.FC<{ r: ResourceLink }> = ({ r }) => (
+
+
+
+ {r.label}
+
+
+
+
+
+ {r.description}
+
+ {r.detail && (
+
+ {r.detail}
+
+ )}
+
+
+);
+
+interface SettingsPageProps {
+ theme: Theme;
+ onToggleTheme: () => void;
+}
+
+export const SettingsPage: React.FC = ({
+ theme,
+ onToggleTheme,
+}) => {
+ const isDark = theme === "dark";
+
+ return (
+
+
+
+
+
+
+ Appearance
+
+
+ Switch between light and dark themes. Preference is saved locally.
+
+
+
+
+ {isDark ? : }
+
+
+ {isDark ? "Dark mode" : "Light mode"}
+
+
+
+
+
+
+
+
+
+
+ Lakebase & Unity Catalog
+
+
+ Tables read by this app. OAuth tokens to Lakebase are refreshed
+ automatically each hour.
+
+
+ {LAKEBASE_RESOURCES.map((r) => (
+
+ ))}
+
+
+
+
+
+ Pipelines
+
+
+ Jobs that feed the pacing dashboard. Deploy via{" "}
+
+ bash scripts/deploy.sh deploy
+ {" "}
+ from the bundle root.
+
+
+ {PIPELINE_RESOURCES.map((r) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/index.css b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/index.css
index b5c61c9..3168740 100644
--- a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/index.css
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/index.css
@@ -1,3 +1,89 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+* {
+ margin: 0;
+}
+
+body {
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+}
+
+img,
+picture,
+video,
+canvas,
+svg {
+ display: block;
+ max-width: 100%;
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+p,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ overflow-wrap: break-word;
+}
+
+#root {
+ isolation: isolate;
+ height: 100vh;
+}
+
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ min-height: 100vh;
+ background-color: #f5f5f5;
+}
+
+html.dark body {
+ background-color: #030712;
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+}
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/lib/theme.ts b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/lib/theme.ts
new file mode 100644
index 0000000..b1e7cd3
--- /dev/null
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/src/lib/theme.ts
@@ -0,0 +1,35 @@
+import { useCallback, useEffect, useState } from "react";
+
+export type Theme = "light" | "dark";
+
+const STORAGE_KEY = "campaign-pacing-theme";
+
+function readInitialTheme(): Theme {
+ if (typeof window === "undefined") return "light";
+ const stored = window.localStorage.getItem(STORAGE_KEY);
+ if (stored === "light" || stored === "dark") return stored;
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+}
+
+function applyTheme(theme: Theme) {
+ const root = document.documentElement;
+ if (theme === "dark") root.classList.add("dark");
+ else root.classList.remove("dark");
+}
+
+export function useTheme() {
+ const [theme, setThemeState] = useState(readInitialTheme);
+
+ useEffect(() => {
+ applyTheme(theme);
+ window.localStorage.setItem(STORAGE_KEY, theme);
+ }, [theme]);
+
+ const setTheme = useCallback((next: Theme) => setThemeState(next), []);
+ const toggleTheme = useCallback(
+ () => setThemeState((prev) => (prev === "dark" ? "light" : "dark")),
+ [],
+ );
+
+ return { theme, setTheme, toggleTheme };
+}
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/tailwind.config.ts b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/tailwind.config.ts
index 74eab37..0cc87d8 100644
--- a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/tailwind.config.ts
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/frontend/tailwind.config.ts
@@ -1,9 +1,45 @@
import type { Config } from "tailwindcss";
+// Palette sourced from adtech-measurement/docs/styleguide/colors.json
+// (Databricks Extended Brand Guidelines).
export default {
content: ["./index.html", "./src/**/*.{ts,tsx}"],
+ darkMode: "class",
theme: {
- extend: {},
+ extend: {
+ colors: {
+ databricks: {
+ orange: "#FF3621",
+ dark: "#1B3139",
+ lava: {
+ 300: "#FABFBA",
+ 400: "#FF9E94",
+ 500: "#FF5F46",
+ 600: "#FF3621",
+ 700: "#BD2B26",
+ 800: "#801C17",
+ },
+ navy: {
+ 300: "#C4CCD6",
+ 400: "#90A5B1",
+ 500: "#618794",
+ 600: "#1B5162",
+ 700: "#143D4A",
+ 800: "#1B3139",
+ 900: "#0B2026",
+ },
+ oat: {
+ light: "#F9F7F4",
+ medium: "#EEEDE9",
+ },
+ gray: {
+ navigation: "#303F47",
+ text: "#5A6F77",
+ lines: "#DCE0E2",
+ },
+ },
+ },
+ },
},
plugins: [],
} satisfies Config;
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/api/campaigns.py b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/api/campaigns.py
index fcda4c7..2077e41 100644
--- a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/api/campaigns.py
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/api/campaigns.py
@@ -6,6 +6,7 @@
from sqlalchemy.orm import Session
from ..core.db import get_db
+from ..core.definitions import get_definitions
from ..models import CampaignPacing
from ..seed import DEMO_CAMPAIGNS
@@ -21,6 +22,7 @@ class CampaignResponse(BaseModel):
pacing_pct: float
status: Literal["ACTIVE", "PACING_FAST", "STOPPED"]
last_updated: datetime | None
+ segment_definition: str | None = None
def _compute_status(pacing_pct: float) -> Literal["ACTIVE", "PACING_FAST", "STOPPED"]:
@@ -38,6 +40,7 @@ def get_campaigns(db: Session = Depends(get_db)) -> list[CampaignResponse]:
Sorted by pacing_pct descending (most at-risk on top).
"""
rows = db.query(CampaignPacing).all()
+ definitions = get_definitions()
results: list[CampaignResponse] = []
for row in rows:
@@ -64,6 +67,7 @@ def get_campaigns(db: Session = Depends(get_db)) -> list[CampaignResponse]:
pacing_pct=pacing_pct,
status=_compute_status(pacing_pct),
last_updated=last_updated,
+ segment_definition=definitions.get(row.campaign_name),
)
)
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/core/config.py b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/core/config.py
index 7b7a3f1..8e7abc0 100644
--- a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/core/config.py
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/core/config.py
@@ -8,5 +8,10 @@ class Settings(BaseSettings):
PGDATABASE: str = "databricks_postgres"
LAKEBASE_INSTANCE: str = "campaign-pacing"
+ DATABRICKS_WAREHOUSE_ID: str = ""
+ SEGMENT_DEFINITIONS_TABLE: str = (
+ "media_advertising.segments.megacorp_segment_definitions"
+ )
+
settings = Settings()
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/core/definitions.py b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/core/definitions.py
new file mode 100644
index 0000000..fbb4a3f
--- /dev/null
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/core/definitions.py
@@ -0,0 +1,65 @@
+"""
+Fetches campaign segment definitions from the Delta table via SQL warehouse.
+
+The result is cached in-process for SEGMENT_DEFINITIONS_CACHE_SECONDS to avoid
+hitting the warehouse on every dashboard refresh (2s poll cadence).
+"""
+import logging
+import time
+from typing import Dict
+
+from databricks.sdk import WorkspaceClient
+from databricks.sdk.service.sql import StatementState
+
+from .config import settings
+
+logger = logging.getLogger(__name__)
+
+_CACHE: Dict[str, str] = {}
+_CACHE_LOADED_AT: float = 0.0
+_CACHE_TTL_SECONDS = 300
+
+_client = WorkspaceClient()
+
+
+def _fetch_definitions() -> Dict[str, str]:
+ if not settings.DATABRICKS_WAREHOUSE_ID:
+ logger.info("DATABRICKS_WAREHOUSE_ID not set; segment definitions disabled.")
+ return {}
+
+ sql = (
+ f"SELECT segment_name, segment_definition "
+ f"FROM {settings.SEGMENT_DEFINITIONS_TABLE} "
+ f"WHERE segment_definition IS NOT NULL"
+ )
+
+ resp = _client.statement_execution.execute_statement(
+ warehouse_id=settings.DATABRICKS_WAREHOUSE_ID,
+ statement=sql,
+ wait_timeout="30s",
+ )
+
+ if resp.status and resp.status.state != StatementState.SUCCEEDED:
+ logger.warning("Definitions query failed: %s", resp.status)
+ return {}
+
+ out: Dict[str, str] = {}
+ if resp.result and resp.result.data_array:
+ for row in resp.result.data_array:
+ if row and len(row) >= 2 and row[0] and row[1]:
+ out[row[0]] = row[1]
+ return out
+
+
+def get_definitions() -> Dict[str, str]:
+ """Return {segment_name: segment_definition}, cached for 5 minutes."""
+ global _CACHE, _CACHE_LOADED_AT
+ now = time.time()
+ if not _CACHE or (now - _CACHE_LOADED_AT) > _CACHE_TTL_SECONDS:
+ try:
+ _CACHE = _fetch_definitions()
+ _CACHE_LOADED_AT = now
+ logger.info("Loaded %d segment definitions.", len(_CACHE))
+ except Exception:
+ logger.exception("Failed to load segment definitions; keeping stale cache.")
+ return _CACHE
diff --git a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/main.py b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/main.py
index ab483bd..90380cc 100644
--- a/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/main.py
+++ b/adtech_series_sp26/impression_logs_realtime/campaign_pacing_app/src/main.py
@@ -47,7 +47,11 @@ async def lifespan(app: FastAPI):
@app.get("/{full_path:path}", include_in_schema=False, response_model=None)
async def serve_spa(full_path: str):
- """SPA fallback — serve index.html for all non-API routes."""
+ """Serve static files from dist if they exist (favicon, logos, etc.), else SPA fallback."""
+ if full_path:
+ candidate = os.path.normpath(os.path.join(_FRONTEND_DIST, full_path))
+ if candidate.startswith(_FRONTEND_DIST) and os.path.isfile(candidate):
+ return FileResponse(candidate)
index = os.path.join(_FRONTEND_DIST, "index.html")
if os.path.isfile(index):
return FileResponse(index)
diff --git a/adtech_series_sp26/impression_logs_realtime/resources/campaign_pacing_app.yml b/adtech_series_sp26/impression_logs_realtime/resources/campaign_pacing_app.yml
index 02e0179..3c2c7cd 100644
--- a/adtech_series_sp26/impression_logs_realtime/resources/campaign_pacing_app.yml
+++ b/adtech_series_sp26/impression_logs_realtime/resources/campaign_pacing_app.yml
@@ -12,6 +12,11 @@ resources:
database_name: "databricks_postgres"
instance_name: ${resources.database_instances.campaign_pacing_instance.name}
permission: "CAN_CONNECT_AND_CREATE"
+ - name: "campaign-pacing-warehouse"
+ description: "SQL warehouse for joining segment definitions from Delta"
+ sql_warehouse:
+ id: e9b34f7a2e4b0561
+ permission: "CAN_USE"
permissions:
- level: CAN_USE