Skip to content

frontend: migrate to Vue 3 + Vite + TypeScript + Carbon Web Components, redesign visualizer - #336

Open
anoncam wants to merge 14 commits into
cbomkit:mainfrom
anoncam:main
Open

frontend: migrate to Vue 3 + Vite + TypeScript + Carbon Web Components, redesign visualizer#336
anoncam wants to merge 14 commits into
cbomkit:mainfrom
anoncam:main

Conversation

@anoncam

@anoncam anoncam commented May 15, 2026

Copy link
Copy Markdown

TL;DR

Two related changes bundled into one PR:

  1. Frontend migration. Migrates the CBOMkit frontend from Vue 2.7 + Vue CLI 5 + @carbon/vue@2 + @carbon/charts-vue@1 (all EOL or stale) to Vue 3.5 + Vite 8 + TypeScript 6 (strict) + Pinia 3 + Vue Router 4 + @carbon/[email protected] + @carbon/[email protected] core. Same look-and-feel, redesigned visualizer (responsive grid, KPI strip, click-to-drill-down, new treemap), zero changes required to docker-compose.yaml or the helm chart. 27 Vitest smoke tests. End-to-end verified against the running Quarkus backend.
  2. Distrusted-CA compliance policy. New Rego policy policies.distrusted_ca plus two new OPAResult levels (DISTRUSTED / TRUSTED) so X.509 certificates issued by root-store-removed CAs (DigiNotar, WoSign/StartCom, Symantec, Camerfirma, TrustCor, e-Tugra, Entrust) are flagged at compliance time and surface through the redesigned visualizer's drill-down. Additive — existing quantum_safe evaluations are unchanged.

All commits are DCO-signed.

Demo (also embedded in the updated README):

CBOMkit visualizer

Why this needs to happen

The current frontend stack has multiple EOL or unmaintained dependencies that block any meaningful UI work:

Package Current Status
vue 2.7 End-of-life Dec 2023. No more security or bug fixes.
@carbon/vue 2.45 Vue-2-only. IBM never shipped a Vue 3 build. This is a dead-end.
@carbon/charts-vue 1.11 Wraps the old @carbon/[email protected] API. Current Carbon Charts is on a 1.27.x line with a different surface.
@vue/cli-service 5 Webpack-based, slow, in maintenance mode. The Vue team recommends Vite.
vue-template-compiler 2.6 Vue-2-only.
vue-simple-progress 1.1 Last release 2019. Vue-2-only.
eslint 7 EOL Oct 2022.
eslint-plugin-vue 8 Vue-2-only.

There is no in-place upgrade path. Vue 2's compat build cannot host @carbon/vue@2 under Vue 3 — Carbon hard-coded Vue 2 APIs in its components. The IBM-supported successor for Vue 3 is @carbon/web-components (framework-agnostic custom elements). Adopting that is what unlocks any future UI work.

The distrusted-CA work piggybacks on this PR because (a) it's the same author's stack of branches and (b) the new visualizer's drill-down is what actually makes the DISTRUSTED finding category usable — clicking the new compliance result type filters the asset table to just the affected certificates.

What's in this PR

11 commits on this branch. Squash-friendly if maintainers prefer a single commit on this side.

Frontend migration

98 files changed, +10,473 / −40,684 (most of the deletions are the pre-compiled carbon-both.css bundle which is now provided by @carbon/styles SCSS).

