Feature Proposal
Target Project:
calm-hub-ui, with supporting changes to calm-hub and calm-server.
Description of Feature:
Replace the placeholder card backgrounds used across CALM Hub's browse/explore surfaces with content that actually represents each resource:
- Visual assets (architectures, patterns): a real thumbnail image of the resource's diagram, rendered the same way it looks in the Hub's own visualizer, instead of today's striped gradient placeholder.
- Non-visual assets (flows, standards, ADRs, interfaces): a distinguishing type icon on the card, matching the pattern Controls already use, plus their description shown where it isn't already (flows/standards/interfaces already show one; ADRs currently have none — closing that gap is left as a follow-up to avoid a backend schema change here).
User Stories:
- As a CALM Hub user browsing a namespace, I want to see a recognizable thumbnail of an architecture or pattern's diagram on its card, so I can identify it without opening it.
- As a CALM Hub user browsing flows, standards, ADRs or interfaces, I want a distinguishing type icon on the card (as Controls already have), so I can tell resource types apart at a glance in a mixed grid or search-result list.
Current Limitations:
- Every architecture/pattern card shows the same per-type-colored striped
repeating-linear-gradient background (calm-hub-ui/src/hub/components/namespace-page/ItemCard.tsx) — there is no thumbnail/image field anywhere in the data model (frontend or backend), so no per-item visual signal exists today.
- Only Controls render a type icon (
IoShieldCheckmarkOutline, hardcoded in calm-hub-ui/src/hub/components/domain-page/ControlCard.tsx) via ItemCard's existing thumbnailIcon slot. Flows, standards, ADRs and interfaces render no icon.
- Flows, standards and interfaces already show a description on their cards; ADRs do not, because neither the frontend
AdrSummary nor the backend NamespaceAdrSummary carry a description field at all today (the full ADR document does have contextAndProblemStatement, but it's never surfaced through the summary endpoint the browse grid uses).
Proposed Implementation:
Phase 1 — Type icons (frontend only, no schema changes)
- Extend
calm-hub-ui/src/hub/components/namespace-page/resource-type-meta.ts (RESOURCE_TYPE_META) with an icon field per resource type, and migrate Controls' hardcoded icon into that same registry so every type is looked up the same way.
- Proposed icons (react-icons
io5 outline set, matching Controls' existing icon): Flow → IoGitBranchOutline, Standard → IoDocumentTextOutline, ADR → IoGitCommitOutline, Interface → IoLinkOutline. Architecture/pattern get no icon (they get real thumbnails in Phase 2 instead).
- Icon color pulled from the existing
colors.resourceTypes.<type>.accentText token, same as Controls.
- Applied to the
NamespacePage/DomainPage grid cards, and to the search dropdown rows (GlobalSearchBar, ExplorerSearch, IntroSearchBar) — there, the icon sits on each type-group header rather than every row, to avoid clutter in a compact list.
Phase 2 — Real thumbnails for architectures/patterns
- Render route (
calm-hub-ui): a new route (e.g. /render/:type/:namespace/:id/:version) that mounts the existing ReactFlow visualizer non-interactively (no chrome, no interaction handlers, auto-fit-to-bounds) and signals when layout has settled. Reuses the actual visualizer so the thumbnail is pixel-faithful to what a user sees when they open the resource — a Mermaid/docify-based render was considered and rejected because it produces a visually different layout (see Alternatives).
- Render endpoint (
calm-server): a new internal endpoint (e.g. POST /render/thumbnail) that drives Playwright to navigate to that route, wait for the ready signal (bounded timeout), screenshot the diagram container, and return PNG bytes.
- Trigger — eager on write: on a successful architecture/pattern version write,
calm-hub asynchronously calls calm-server's render endpoint (fire-and-forget; a render failure never blocks or fails the write) and stores the resulting bytes on that version's record on success.
- Trigger — on-demand fallback:
GET /calm/namespaces/{namespace}/{type}/{id}/versions/{version}/thumbnail serves the stored bytes, or 404 if none exist. On a miss, it synchronously attempts the same render (bounded timeout), stores and returns the result if it succeeds, and only 404s if that also fails. A small in-memory single-flight guard (keyed by namespace/type/id/version) collapses concurrent requests for the same missing thumbnail into one render. This makes write-time failures self-healing: the first browse after a failed write-time render pays a one-time render cost, then it's cached for everyone after.
- Storage: thumbnail bytes stored as a binary field directly on the version's document in whichever store is active (Nitrite or MongoDB) — no new storage subsystem, versioned for free alongside the document. Not embedded in list/summary responses (kept out of the browse-grid list payload); served only via the dedicated
GET .../thumbnail endpoint.
- Frontend wiring:
ItemCard gets a thumbnailUrl?: string prop; when set, renders <img src={thumbnailUrl} onError={...}> in place of the stripe, falling back to today's striped placeholder on any load error (covers both "never generated" and transient failures).
API changes: new GET .../versions/{version}/thumbnail endpoint on calm-hub; new internal POST /render/thumbnail endpoint on calm-server.
Data model changes: thumbnail: byte[] + thumbnailGeneratedAt added to the stored version document (both Nitrite and MongoDB backends). No changes to list/summary response payloads.
Dependencies: calm-server gains a Playwright/headless-Chromium dependency for the render endpoint (an existing pattern in the monorepo — shared/src/docify/diagram-rendering/mermaid-browser-renderer.ts already uses playwright-core for a different rendering path).
Alternatives Considered:
- Reuse the existing calm-widgets → Mermaid → Playwright docify pipeline for thumbnails — rejected because it produces a different diagram layout than the Hub's own ReactFlow visualizer, so the "thumbnail" wouldn't match what users actually see when they open the resource.
- Lightweight client-side thumbnail (new static SVG layout rendered from already-fetched node/relationship data, no headless browser) — rejected as the primary mechanism: it's new layout code to build and wouldn't match the real visualizer's layout either.
- Client-side capture on view/save (ReactFlow's built-in export, uploaded from the user's browser) — rejected as the primary trigger because writes coming from the CLI/API/CI have no browser involved, so eager-on-write coverage would be incomplete; the on-demand fallback in this proposal achieves a similar self-healing effect without that gap.
- Manually uploaded thumbnail image — rejected as the primary mechanism: not automatic, most items would stay on the placeholder indefinitely.
- Dedicated new render sidecar service (instead of an endpoint on
calm-server) — rejected for v1: isolates Playwright's resource footprint, but is a new service to build, deploy and operate versus reusing an existing one.
- Thumbnail on disk/object storage with a reference on the document — rejected in favor of storing bytes directly on the document: needs a storage location that works the same way across both standalone/Nitrite and MongoDB deployments, whereas storing on the document is simpler and versioned for free.
- Blocking the write if thumbnail generation fails — rejected: would introduce a new failure mode for document writes that work reliably today.
- Closing the ADR description gap in this proposal — descoped to avoid a backend schema change (
NamespaceAdrSummary) bundled into a UI-focused change; left as a natural follow-up.
Testing Strategy:
- Phase 1: component tests extending existing
ItemCard/TypeBadge/resource-type-meta coverage — no new infrastructure to mock.
- Phase 2:
calm-server render endpoint unit-tested with Playwright mocked; calm-hub tests cover the async write-time trigger, graceful failure (calm-server unavailable → write still succeeds), the on-demand fallback and single-flight dedup, and both stores round-tripping the binary field; calm-hub-ui tests cover the new render route and ItemCard's image/fallback behavior. Manual/E2E: upload an architecture locally, confirm a thumbnail appears; kill calm-server and confirm writes still succeed with the placeholder shown, then confirm browsing that card triggers the on-demand fallback once calm-server is back.
Documentation Requirements:
- Update
calm-hub-ui/AGENTS.md (new render route, icon registry pattern), calm-hub/AGENTS.md (new thumbnail endpoint and storage field), and calm-server's docs (new internal render endpoint) to reflect the new components.
Implementation Checklist:
Additional Context:
This proposal is deliberately split into two phases so Phase 1 (icons) can ship quickly as a low-risk, frontend-only change, while Phase 2 (thumbnails) — the larger, riskier piece touching three services — is scoped, built and reviewed on its own. Existing reference points: calm-hub-ui/src/hub/components/namespace-page/ItemCard.tsx (shared card component), resource-type-meta.ts (per-type registry), ControlCard.tsx (existing icon pattern to generalize).
Feature Proposal
Target Project:
calm-hub-ui, with supporting changes tocalm-hubandcalm-server.Description of Feature:
Replace the placeholder card backgrounds used across CALM Hub's browse/explore surfaces with content that actually represents each resource:
User Stories:
Current Limitations:
repeating-linear-gradientbackground (calm-hub-ui/src/hub/components/namespace-page/ItemCard.tsx) — there is no thumbnail/image field anywhere in the data model (frontend or backend), so no per-item visual signal exists today.IoShieldCheckmarkOutline, hardcoded incalm-hub-ui/src/hub/components/domain-page/ControlCard.tsx) viaItemCard's existingthumbnailIconslot. Flows, standards, ADRs and interfaces render no icon.AdrSummarynor the backendNamespaceAdrSummarycarry a description field at all today (the full ADR document does havecontextAndProblemStatement, but it's never surfaced through the summary endpoint the browse grid uses).Proposed Implementation:
Phase 1 — Type icons (frontend only, no schema changes)
calm-hub-ui/src/hub/components/namespace-page/resource-type-meta.ts(RESOURCE_TYPE_META) with aniconfield per resource type, and migrate Controls' hardcoded icon into that same registry so every type is looked up the same way.io5outline set, matching Controls' existing icon): Flow →IoGitBranchOutline, Standard →IoDocumentTextOutline, ADR →IoGitCommitOutline, Interface →IoLinkOutline. Architecture/pattern get no icon (they get real thumbnails in Phase 2 instead).colors.resourceTypes.<type>.accentTexttoken, same as Controls.NamespacePage/DomainPagegrid cards, and to the search dropdown rows (GlobalSearchBar,ExplorerSearch,IntroSearchBar) — there, the icon sits on each type-group header rather than every row, to avoid clutter in a compact list.Phase 2 — Real thumbnails for architectures/patterns
calm-hub-ui): a new route (e.g./render/:type/:namespace/:id/:version) that mounts the existing ReactFlow visualizer non-interactively (no chrome, no interaction handlers, auto-fit-to-bounds) and signals when layout has settled. Reuses the actual visualizer so the thumbnail is pixel-faithful to what a user sees when they open the resource — a Mermaid/docify-based render was considered and rejected because it produces a visually different layout (see Alternatives).calm-server): a new internal endpoint (e.g.POST /render/thumbnail) that drives Playwright to navigate to that route, wait for the ready signal (bounded timeout), screenshot the diagram container, and return PNG bytes.calm-hubasynchronously calls calm-server's render endpoint (fire-and-forget; a render failure never blocks or fails the write) and stores the resulting bytes on that version's record on success.GET /calm/namespaces/{namespace}/{type}/{id}/versions/{version}/thumbnailserves the stored bytes, or404if none exist. On a miss, it synchronously attempts the same render (bounded timeout), stores and returns the result if it succeeds, and only 404s if that also fails. A small in-memory single-flight guard (keyed by namespace/type/id/version) collapses concurrent requests for the same missing thumbnail into one render. This makes write-time failures self-healing: the first browse after a failed write-time render pays a one-time render cost, then it's cached for everyone after.GET .../thumbnailendpoint.ItemCardgets athumbnailUrl?: stringprop; when set, renders<img src={thumbnailUrl} onError={...}>in place of the stripe, falling back to today's striped placeholder on any load error (covers both "never generated" and transient failures).API changes: new
GET .../versions/{version}/thumbnailendpoint oncalm-hub; new internalPOST /render/thumbnailendpoint oncalm-server.Data model changes:
thumbnail: byte[]+thumbnailGeneratedAtadded to the stored version document (both Nitrite and MongoDB backends). No changes to list/summary response payloads.Dependencies:
calm-servergains a Playwright/headless-Chromium dependency for the render endpoint (an existing pattern in the monorepo —shared/src/docify/diagram-rendering/mermaid-browser-renderer.tsalready usesplaywright-corefor a different rendering path).Alternatives Considered:
calm-server) — rejected for v1: isolates Playwright's resource footprint, but is a new service to build, deploy and operate versus reusing an existing one.NamespaceAdrSummary) bundled into a UI-focused change; left as a natural follow-up.Testing Strategy:
ItemCard/TypeBadge/resource-type-metacoverage — no new infrastructure to mock.calm-serverrender endpoint unit-tested with Playwright mocked;calm-hubtests cover the async write-time trigger, graceful failure (calm-server unavailable → write still succeeds), the on-demand fallback and single-flight dedup, and both stores round-tripping the binary field;calm-hub-uitests cover the new render route andItemCard's image/fallback behavior. Manual/E2E: upload an architecture locally, confirm a thumbnail appears; kill calm-server and confirm writes still succeed with the placeholder shown, then confirm browsing that card triggers the on-demand fallback once calm-server is back.Documentation Requirements:
calm-hub-ui/AGENTS.md(new render route, icon registry pattern),calm-hub/AGENTS.md(new thumbnail endpoint and storage field), and calm-server's docs (new internal render endpoint) to reflect the new components.Implementation Checklist:
Additional Context:
This proposal is deliberately split into two phases so Phase 1 (icons) can ship quickly as a low-risk, frontend-only change, while Phase 2 (thumbnails) — the larger, riskier piece touching three services — is scoped, built and reviewed on its own. Existing reference points:
calm-hub-ui/src/hub/components/namespace-page/ItemCard.tsx(shared card component),resource-type-meta.ts(per-type registry),ControlCard.tsx(existing icon pattern to generalize).