finos / finos/architecture-as-code

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

Open
#2,806 0 comments 0 reactions 1 assignee Claimed by @rocketstack-matt View on GitHub
calm-hub calm-hub-ui enhancement Roadmap Group: CALM Tools Roadmap Group: Schema
Dominant language
TypeScript
Stars
399
Forks
138
Avg merge
2d 14h
Merged PRs (30d)
37

Description

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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.