Concern Before After
Framework Vue 2.7 (EOL) Vue 3.5
Language JavaScript TypeScript 6 (strict, verbatimModuleSyntax)
Build Vue CLI 5 (Webpack 5) Vite 8
State reactive() blob in model.js Pinia 3 — split into app / cbom / scan / errors stores
Routing none (state-flag switching) Vue Router 4 with a guard on /results
UI kit @carbon/vue@2 (Vue-2-only) @carbon/[email protected] (Carbon's framework-agnostic line)
Charts @carbon/charts-vue@1 wrapper @carbon/[email protected] core wrapped in a thin Vue 3 component
Styles SCSS w/ pre-compiled carbon-both.css @carbon/[email protected] SCSS, themed via Carbon design tokens
Tests none Vitest 4 + 27 smoke tests over the lib helpers
Lint ESLint 7 + Prettier (none) ESLint 10 flat config + Prettier 3
Icons @carbon/icons-vue@10 @carbon/[email protected] (SVG descriptors rendered via a small wrapper)

All deps Sonatype-vetted; npm install reports 0 vulnerabilities.

Distrusted CA compliance policy

New Rego policy in opa/distrusted_ca.rego under the existing policies package. Matches CycloneDX cryptographic-asset certificates whose issuerName (case-insensitive substring) appears on a curated list of root-store removals from 2011–2024:

CA family Reason root stores removed it
DigiNotar 2011 mis-issuance compromise
WoSign / StartCom 2016 backdating + audit failures
Symantec (incl. Thawte / GeoTrust / RapidSSL / VeriSign-issued legacy) 2017 distrust over mis-issuance
Camerfirma 2021 sustained validation failures
TrustCor 2022 distrust over operational concerns
e-Tugra 2023 security incident
Entrust 2024 distrust over compliance failures

Result vocabulary: distrusted / trusted / unknown. Finding shape matches the existing OPAFinding contract (rule, result, value, bom-ref, property).

OPAResult.java and ComplianceLevel extended additively so the new findings deserialize cleanly and the frontend doesn't fall back to UNKNOWN:

Rank Result Color Icon Compliant?
0 DISTRUSTED red ERROR no
1 QUANTUM_VULNERABLE yellow WARNING no (unchanged)
2 UNKNOWN blue UNKNOWN no (unchanged)
3 QUANTUM_SAFE green SECURE yes (unchanged)
4 NA gray NA yes (unchanged)
5 TRUSTED green CHECKMARK yes

Ships with an upload script (opa/upload_distrusted_ca.sh, mirroring upload_quantum_safe.sh), a sample input fixture (opa/testdata/distrusted_ca_input.json) covering all three outcomes, and an opa/README.md documenting both policies and the local OPA development workflow.

CI

.github/workflows/frontend.yml Node matrix bumped from 21.x → 22.x. Node 21 is past EOL and Vite 8's Rolldown bindings (@rolldown/binding-linux-x64-gnu) fail to resolve on it; 22.x is the next LTS line.

Visualizer redesign — the user-facing reason

The legacy StatisticsView.vue renders four charts in fixed-width columns (20% each) with fixed pixel heights (320 / 230) and toolbar: { enabled: false } everywhere — see the legacy source. The donuts are read-only and there's no summary card.

The new visualizer keeps the same Carbon palette but addresses each of those constraints:

Before After
Layout 4 fixed 20% columns, 320/230 px tall Responsive CSS grid (auto-fit minmax, charts reflow at narrow viewports)
Charts 3 donuts + 1 circle pack + a primitive→algorithm treemap (5 total)
KPI summary none 4-card KPI strip above the charts: asset count, vulnerable count, top primitive, top function
Toolbar enabled: false Carbon toolbar enabled on every chart (zoom, expand, export PNG/CSV)
Drill-down none — charts are read-only Click any slice / leaf / node → filter the asset table below; dismissible filter chip above the table; click the same slice twice to toggle the filter off
Empty / loading states inconsistent (blank donut on empty data) Per-chart explicit messages ("No primitive metadata", "Evaluating compliance…", etc.)
Dark mode class-based, surface-only Carbon g100 chart theme + tokenized surfaces; three-state toggle (auto / light / dark) in the header tracks OS preference

The GIF at the top shows the full flow: load sample → KPI populates (103/27/Pke/Keygen) → click Compliance "Not Quantum Safe" slice → table filters 103 → 27 with a chip → dismiss chip → click largest treemap leaf → table filters 103 → 24 by name.

The new DISTRUSTED / TRUSTED compliance levels render in the same Compliance donut as QUANTUM_VULNERABLE / QUANTUM_SAFE etc., with the severity ranks above driving color and icon — and clicking a DISTRUSTED slice filters the asset table to just the certificates whose issuer is on the distrust list.

Backwards-compatibility — nothing breaks for operators

The deployment surface is identical. No changes are required in docker-compose.yaml, the helm chart, or the runtime env contract:

  • VUE_APP_HTTP_API_BASE, VUE_APP_WS_API_BASE, VUE_APP_TITLE, VUE_APP_VIEWER_ONLY, VUE_APP_POLICY_NAME are all still honored. The new frontend/docker/entrypoint.sh reads them and substitutes the corresponding __CBOMKIT_*__ placeholder tokens that were baked into the Vite bundle at build time.
  • The frontend Dockerfile is now multi-stage and builds inside the image (node:22-alpine build stage → nginxinc/nginx-unprivileged:alpine runtime), so no host Node toolchain is required for make build-frontend-image.
  • make production, make coeus, make dev-frontend are untouched. They continue to pull ghcr.io/cbomkit/cbomkit-frontend:${VERSION} until the maintainers publish a new image from this code.
  • make build-frontend-image now produces a Vue 3 image; the legacy target's behavior is preserved.
  • New convenience target: make dev-frontend-local runs the Vite dev server on :8002 and proxies /api and /v1/scan to the backend on :8081. The dev proxy rewrites the Origin header to match the backend's CORS allowlist (CBOMKIT_FRONTEND_URL_CORS, default http://localhost:8001) so cross-origin scans work locally.
  • The distrusted_ca policy is opt-in — operators must upload it via opa/upload_distrusted_ca.sh (or select it via CBOMKIT_POLICY_NAME / VUE_APP_POLICY_NAME) for it to evaluate. Default behavior is unchanged for existing deployments.

How it was built — side-by-side then swap

To minimize review burden and keep make production working at every checkpoint, the frontend migration was built side-by-side at frontend-next/, then swapped into frontend/ at the end. Each step was a separate sub-PR on the fork:

  1. Migration planfrontend/MIGRATION.md (preserved in this PR's git history at commit 1c8d9273d) locking in the target stack and risk register.
  2. Scaffold — minimal runnable Vue 3 + Vite + TS app proving the toolchain end-to-end. Layout, dark mode, generic <CarbonChart> wrapper, multi-stage Dockerfile.
  3. Services + state + types — typed ports of helpers/* and model.js. Pinia stores. CBOM / scan / compliance / errors / filter TS types. config.ts replacing app.config.js.
  4. Visualizer redesign — the actual goal. Five charts, KPI strip, drill-down dispatch, filter chip, treemap.
  5. Scan flow + notifications + tests + cutover — SearchBar (with branch/subfolder/PAT), ListTable for last CBOMs, NotificationsView surface, LoaderView, SearchOrUploadView container, Vitest suite. Then git mv frontend-next/ → frontend/ with the legacy code removed.
  6. Backend integration fix-ups — four bugs caught when actually scanning a real PURL: WebSocket URL synthesis, dev-proxy Origin rewrite for backend CORS, missing post-scan compliance trigger, router-guard race against the async WebSocket open.
  7. Distrusted-CA policy + compliance enum — new Rego policy and the OPAResult / ComplianceLevel additions to surface the findings through the API and the new visualizer.
  8. CI — Node matrix bumped to 22.x for Vite 8 / Rolldown.

The commit history on this branch tells the story:

53759ab0f fix(ci): bump frontend node matrix from 21.x to 22.x
76682bb4e feat(compliance): recognize distrusted/trusted results in OPAResult
f75fd83c8 feat(opa): add distrusted CA policy
4a3a5664e docs: refresh demo GIF + visualizer write-up for the Vue 3 redesign
9631bebc2 fix(frontend): wire backend integration end-to-end
359cf6710 chore(frontend): cutover frontend-next/ -> frontend/ (PR #4 part 2)
10a54936b feat(frontend-next): scan flow + notifications + tests (PR #4 part 1)
87ec3757b feat(frontend-next): redesigned visualizer (PR #3/4)
707be4af2 feat(frontend-next): port services, state, and types to TS (PR #2/4)
b5d738e10 feat(frontend-next): Vite + Vue 3 + TS scaffold (PR #2/5 of Vue 3 migration)
1c8d9273d docs(frontend): add Vue 3 migration plan

Every commit is DCO-signed. Squash-friendly if you prefer a single merge commit. Or merge as-is and bisect-friendly history is preserved.

Verification

  • npm run type-checkvue-tsc --noEmit passes in strict mode with verbatimModuleSyntax on.
  • npm test — 27/27 Vitest cases pass covering lib/compliance.ts, lib/compliance-local.ts, lib/cbom.ts (CBOM validation, occurrence unwrap, dependency map construction), and lib/info.ts. ~1 s wall-clock.
  • npm run build — succeeds. Production bundle 1.97 MB / 258 kB gzip. (Per-chart-type dynamic imports are a follow-up to shrink further.)
  • make build-frontend-image — produces a working image via the multi-stage Dockerfile.
  • OPA policyopa eval -d opa/distrusted_ca.rego -i opa/testdata/distrusted_ca_input.json 'data.policies.compliance' returns the expected distrusted / trusted / unknown mix for the three sample certificates (DigiNotar, Let's Encrypt, custom issuer).
  • Manual end-to-end against a real Quarkus backend:
    • Scanned pkg:maven/io.quarkus/[email protected] via the new SearchBar.
    • Backend WebSocket → frontend live-streamed LABEL ("Cloning git repository: Receiving objects 25% → 90%, Resolving deltas 45%…" → "Scan finished"), GITURL, BRANCH, REVISION_HASH (commit 44bc4a9), FOLDER (core/runtime) messages — all auto-populated the result-title tags.
    • 4 cryptographic assets detected. KPI strip computed correctly (top primitive Hash, top function Digest). Charts rendered with real data.
    • Loaded the bundled keycloak sample CBOM (103 detections). Clicking the Compliance "Unknown" slice filtered the asset table 103 → 50.
    • Toggled the theme three ways (auto / light / dark) — Carbon chart palette and surrounding surfaces both respond.
    • Drag-drop CBOM upload path also works.

Risks + how they're mitigated

Risk Mitigation
Massive diff size makes review painful Built as sequential sub-PRs on the fork (above). Each is reviewable independently. Side-by-side at frontend-next/ was used so every interim commit left make production functional.
Carbon Web Components don't speak v-model The two inputs the visualizer needs (search bar, file upload) use plain HTML inputs. The remaining cv-* components were either drop-in to cds-* or hand-rolled when the slot composition didn't translate.
Runtime env-var substitution against Vite's hashed filenames Entrypoint script rewritten to glob assets/**/*.{js,html,css} with unique placeholder tokens (__CBOMKIT_HTTP_API_BASE__ etc.) that can't collide with real code. The VUE_APP_* contract on docker-compose.yaml is preserved verbatim.
Bundle size grew 1.97 MB / 258 kB gzip vs the legacy ~600 kB / ~90 kB gzip, mostly from @carbon/charts + d3. Per-chart-type dynamic imports are a follow-up that should claw most of this back. Not a blocker for the visual improvements.
Some legacy components not yet ported Asset-detail modal, dependencies-graph modal, GitHub-embed modal, and post-upload Git info prompt are deferred. The 17 ported components cover the entire scan + visualize + filter flow.
@carbon/icons no longer exports WatsonHealthImageAvailabilityUnavailable24 Substituted with the current unknown icon.
Distrusted-CA list drifts as root stores publish more removals Curated list lives in a single distrusted_cas set at the top of opa/distrusted_ca.rego — updates are a one-line change. Substring (not equality) matching tolerates RDN ordering / quoting variation across CBOM emitters. False positives possible if a legitimate org embeds one of these names in its issuer DN — acceptable trade-off given the threat model.

Conflicts with open Dependabot PRs

Closing or dropping these would be appropriate after merge:

(All other open PRs are Java-side and unaffected.)

What I'd ask of reviewers

  • Read frontend/MIGRATION.md at commit 1c8d9273d (or the squashed equivalent) for the stack-choice rationale.
  • cd frontend && npm install && npm run dev and try the four interactions in the GIF.
  • npm test and npm run type-check — both should be green.
  • Sanity-check that nothing in docker-compose.yaml, the helm chart, or the frontend-image GitHub Actions workflow needs adjustment.
  • Spot-check opa/distrusted_ca.rego against the sample fixture and the curated CA list. The list is intentionally conservative (root-store removals only, not "looks suspicious"); additions should cite a public distrust announcement.
  • Confirm the side-by-side → cutover commit shape is acceptable, or ask for a squash.

Out of scope (intentional follow-ups, not blockers)

  • Asset-detail modal, dependencies-graph modal, GitHub-embed modal, post-upload Git info prompt.
  • Per-chart-type dynamic imports to halve the main bundle.
  • Wiring Vitest into CI alongside the existing Java tests.
  • Publishing a new ghcr.io/cbomkit/cbomkit-frontend:${VERSION} image from this code so make production exercises Vue 3 end-to-end out of the box.
  • Extending the distrusted-CA list to cover intermediate-CA pinning and serial-number revocations (currently issuer-DN substring only).

Test plan

  • cd frontend && npm install && npm run dev — home renders, sample loads, charts + drill-down work, no console errors.
  • npm test — 27 pass.
  • npm run type-check && npm run build — both pass.
  • make build-frontend-image — image builds.
  • Drag each example CBOM (example/{flick,kafka,keycloak}-cbom.json) onto the new UI — charts render, KPI numbers match donut totals.
  • Toggle theme auto / light / dark — chart palette and surfaces both update.
  • Real scan via Git URL or PURL against a running backend — live progress streams, post-scan compliance fires, drill-down filters the resulting table.
  • opa/upload_distrusted_ca.sh against a local OPA — policy uploads cleanly.
  • Scan a target with a known DigiNotar/Symantec leaf (or use opa/testdata/distrusted_ca_input.json) — compliance result returns DISTRUSTED, frontend renders the red ERROR icon, clicking the Compliance "Distrusted" slice filters the asset table to the affected certificates.

@anoncam
anoncam requested a review from a team as a code owner May 15, 2026 19:27
anoncam added 8 commits May 15, 2026 12:31
Documents the side-by-side Vue 2.7 -> Vue 3 migration strategy: Vite +
TypeScript + Pinia + Vue Router 4 + @carbon/web-components + @carbon/charts
core. Plan covers PR sequencing (scaffold -> services -> visualizer
redesign -> cutover), Carbon component mapping, risk register, and
verification steps per PR. Visualizer redesign with interactive,
responsive charts is the motivating goal and lands in PR #3.

Signed-off-by: Cameron Banowsky <[email protected]>
…ration)

Scaffold the next-gen frontend at frontend-next/, runnable side-by-side
with the legacy Vue 2.7 app. No feature parity yet -- this PR proves
the toolchain end to end.

Stack
-----
- Vue 3.5 + Vite 8 + TypeScript 6 (strict)
- Pinia 3, Vue Router 5 (Vue 3 line)
- @carbon/web-components 2.54 (replaces @carbon/vue@2)
- @carbon/charts 1.27 (vanilla core, no Vue wrapper)
- @carbon/styles 1.106 + @carbon/icons 11.80
- ESLint 10 flat config + Prettier 3

Deliverables
------------
- App shell: main.ts bootstraps Pinia + Router, App.vue applies the
  preserved make-the-carbon-theme-go-{white,dark} class names so the
  legacy theming pattern carries over. Theme state lives in
  stores/app.ts; OS preference is detected via matchMedia.
- HeaderBar.vue ported to cds-header / cds-header-name /
  cds-header-global-action with a Pinia-bound 3-state theme toggle
  (auto/light/dark). A small CarbonIcon.vue helper renders icons from
  @carbon/icons descriptor objects.
- FooterView.vue ported to a plain footer using Carbon CSS custom
  properties; dropped the @carbon/ibmdotcom-web-components dependency.
- CarbonChart.vue: generic Vue 3 wrapper around @carbon/charts core
  classes (donut, treemap, bar variants, circle-pack, area, line, etc.).
  Mounts in onMounted, calls model.setData on data change,
  model.setOptions on options/theme change, destroys in onBeforeUnmount.
- HomeView renders a placeholder donut through the wrapper to prove the
  pipeline. ResultsView is an empty placeholder for PR #3.

Build & runtime
---------------
- Multi-stage Dockerfile builds inside the image (node:22-alpine) so
  the host needs no Node toolchain. Placeholder tokens
  (__CBOMKIT_HTTP_API_BASE__, __CBOMKIT_WS_API_BASE__,
  __CBOMKIT_TITLE__, __CBOMKIT_VIEWER_ONLY__, __CBOMKIT_POLICY_NAME__)
  are baked into the bundle via build-time CBOMKIT_* env vars.
- docker/entrypoint.sh maps the existing VUE_APP_* env var contract
  (still used by docker-compose.yaml) onto those placeholders at
  container start, so swapping images at cutover requires no compose
  changes. Glob updated from js/app.*.js to assets/**.{js,html,css}
  to match Vite's hashed asset filenames.
- nginx config copied verbatim from the legacy frontend.
- Makefile gains additive dev-frontend-next and
  build-frontend-next-image targets; existing frontend targets and
  docker-compose.yaml are untouched.

Verification
------------
- npm install succeeds with 0 vulnerabilities (Sonatype-vetted
  versions).
- npm run build (vue-tsc --noEmit && vite build) passes type check and
  produces a working bundle.
- npm run dev serves on http://localhost:8002 alongside the legacy
  frontend on :8001 with no port conflict.
- grep -c "__CBOMKIT_TITLE__" dist/** confirms the placeholder tokens
  survive minification, so runtime sed substitution will work as
  designed.

Out of scope for this PR
------------------------
Service layer (api.js, scan.js, model.js -> Pinia stores), TS types
for the CBOM schema, the visualizer redesign, scan/upload pages,
and the legacy-to-new cutover. Those land in PRs #3/#4/#5 per
frontend/MIGRATION.md.

Signed-off-by: Cameron Banowsky <[email protected]>
Ports the legacy frontend's model and helpers to TypeScript modules
under frontend-next/src/. No UI changes yet -- legacy frontend still
owns the user-facing experience until PR #3.

Pinia stores (split by responsibility)
--------------------------------------
- stores/errors.ts: ErrorEntry list + addError/closeError/clear.
- stores/scan.ts:   scanning state, codeOrigin, credentials, reset
                    helpers (resetScanningInfo/CodeOriginInfo/Credentials).
- stores/cbom.ts:   cbom, dependency maps, policyCheckResult, lastCboms,
                    showResults; startAgain() composes resets across
                    scan + cbom stores.
- stores/app.ts:    theme state (unchanged from PR #1).

Services
--------
- services/api.ts:  typed fetchLastCboms + getComplianceReport with the
                    same fallback-to-local-compliance contract.
- services/scan.ts: typed WebSocket client. Replaces the legacy
                    uuid4 dep with crypto.randomUUID() and a small
                    Math.random fallback. Message dispatch uses a
                    discriminated union over ScanMessageType; payload
                    parsing for DETECTION/CBOM funnels into setCbom +
                    buildDependencyMaps.

Pure helpers
------------
- lib/cbom.ts:           checkCbomValidity, resolvePath,
                         buildDependencyMaps, getDependenciesFor,
                         setCbom, showResultFromApi/Upload,
                         getDetections{,FromCbom}.
- lib/compliance.ts:     getComplianceLevel/Object/Color/Icon/Label/
                         Description, getPolicyResultsByAsset,
                         hasValidComplianceResults type guard,
                         checkValidComplianceResults validator,
                         getComplianceRepartition/getColorScale.
- lib/compliance-local.ts: createLocalComplianceReport (NIST PQC
                           quantum-safe whitelist mirror of the
                           legacy implementation).
- lib/info.ts:           getTermFullName/Description from the copied
                         crypto-dictionary.json; countOccurrences /
                         countNames over typed CbomComponent arrays.
- lib/general.ts:        capitalizeFirstLetter, numberFormatter,
                         formatSeconds, openGitRepo, canOpenOnline,
                         getCodeLink/openOnline (now take CodeOrigin
                         as an arg so they remain pure), limitString.

Types
-----
- types/cbom.ts:       Partial CycloneDX 1.6 + cryptography-ext schema
                       covering the fields the frontend reads:
                       AlgorithmProperties, CertificateProperties,
                       ProtocolProperties, RelatedCryptoMaterial-
                       Properties, CryptoProperties, EvidenceOccurrence,
                       CbomComponent, CbomDependency, Cbom, ScanRecord,
                       DependencyMaps, DependencyComponentList.
- types/scan.ts:       ScanState const-object enum, ScanMessageType
                       union, ScanMessageEnvelope, ScanCredentials/Input,
                       ScanRequestBody, CodeOrigin.
- types/compliance.ts: ComplianceIconKey, ComplianceLevel,
                       ComplianceFinding, PolicyCheckResultOk/Error
                       (discriminated on `error`).
- types/errors.ts:     ErrorStatus const-object enum + ErrorEntry.

Config
------
- config.ts replaces app.config.js. Composes API URLs from
  CBOMKIT_HTTP_API_BASE + CBOMKIT_WS_API_BASE via api.json. Empty base
  falls back to relative URLs so the Vite dev proxy works without env
  vars set locally. getTitle() / isViewerOnly() live here too.
- api.json copied from frontend/.
- data/crypto-dictionary.json copied from frontend/resources/.

MIGRATION.md
------------
Reflects the renumbered sequence (PR #2 is now services/state, PR #3
is the visualizer, PR #4 is scan flow + cutover) and the updated PR #2
scope (no router changes -- routes get expanded in PR #3 alongside the
visualizer ports).

Verification
------------
- npm run type-check (vue-tsc --noEmit) passes with strict mode +
  verbatimModuleSyntax enabled.
- npm run build succeeds; new modules tree-shake out for now (bundle
  hashes unchanged from PR #1) since no UI references them yet -- they
  enter the bundle in PR #3 when components start importing.

Signed-off-by: Cameron Banowsky <[email protected]>
Ports the visualizer UI to Vue 3 and ships the chart improvements that
motivated the whole migration. Drag-drop or click "Try the sample CBOM"
on the home page to land on the new /results page.

Charts
------
Five charts in a responsive CSS-grid (no fixed pixel heights anymore):
- Compliance donut (with local-approximation indicator)
- Asset names circle pack
- Primitives donut
- Functions donut
- Primitive -> algorithm treemap (new chart type, full-width)

Each chart has the Carbon toolbar enabled (zoom, expand, export PNG/CSV).
The wrapper resolves chart-specific click events into a single
`datum-click` emit so parents don't have to learn each chart's event name.
Carbon Charts wraps donut datums as `{data: {group, value}}` and treemap
leaves as `{name, value}` -- a small `unwrap()` helper in StatisticsView
handles both shapes uniformly.

KPI strip
---------
Above the charts, four metric cards summarise at-a-glance:
crypto-asset count, quantum-vulnerable count, top primitive, top
function. The "vulnerable" card flips between positive (green) and
warn (amber) accents based on the count.

Drill-down
----------
Clicking a donut slice, a circle-pack node, or a treemap leaf emits a
`filter-change` event that the ResultsView routes into a filter prop on
the DataTable. A dismissible filter chip appears above the table; the
chip's X clears the filter. Clicking the same slice twice toggles the
filter off.

DataTable
---------
A simplified port (versus the 467-LOC legacy version): renders
detections from the cbom store with columns Status / Name / Primitive
/ Functions / Location. Free-text search input alongside the filter
chip. Empty-state row when filters yield no matches. Modal triggers
and column-level customisation are deferred to PR #4 (defaults are
enough for the visualizer redesign).

Component inventory
-------------------
New under src/components/results/:
- ComplianceIcon.vue:  resolves the compliance icon descriptor through
                       complianceIconMap and applies the level colour.
                       Substitutes WatsonHealthImageAvailabilityUnavailable
                       (no longer in @carbon/icons) with the `unknown`
                       icon.
- KpiStrip.vue:        the four metric cards.
- StatisticsView.vue:  the charts grid + drill-down dispatch.
- DataTable.vue:       the table with filter + search.
- ResultTitle.vue:     title, subtitle (live detection count) + code-origin
                       tags (gitUrl/revision/commit/subfolder).
- ReturnButton.vue:    stops the WebSocket, calls store.startAgain(),
                       navigates back to home.

New under src/components/home/:
- FileUploader.vue:    plain drag-drop + click-to-browse JSON upload.
                       No Carbon Web Component used (the legacy
                       cv-file-uploader's slot API didn't translate
                       cleanly). Reads the file via FileReader, calls
                       showResultFromUpload + getComplianceReport,
                       emits an `uploaded` event the parent uses to
                       push to /results.

Other changes
-------------
- main.ts side-effect imports for cds-tile, tag, link, loading,
  inline-loading, skeleton-text, file-uploader, data-table, search,
  tooltip (registers them as custom elements).
- HomeView replaces the PR #1 placeholder donut with the uploader and
  "Try the sample CBOM" button. The bundled sample is a copy of
  example/keycloak-cbom.json -- yields 103 detections.
- Router: beforeEnter guard on /results redirects to home if no CBOM
  is loaded.
- App.vue sets document.title = getTitle() at startup so the dev tab
  title reads "CBOMkit" instead of the runtime placeholder token.
- types/filter.ts: shared DetectionFilter discriminated union, used by
  StatisticsView, ResultsView, and DataTable.

Verification
------------
- npm run type-check (strict vue-tsc) passes.
- npm run build succeeds. Bundle: ResultsView 438 kB (gzip 131 kB),
  index 1.27 MB (gzip 185 kB). Code-splitting per chart type can come
  later; the warning is noise for the redesign milestone.
- Manual: dropped the bundled keycloak sample on the new UI in
  Chrome; all 5 charts rendered, KPI strip showed (103 / 27 / Pke /
  Keygen), clicking the compliance "Unknown" slice filtered the table
  from 103 to 50 rows and surfaced a "Compliance: Unknown" chip; dark
  mode matched OS preference.
- Legacy `make production` flow still works unchanged.

Out of scope for this PR
------------------------
- Scan flow (SearchBar, WebSocket UI, GitInfoPrompt).
- Modal subcomponents (CryptoAssetDetails, DependenciesView,
  GithubEmbed).
- Notifications surface for errors.
- Vitest smoke tests + the legacy-to-new cutover.
All deferred to PR #4.

Signed-off-by: Cameron Banowsky <[email protected]>
Ports the remaining UI to reach parity with the legacy Vue 2 frontend,
plus a Vitest smoke suite. The next commit performs the legacy
cutover.

Components
----------
- global/NotificationsView.vue: toast notification surface bound to
  the errors Pinia store. Maps the ErrorStatus enum to friendly title
  + description + kind (error / warning / info) and uses
  cds-toast-notification (registered in main.ts).

- home/SearchBar.vue: Git URL / PURL input with optional advanced
  options (Scan tab: branch + subfolder; Authentication tab: username
  + password/PAT). Builds a credentials object and calls
  connectAndScan from services/scan, then routes to /results. Plain
  HTML inputs rather than cds-text-input -- the v-model story for
  custom-element inputs is fiddly and not worth a wrapper for this
  PR.

- home/ListTable.vue: last CBOMs fetched via fetchLastCboms in
  onBeforeMount. Skeleton-text rows while loading. Clicking a row
  calls showResultFromApi + getComplianceReport and routes to
  /results.

- home/SearchOrUploadView.vue: container that pairs SearchBar +
  FileUploader (with ListTable above when not in viewer-only mode).
  Replaces the bare HomeView layout.

- results/LoaderView.vue: cds-inline-loading bound to the scan store's
  scanningStatus state machine (LOADING/ENDING/LOADED/ERROR).

Wire-up
-------
- App.vue mounts NotificationsView next to the header.
- HomeView delegates to SearchOrUploadView and keeps the sample-CBOM
  shortcut.
- ResultsView shows LoaderView in the toolbar while a scan is in
  progress.
- Router guard on /results allows entry when a scan is in flight
  (live detections render) in addition to the previous "cbom is
  loaded" case.
- main.ts registers additional cds-* tags: notification, checkbox,
  tabs, text-input.

Tests (new)
-----------
- vitest 4 + jsdom 29 + @vue/test-utils 2 set up via vite.config.ts.
  Imported from vitest/config so the `test` block type-checks.
- src/lib/__tests__/compliance.test.ts: 9 cases covering
  checkValidComplianceResults validator, hasValidComplianceResults
  type guard, getComplianceLevel min-finding logic + default
  fallback.
- src/lib/__tests__/compliance-local.test.ts: 5 cases over the
  NIST PQC local report -- RSA flagged Not Quantum Safe, ML-KEM hits
  the whitelist, symmetric primitives are Not Applicable, missing
  primitive is Unknown.
- src/lib/__tests__/cbom.test.ts: 9 cases over resolvePath,
  getDetectionsFromCbom occurrence unwrap, buildDependencyMaps for
  both top-level dependencies and crypto-property refs.
- src/lib/__tests__/info.test.ts: 4 cases over countOccurrences
  (string + array values) and countNames.

All 27 tests pass. type-check + build remain clean.

Out of scope (deferred to follow-up issues, not blockers for cutover)
--------------------------------------------------------------------
- Modal subcomponents (CryptoAssetDetails, DependenciesView,
  GithubEmbed, GitInfoPrompt for post-upload git info).
- RegulatorResults compliance scorecard (KPI strip already shows the
  vulnerable count and the StatisticsView donut covers the
  distribution).
- ExplainerView / PluginExplainerView informational cards.
- DebugView (dev-only).

Signed-off-by: Cameron Banowsky <[email protected]>
Renames frontend-next/ to frontend/ and removes the legacy Vue 2 code.
Vue 3 + Vite + TypeScript + Pinia + Vue Router + @carbon/web-components
+ @carbon/charts is now the production frontend.

Changes
-------
- git mv frontend-next/ -> frontend/.
- git rm -rf frontend/ (legacy contents moved to frontend-legacy/ then
  deleted; git rename detection keeps the per-file history through
  the move).
- Makefile: the dev-frontend-next + build-frontend-next-image targets
  are removed (only one frontend now). build-frontend-image points at
  the multi-stage Vite Dockerfile. Added dev-frontend-local for `npm
  run dev` without docker.
- frontend/MIGRATION.md is retained in git history; the migration is
  complete.

User-visible
------------
- `make build-frontend-image` now produces an image built from the
  Vue 3 source. Compose `make production` / `make dev-frontend` /
  `make coeus` flows are unchanged from the outside; they continue
  to pull `ghcr.io/cbomkit/cbomkit-frontend:${VERSION}` until the
  user publishes a new image.
- `cd frontend && npm install && npm run dev` for local dev on :8002
  (Vite proxies /api and /v1/scan to the Quarkus backend on :8081).
- The same VUE_APP_HTTP_API_BASE / VUE_APP_WS_API_BASE /
  VUE_APP_TITLE / VUE_APP_VIEWER_ONLY / VUE_APP_POLICY_NAME env vars
  are honoured by the nginx entrypoint (mapped to __CBOMKIT_*__
  placeholder tokens at build time), so docker-compose.yaml requires
  no changes.

Verification
------------
- npm install, npm run type-check, npm run build, npm test all pass
  in the new frontend/ location (27 tests).
- Manual smoke in Chrome: home page, sample CBOM load, charts +
  drill-down, notifications surface, search bar, list table skeleton
  all render correctly and have no console errors.
- `make build-frontend-image` builds a working image via the
  multi-stage Dockerfile.

Out of scope, follow-up issues
------------------------------
- Asset-detail modal, dependencies-graph modal, GitHub embed modal,
  post-upload Git info prompt.
- Per-chart-type dynamic imports to shrink the main bundle from its
  current ~1.97 MB (gzip 258 kB) baseline.
- Wire Vitest into CI.

Signed-off-by: Cameron Banowsky <[email protected]>
Three small bugs surfaced when running the new frontend against a real
Quarkus backend.

WebSocket URL synthesis
-----------------------
joinURL fallback returned a relative URL ("/v1/scan/...") when no
CBOMKIT_WS_API_BASE was set. fetch() resolves relative URLs against
the document base, but `new WebSocket()` requires an absolute URL --
so scans failed with close code 1006 before the upgrade was even
sent.

Split into joinHttpURL (relative is fine) and joinWsURL (synthesises
ws://<host>:<port>/... from window.location when no base is
configured).

Dev proxy CORS Origin rewrite
-----------------------------
The backend's allowlist is set via CBOMKIT_FRONTEND_URL_CORS, which
docker-compose pins to http://localhost:8001 (legacy frontend port).
The Vite dev server runs on :8002, so HTTP requests appeared to work
(Vite proxy hides origin from the browser) but the WebSocket upgrade
forwarded the browser's Origin header and the backend rejected it.

Both proxy entries now send Origin: http://localhost:8001 explicitly,
matching the allowlist. No change in production -- there the
frontend container talks to the backend on the docker network with
the right origin already.

Compliance trigger on scan completion
-------------------------------------
The legacy frontend ran the compliance check via a watcher on
model.cbom. PR #2 moved cbom into a Pinia store but the equivalent
trigger never landed, so post-scan CBOMs stayed at "Evaluating
compliance..." indefinitely.

services/scan.ts now calls getComplianceReport(cbom) inside the
CBOM message handler. The upload path already did this directly in
FileUploader, so both flows now produce a policy result.

Pre-set scan state for route guard
----------------------------------
connectAndScan sets isScanning + scanningStatus to LOADING
synchronously before opening the WebSocket. Without this, the
synchronous router.push('/results') hit the route guard before the
async onopen fired, and the guard sent the user back to home.

Verification
------------
- Manual: pkg:maven/io.quarkus/[email protected] scanned to
  completion via the new UI; live progress messages streamed
  ("Cloning... Receiving objects 25% -> 90%, Resolving deltas 45%..."
  -> "Scan finished"); 4 cryptographic assets detected; KPI strip
  populated (Top primitive Hash, Top function Digest); commit hash
  44bc4a9 and subfolder core/runtime auto-populated from backend
  messages; donut + circle-pack charts rendered with real data.
- 27/27 Vitest tests still pass, type-check clean, build clean.

Signed-off-by: Cameron Banowsky <[email protected]>
- Replace .github/img/cbomkit.gif (1.7 MB, 14 frames) with a recording
  of the new visualizer: load the sample CBOM, click the Compliance
  donut "Not Quantum Safe" slice (table filters 103 -> 27, chip
  appears), clear the filter, click the largest treemap leaf (table
  filters by name 'key', 103 -> 24), scroll back to the chart grid.
- Annotate the README image with a one-line caption naming what the
  GIF demonstrates.
- Add a "Visualizer features" subsection under "Frontend and
  CBOMkit-coeus" describing the KPI strip, the five charts, drill-down
  behaviour, toolbar, responsive grid, and theme toggle. Notes the
  stack underneath (Vue 3 + Vite + TS + Pinia + Carbon).
- Replace the stale CONTRIBUTING snippet that ran
  `vue-cli-service serve --port 8001` with the current
  `cd frontend && npm install && npm run dev` flow (Vite on :8002,
  proxies /api and /v1/scan to :8081, Origin rewrite for backend
  CORS). Add the type-check / test / build / lint shortcuts.

Signed-off-by: Cameron Banowsky <[email protected]>
anoncam added 3 commits May 20, 2026 09:25
New Rego policy `distrusted_ca` in the existing `policies` package that
flags CycloneDX cryptographic-asset certificates whose issuerName matches
a known-distrusted or malicious Certificate Authority. Uses case-insensitive
substring matching against issuer DNs (more robust than equality given RDN
ordering/quoting variation across CBOM emitters).

Initial distrusted list covers root-store removals from 2011-2024:
DigiNotar, WoSign/StartCom, Symantec (incl. Thawte/GeoTrust/RapidSSL),
Camerfirma, TrustCor, e-Tugra, Entrust.

Result vocabulary: distrusted / trusted / unknown. Finding shape matches
the existing OPAFinding contract (rule, result, value, bom-ref, property).

Includes upload script mirroring upload_quantum_safe.sh, a sample input
fixture covering all three outcomes, and a README documenting both
policies and the local development workflow.

Signed-off-by: Cameron Banowsky <[email protected]>
Adds DISTRUSTED and TRUSTED entries to the OPAResult enum and the
ComplianceLevel map so findings emitted by the distrusted_ca Rego policy
deserialize cleanly and surface through the compliance API and frontend
without falling back to UNKNOWN.

Severity ranks:
  0 DISTRUSTED   red,   ERROR icon         (uncompliant)
  1 QUANTUM_VULNERABLE  yellow, WARNING    (uncompliant, unchanged)
  2 UNKNOWN             blue,   UNKNOWN    (uncompliant, unchanged)
  3 QUANTUM_SAFE        green,  SECURE     (compliant,   unchanged)
  4 NA                  gray,   NA         (compliant,   unchanged)
  5 TRUSTED             green,  CHECKMARK  (compliant)

Additive only; existing quantum_safe policy results are unaffected.

Signed-off-by: Cameron Banowsky <[email protected]>
The Vue 3 migration upgraded Vite to ^8.0.13, which requires
Node.js 20.19+ or 22.12+. Node 21 is past EOL and breaks Rolldown's
native binding resolution with:

  Cannot find module '@rolldown/binding-linux-x64-gnu'

Failing CI run prior to this fix:
https://github.com/anoncam/cbomkit/actions/runs/25937520262

Bumping the matrix to 22.x (the next LTS line) restores green CI on
every PR. No code or lockfile changes required.

Signed-off-by: Cameron Banowsky <[email protected]>
@san-zrl

san-zrl commented May 26, 2026

Copy link
Copy Markdown
Contributor

Hi @anoncam - Many thanks for this PR. This is simply amazing and very, very valuable going forward.

Two humble remarks,

  1. The primitive -> algorithm breakdown in rectangular form takes a lot of real estate. You could turn it into a circle/pie similar to the other "widgets". In the interest of conciseness, we shouldn't use more than one row of such widgets. Not sure which views provide the most useful information.
  2. We use the viewer for demonstrations to interested people and found out that the asset-detail modal and the github-embed modal are very useful since they provide some sort of context to the findings. If you feel like it, you could try to add them. That would be fantastic.

@anoncam

anoncam commented May 26, 2026

Copy link
Copy Markdown
Author

@san-zrl I got you and will make those adjustments.

… modals

Addresses maintainer feedback on PR cbomkit#336:

1. Single-row visualizer layout
   - Drop the wide primitive -> algorithm treemap card. The treemap was the
     only widget on a second row and the maintainer flagged it as taking too
     much real estate. Its two info dimensions (primitive distribution and
     algorithm distribution) are already covered by the existing "Primitives"
     donut and "Asset names" circle pack, so removing it loses no information
     while collapsing the grid to one row.
   - Tighten the grid's column min-width from 280px to 240px so the four
     remaining cards fit comfortably across one row inside the 1200px
     app-main container, while still wrapping gracefully on narrow viewports.
   - Pin .charts__card to a fixed height of 380px with overflow:hidden.
     Without this, Carbon Charts' fullscreen toolbar action could leave the
     SVG with an oversized intrinsic size after exit, dragging the whole
     card vertically with the chart stuck at the top and legend pushed out
     of view.
   - Drop the now-unused unwrap-for-treemap path in the click handler and
     the .charts__card--wide / .charts__body--tall style rules.

2. Asset-detail modal
   - Re-introduce the legacy CryptoAssetDetails surface (deferred from PR #4
     part 1) in components/results/modal/. Built on cds-modal +
     cds-structured-list web components so we stay on the new Vue 3 +
     Carbon Web Components stack.
   - DataTable rows are now keyboard-focusable buttons (role=button,
     tabindex=0, Enter/Space activated) that emit select-asset; ResultsView
     hosts the modal envelope and tracks the selected asset.
   - The modal surfaces the same four sections as before: code preview
     (GithubEmbed below), compliance finding + per-finding messages in a
     structured list, dependency graph (depends-on / provides-to / used-by /
     provided-by, each linking to a sub-detail view), and the full ordered
     CycloneDX 1.6 cryptographic-asset specification table.
   - Sub-asset navigation uses an in-modal stack: clicking a related
     asset's "See details" pushes a new frame; a "<- Back" affordance in
     the modal header pops back to the original selection. Closing the
     modal clears the stack.

3. GitHub-embed modal
   - Port the legacy GithubEmbed (deferred from PR #4 part 1) to consume
     getCodeLink + useScanStore.codeOrigin, swap the legacy emgithub theme
     selector to read useAppStore.useDarkMode, and keep the line-of-interest
     highlight via the same :nth-child(5) global rule.
   - Renders inline inside the asset-detail modal. Shows a placeholder
     copy when the CBOM lacks code-location metadata or the repository
     isn't GitHub.

4. Vue 3 component wiring
   - Register cds-modal and cds-structured-list custom elements in main.ts
     so the new modal's template renders without ad-hoc imports.

Verification
- npm run type-check, npm test (27/27), and npm run build all pass.
- Drove a real keycloak-cbom.json scan against the running Quarkus
  backend in the dev server (vite :8002 -> :8081):
  * All four widgets render in a single row with their legends below.
  * Clicking the Compliance donut's "Not Quantum Safe" slice filters the
    asset table 103 -> 27 and surfaces a dismissible filter chip
    (drill-down preserved across the layout change).
  * Clicking an asset row opens the modal with code preview (live
    emgithub.com embed against BCEcdhEsAlgorithmProvider.java:208),
    compliance finding, and full specification.
  * Clicking a row with "Related Crypto Material" surfaces the
    Depends-on graph; pushing into a sub-asset (DSA) shows its own code
    preview, compliance, and "Is used by" reverse edges; Back pops the
    stack and the original asset re-renders.
  * Entering Carbon Charts fullscreen on the Compliance donut and
    exiting now leaves the card layout untouched (was: card stretched
    to ~1600px tall with legend lost).

Signed-off-by: Cameron Banowsky <[email protected]>
@anoncam

anoncam commented May 26, 2026

Copy link
Copy Markdown
Author

@san-zrl pushed 072787b addressing both points:

1. Single-row visualizer (no more second-row real-estate hog). Dropped the wide primitive→algorithm treemap rather than reshaping it — its two info dimensions are already covered by the existing Primitives donut and Asset names circle-pack, so removing it loses no information and lets the four remaining widgets sit in a single tightened-grid row. Cards are now pinned at height: 380px with overflow: hidden, which also fixes a Carbon Charts side-effect I caught in testing: entering fullscreen on a chart and exiting used to leave the card stretched ~1600px tall with the legend pushed out of view.

2. Asset-detail modal + GitHub-embed restored. New CryptoAssetDetails.vue + AssetDetailModal.vue built on cds-modal + cds-structured-list (same Vue 3 + Carbon Web Components stack as the rest of the PR). Same four sections as the legacy version: code preview (live emgithub.com embed via the ported GithubEmbed.vue), compliance finding + per-finding messages, dependencies graph (depends-on / provides-to / used-by / provided-by), and the full ordered CycloneDX 1.6 cryptographic-asset specification table. Sub-asset navigation uses an in-modal stack — clicking "See details" on a related asset pushes a new frame, with a ← Back affordance to pop back.

Verification against the live Quarkus backend (keycloak sample, 103 detections):

  • All four widgets render in one row with legends below.
  • Drill-down preserved: clicking the Compliance "Not Quantum Safe" slice filters the asset table 103 → 27 with a dismissible chip.
  • Clicking an asset row opens the modal with the live GitHub embed (BCEcdhEsAlgorithmProvider.java:208 highlighted).
  • The dependencies graph works end-to-end: a Related-Crypto-Material key shows "Depends on DSA"; clicking through pushes a DSA frame with its own code preview, compliance finding, and "Is used by" reverse edges; ← Back pops cleanly.
  • Carbon Charts fullscreen enter/exit no longer disturbs the card layout.
  • npm run type-check, npm test (27/27), and npm run build all green.

Out-of-scope follow-ups from the original PR description still apply (dependencies-graph modal, post-upload Git-info prompt, per-chart-type dynamic imports for bundle shrink).

anoncam added 2 commits May 26, 2026 12:07
… row

repeat(auto-fit, minmax(240px, 1fr)) treats each Carbon Charts cell's
min-content as the column's lower bound, and Carbon Charts' legend (the
horizontal row of swatches + labels below the donut) routinely measures
wider than 240px. On narrower-than-1200px result viewports that pushed
the fourth card onto a second row, defeating the single-row layout the
maintainer asked for.

Switch to repeat(4, minmax(0, 1fr)) so the columns ignore intrinsic
content width and stay at four equal fractions of the container. The
existing overflow:hidden on .charts__card absorbs any legend that
would otherwise overflow horizontally at very narrow widths.

Add a single media-query break at <=720px that drops to two columns,
where four would be unreadably small.

Signed-off-by: Cameron Banowsky <[email protected]>
The `Build Frontend` workflow was red on both 072787b and 0b3551c
with:

  Cannot find module '@rolldown/binding-linux-arm64-musl'
  npm has a bug related to optional dependencies
  (npm/cli#4828)

Two compounding root causes:

1. No `frontend/.dockerignore`. The `COPY . .` step after `npm install`
   was clobbering the freshly resolved linux-<arch>-musl `node_modules/`
   with whatever sat on the build host — on a Mac, that's the
   arm64-darwin tree, which obviously has no linux-musl Rolldown
   binding. Add a `.dockerignore` excluding `node_modules`, `dist`,
   caches, and `.git`.

2. `npm ci` inside the multi-arch image build also fails because the
   committed lockfile only lists the binding for the platform the
   lockfile was generated on (npm cli #4828, which Rolldown's runtime
   error explicitly cites). Rolldown maintainers' instruction is to
   delete the lockfile and run a fresh install. Switch the build-stage
   install to:

     rm -f package-lock.json && npm install --include=dev \
       --no-audit --no-fund --no-progress

   The pinned versions in package.json still give us a deterministic
   build for non-native deps; the lockfile stays the source of truth
   for `npm ci` in CI matrix runs (which only ever build for one
   platform). `--include=dev` is required because the image sets
   NODE_ENV=production at the top, which would otherwise omit vue-tsc
   and vite themselves.

Verified locally with `docker buildx build --platform linux/arm64`
and `--platform linux/amd64` — both produce a working image (the
`npm run build` step exits 0, the Stage 2 nginx-unprivileged image
exports cleanly).

Signed-off-by: Cameron Banowsky <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants