Skip to content

feat(calm-hub-ui): real thumbnails for architecture/pattern cards, type icons for non-visual resources #2806

Description

@rocketstack-matt

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:

  • Design reviewed and approved
  • Implementation completed
  • Tests written and passing
  • Documentation updated
  • Relevant workflows updated (if needed)
  • Performance impact assessed

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).

Metadata

Metadata

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions