Skip to content

Commit 8aa4603

Browse files
feat: serve Design Notes from manifest — no iframe rebuild required
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f106bbc commit 8aa4603

3 files changed

Lines changed: 120 additions & 44 deletions

File tree

hub/src/App.tsx

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,23 +1190,6 @@ function OsacEmbedFullscreenPage() {
11901190
/** Allowlist: registry ids are kebab-case; some use dots (e.g. fleet-admin-rbac-v1.1). */
11911191
const HPUX_PROTOTYPE_ID_RE = /^[a-z][a-z0-9._-]{0,79}$/i;
11921192

1193-
/**
1194-
* Look up design doc and recording URLs from the manifest for a given hpux prototype id.
1195-
* Matches by checking whether the entry's `prototypeUrl` contains `prototype=<id>`.
1196-
* Returns undefined for each field when the entry has no value so callers can safely coalesce.
1197-
*/
1198-
function lookupManifestLinks(prototypeId: string): { designDocUrl?: string | null; recordingUrl?: string | null } {
1199-
if (!prototypeId) return {};
1200-
const param = `prototype=${prototypeId}`;
1201-
const entry = manifest.prototypes.find(
1202-
(p) => typeof p.prototypeUrl === "string" && p.prototypeUrl.includes(param),
1203-
);
1204-
if (!entry) return {};
1205-
return {
1206-
designDocUrl: entry.designDocUrl,
1207-
recordingUrl: entry.prototypeRecordingUrl,
1208-
};
1209-
}
12101193

12111194
/**
12121195
* Prototypes that share one hub embed with a version dropdown (see {@link HpuxPrototypesEmbedFullscreenPage}).
@@ -1362,8 +1345,9 @@ function HpuxPrototypesEmbedFullscreenPage() {
13621345
const [searchParams, setSearchParams] = useSearchParams();
13631346
useEmbedFullscreenChrome();
13641347

1365-
const [designNotes, setDesignNotes] = useState<HpuxDesignNotesData | null>(null);
1366-
const [designNotesPrototypeName, setDesignNotesPrototypeName] = useState<string>("");
1348+
// iframeDesignNotes: set by postMessage from the hpux-prototypes iframe after it loads.
1349+
const [iframeDesignNotes, setIframeDesignNotes] = useState<HpuxDesignNotesData | null>(null);
1350+
const [iframePrototypeName, setIframePrototypeName] = useState<string>("");
13671351
const [isDesignNotesOpen, setIsDesignNotesOpen] = useState(false);
13681352
const [prototypeStatus, setPrototypeStatus] = useState<string | undefined>(undefined);
13691353

@@ -1372,6 +1356,45 @@ function HpuxPrototypesEmbedFullscreenPage() {
13721356
const prototype = searchParams.get("prototype")?.trim() ?? "";
13731357
const valid = prototype.length > 0 && HPUX_PROTOTYPE_ID_RE.test(prototype);
13741358

1359+
// Look up the manifest entry for the active prototype. This is the primary data source for
1360+
// design notes — available immediately on page load, no iframe postMessage required.
1361+
const manifestEntry = useMemo(() => {
1362+
if (!prototype) return undefined;
1363+
const param = `prototype=${prototype}`;
1364+
return manifest.prototypes.find(
1365+
(p) => typeof p.prototypeUrl === "string" && p.prototypeUrl.includes(param),
1366+
);
1367+
}, [prototype]);
1368+
1369+
// Derive design notes from the manifest entry (available before the iframe loads).
1370+
const manifestDesignNotes = useMemo((): HpuxDesignNotesData | null => {
1371+
if (!manifestEntry?.designNotes) return null;
1372+
return {
1373+
designerNotes: manifestEntry.designNotes.designerNotes,
1374+
navigationGuide: manifestEntry.designNotes.navigationGuide,
1375+
personaName: manifestEntry.persona ?? undefined,
1376+
designDocUrl: manifestEntry.designDocUrl ?? undefined,
1377+
recordingUrl: manifestEntry.prototypeRecordingUrl ?? undefined,
1378+
ownerName: manifestEntry.designer ?? manifestEntry.author,
1379+
};
1380+
}, [manifestEntry]);
1381+
1382+
// Merge manifest (base) with iframe postMessage data (override). Manifest fills gaps immediately;
1383+
// the iframe response overwrites individual fields when it arrives.
1384+
const effectiveDesignNotes = useMemo((): HpuxDesignNotesData | null => {
1385+
if (!manifestDesignNotes && !iframeDesignNotes) return null;
1386+
return {
1387+
designerNotes: iframeDesignNotes?.designerNotes || manifestDesignNotes?.designerNotes || "",
1388+
navigationGuide: iframeDesignNotes?.navigationGuide ?? manifestDesignNotes?.navigationGuide,
1389+
ownerName: iframeDesignNotes?.ownerName ?? manifestDesignNotes?.ownerName,
1390+
ownerSlack: iframeDesignNotes?.ownerSlack ?? manifestDesignNotes?.ownerSlack,
1391+
personaName: iframeDesignNotes?.personaName ?? manifestDesignNotes?.personaName,
1392+
jiraUrl: iframeDesignNotes?.jiraUrl ?? manifestDesignNotes?.jiraUrl,
1393+
recordingUrl: iframeDesignNotes?.recordingUrl ?? manifestDesignNotes?.recordingUrl,
1394+
designDocUrl: iframeDesignNotes?.designDocUrl ?? manifestDesignNotes?.designDocUrl,
1395+
};
1396+
}, [manifestDesignNotes, iframeDesignNotes]);
1397+
13751398
// Listen for design notes data posted from the hpux-prototypes iframe on load.
13761399
useEffect(() => {
13771400
const handler = (event: MessageEvent) => {
@@ -1381,7 +1404,7 @@ function HpuxPrototypesEmbedFullscreenPage() {
13811404
event.data.type === "hpux-prototype-loaded"
13821405
) {
13831406
const dn = event.data.designNotes as HpuxDesignNotesData | null;
1384-
setDesignNotes(
1407+
setIframeDesignNotes(
13851408
dn
13861409
? {
13871410
...dn,
@@ -1394,7 +1417,7 @@ function HpuxPrototypesEmbedFullscreenPage() {
13941417
}
13951418
: null,
13961419
);
1397-
setDesignNotesPrototypeName(typeof event.data.prototypeName === "string" ? event.data.prototypeName : "");
1420+
setIframePrototypeName(typeof event.data.prototypeName === "string" ? event.data.prototypeName : "");
13981421
setPrototypeStatus(typeof event.data.status === "string" ? event.data.status : undefined);
13991422
setIsDesignNotesOpen(false);
14001423
}
@@ -1413,9 +1436,9 @@ function HpuxPrototypesEmbedFullscreenPage() {
14131436
return () => clearTimeout(t);
14141437
}, [prototype, valid]);
14151438

1416-
// Reset design notes and status when the prototype param changes so stale data never shows.
1439+
// Reset iframe-sourced design notes and status when the prototype param changes.
14171440
useEffect(() => {
1418-
setDesignNotes(null);
1441+
setIframeDesignNotes(null);
14191442
setIsDesignNotesOpen(false);
14201443
setPrototypeStatus(undefined);
14211444
}, [prototype]);
@@ -1424,7 +1447,7 @@ function HpuxPrototypesEmbedFullscreenPage() {
14241447
return <Navigate to="/" replace />;
14251448
}
14261449

1427-
const manifestLinks = useMemo(() => lookupManifestLinks(prototype), [prototype]);
1450+
const effectivePrototypeName = iframePrototypeName || manifestEntry?.title || "";
14281451

14291452
const { backTo, backLabel, versionOptions } = resolveHpuxEmbedVersionContext(prototype);
14301453
const hubBase = import.meta.env.BASE_URL;
@@ -1435,7 +1458,7 @@ function HpuxPrototypesEmbedFullscreenPage() {
14351458
const src = `${hubBase}hpux-prototypes/?${iframeQs.toString()}`;
14361459
const label = `Shared HPUX Prototypes: ${prototype}`;
14371460

1438-
const designNotesButton = designNotes ? (
1461+
const designNotesButton = effectiveDesignNotes ? (
14391462
<Button
14401463
variant="plain"
14411464
size="sm"
@@ -1448,13 +1471,13 @@ function HpuxPrototypesEmbedFullscreenPage() {
14481471
</Button>
14491472
) : null;
14501473

1451-
const designNotesPanelContent = designNotes ? (
1474+
const designNotesPanelContent = effectiveDesignNotes ? (
14521475
<DrawerPanelContent defaultSize="420px" minSize="350px">
14531476
<DrawerHead>
14541477
<div>
14551478
<Title headingLevel="h2" size="xl">Design Notes</Title>
14561479
<Content component="small" style={{ color: "var(--pf-t--global--text--color--subtle)" }}>
1457-
{designNotesPrototypeName}
1480+
{effectivePrototypeName}
14581481
</Content>
14591482
</div>
14601483
<DrawerActions>
@@ -1464,8 +1487,8 @@ function HpuxPrototypesEmbedFullscreenPage() {
14641487
<DrawerPanelBody>
14651488
{/* Resources row — compact, at top */}
14661489
{(() => {
1467-
const effectiveDesignDocUrl = designNotes.designDocUrl ?? manifestLinks.designDocUrl;
1468-
const effectiveRecordingUrl = designNotes.recordingUrl ?? manifestLinks.recordingUrl;
1490+
const effectiveDesignDocUrl = effectiveDesignNotes.designDocUrl;
1491+
const effectiveRecordingUrl = effectiveDesignNotes.recordingUrl;
14691492
const linkStyle: React.CSSProperties = { paddingLeft: 0, paddingRight: 0, fontSize: "var(--pf-t--global--font--size--sm)" };
14701493
const notLinkedStyle: React.CSSProperties = { color: "var(--pf-t--global--text--color--subtle)", display: "inline-flex", alignItems: "center", gap: "4px", fontSize: "var(--pf-t--global--font--size--sm)" };
14711494
return (
@@ -1480,45 +1503,45 @@ function HpuxPrototypesEmbedFullscreenPage() {
14801503
) : (
14811504
<span style={notLinkedStyle}><VideoIcon aria-hidden style={{ color: "#c9190b", opacity: 0.5 }} /> Recording — Not linked</span>
14821505
)}
1483-
{designNotes.jiraUrl && (
1484-
<Button variant="link" isInline icon={<ExternalLinkAltIcon aria-hidden />} iconPosition="end" component="a" href={designNotes.jiraUrl} target="_blank" rel="noopener noreferrer" style={linkStyle}>Jira</Button>
1506+
{effectiveDesignNotes.jiraUrl && (
1507+
<Button variant="link" isInline icon={<ExternalLinkAltIcon aria-hidden />} iconPosition="end" component="a" href={effectiveDesignNotes.jiraUrl} target="_blank" rel="noopener noreferrer" style={linkStyle}>Jira</Button>
14851508
)}
14861509
</div>
14871510
);
14881511
})()}
14891512

14901513
{/* Persona + Designer — single compact row */}
1491-
{(designNotes.personaName || designNotes.ownerName) && (
1514+
{(effectiveDesignNotes.personaName || effectiveDesignNotes.ownerName) && (
14921515
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--pf-t--global--spacer--sm)", alignItems: "center", marginBottom: "var(--pf-t--global--spacer--lg)" }}>
1493-
{designNotes.personaName && (
1494-
<Label isCompact color="purple">{designNotes.personaName}</Label>
1516+
{effectiveDesignNotes.personaName && (
1517+
<Label isCompact color="purple">{effectiveDesignNotes.personaName}</Label>
14951518
)}
1496-
{designNotes.ownerName && (
1519+
{effectiveDesignNotes.ownerName && (
14971520
<Content component="small" style={{ color: "var(--pf-t--global--text--color--subtle)" }}>
1498-
{designNotes.ownerName}{designNotes.ownerSlack ? ` — @${designNotes.ownerSlack}` : ""}
1521+
{effectiveDesignNotes.ownerName}{effectiveDesignNotes.ownerSlack ? ` — @${effectiveDesignNotes.ownerSlack}` : ""}
14991522
</Content>
15001523
)}
15011524
</div>
15021525
)}
15031526

1504-
{designNotes.designerNotes && (
1527+
{effectiveDesignNotes.designerNotes && (
15051528
<div style={{ marginBottom: "var(--pf-t--global--spacer--md)" }}>
15061529
<Title headingLevel="h3" size="md" style={{ marginBottom: "var(--pf-t--global--spacer--sm)" }}>
15071530
Designer Notes
15081531
</Title>
15091532
<Content component="p">
1510-
{designNotes.designerNotes}
1533+
{effectiveDesignNotes.designerNotes}
15111534
</Content>
15121535
</div>
15131536
)}
15141537

1515-
{designNotes.navigationGuide && designNotes.navigationGuide.length > 0 && (
1538+
{effectiveDesignNotes.navigationGuide && effectiveDesignNotes.navigationGuide.length > 0 && (
15161539
<div style={{ marginBottom: "var(--pf-t--global--spacer--md)" }}>
15171540
<Title headingLevel="h3" size="md" style={{ marginBottom: "var(--pf-t--global--spacer--sm)" }}>
15181541
Where to navigate
15191542
</Title>
15201543
<ol style={{ listStyle: "none", margin: 0, padding: 0 }}>
1521-
{designNotes.navigationGuide.map((entry, index) => (
1544+
{effectiveDesignNotes.navigationGuide.map((entry, index) => (
15221545
<li key={index} style={{ marginBottom: "var(--pf-t--global--spacer--md)" }}>
15231546
<div style={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: "var(--pf-t--global--spacer--sm)", marginBottom: entry.notes ? "var(--pf-t--global--spacer--xs)" : 0 }}>
15241547
<span style={{ minWidth: "1.25rem", fontWeight: 700, color: "var(--pf-t--global--text--color--subtle)" }}>
@@ -1534,7 +1557,7 @@ function HpuxPrototypesEmbedFullscreenPage() {
15341557
{entry.notes}
15351558
</Content>
15361559
)}
1537-
{index < designNotes.navigationGuide!.length - 1 && (
1560+
{index < effectiveDesignNotes.navigationGuide!.length - 1 && (
15381561
<Divider style={{ marginTop: "var(--pf-t--global--spacer--sm)" }} />
15391562
)}
15401563
</li>
@@ -1559,7 +1582,7 @@ function HpuxPrototypesEmbedFullscreenPage() {
15591582
extraActions={designNotesButton}
15601583
/>
15611584
<Drawer
1562-
isExpanded={isDesignNotesOpen && designNotes !== null}
1585+
isExpanded={isDesignNotesOpen && effectiveDesignNotes !== null}
15631586
position="end"
15641587
className="ops-hub-design-notes-drawer"
15651588
>

hub/src/data/prototypes.manifest.json

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,27 @@
367367
"prototypeUrl": "/embed/hpux-prototypes?prototype=observability-agentic-troubleshooting-ai",
368368
"jiraUrl": "https://redhat.atlassian.net/browse/HPUX-1534",
369369
"jiraIssueStatus": "In Progress",
370-
"jiraIssueRelease": null
370+
"jiraIssueRelease": null,
371+
"designNotes": {
372+
"designerNotes": "Summit demo prototype for AI-assisted troubleshooting in the Observability stack. The agent guides an SRE through a structured investigation flow — gathering evidence, correlating signals, and surfacing recommended next steps — without requiring deep Prometheus/OCP expertise.",
373+
"navigationGuide": [
374+
{
375+
"page": "AI Hub entry point",
376+
"path": "/core/observe/ai-hub",
377+
"notes": "Landing page for the agentic session. Look for the \"start new investigation\" CTA and the recent sessions list — confirm with Foday whether session history is in scope for Summit."
378+
},
379+
{
380+
"page": "Active Investigation",
381+
"path": "/core/observe/ai-hub/investigation",
382+
"notes": "Chat-style interface with step cards. Each agent action (query metrics, check events, correlate) surfaces as a collapsible card so the SRE can follow the reasoning chain."
383+
},
384+
{
385+
"page": "Evidence Panel",
386+
"path": "/core/observe/ai-hub/investigation/evidence",
387+
"notes": "Side panel showing raw signal data the agent gathered. Review whether this should be a persistent panel or an on-demand drawer."
388+
}
389+
]
390+
}
371391
},
372392
{
373393
"teamId": "observability",
@@ -380,7 +400,22 @@
380400
"prototypeUrl": "/embed/hpux-prototypes?prototype=shiri-alerting-ui-v2",
381401
"jiraUrl": "https://redhat.atlassian.net/browse/HPUX-1444",
382402
"jiraIssueStatus": "Closed",
383-
"jiraIssueRelease": null
403+
"jiraIssueRelease": null,
404+
"designNotes": {
405+
"designerNotes": "v2 of the Multi-cluster Alerting UI. Navigation moves from a dedicated Alerting page to a filtering model: clicking a cluster in the heatmap filters the Firing alerts tab in place, keeping the SRE in one context rather than bouncing between views.",
406+
"navigationGuide": [
407+
{
408+
"page": "Clusters Health",
409+
"path": "/fleet-management/alerting",
410+
"notes": "Heatmap entry point. Each cluster cell is clickable and sets the active filter for the Firing Alerts tab. Severity bands and cluster status badges are the primary scannable signal."
411+
},
412+
{
413+
"page": "Firing Alerts",
414+
"path": "/fleet-management/alerting/firing",
415+
"notes": "Full alert table filtered by the selected cluster. Review the column set, sort order, and bulk-action affordances — confirm with Shiri before finalising."
416+
}
417+
]
418+
}
384419
},
385420
{
386421
"teamId": "rbac",

hub/src/manifest.types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ export interface ManifestPrototypeEntry {
4848
designDocUrl?: string | null;
4949
/** Walkthrough or demo recording (Loom, Drive, etc.). Opens in a new tab from the card when set. */
5050
prototypeRecordingUrl?: string | null;
51+
/**
52+
* Optional design notes for hpux-prototypes embeds. When present, the hub renders the Design Notes
53+
* panel immediately on page load without waiting for a postMessage from the iframe.
54+
* Mirrors the `designNotes` shape in `hpux-prototypes/src/app/core/types.ts`.
55+
*/
56+
designNotes?: {
57+
/** Free-form notes from the designer about design decisions, intent, and open questions. */
58+
designerNotes: string;
59+
/** Ordered list of pages/screens the reviewer should navigate through. */
60+
navigationGuide?: Array<{
61+
/** Page name, e.g. "Alert List" */
62+
page: string;
63+
/** Route path to navigate to, e.g. /observe/alerting */
64+
path: string;
65+
/** What to look at or focus on when on this page. */
66+
notes?: string;
67+
}>;
68+
};
5169
/**
5270
* When true, hide from hub listings and search. Use for embed-only cards without an hpux-prototypes config,
5371
* or alongside `private: true` in hpux `prototype.config.ts` (hub also reads generated private-id list).

0 commit comments

Comments
 (0)