Skip to content

Feat/web v2 scaffold#11

Merged
Mnikley merged 60 commits into
masterfrom
feat/web-v2-scaffold
Jun 1, 2026
Merged

Feat/web v2 scaffold#11
Mnikley merged 60 commits into
masterfrom
feat/web-v2-scaffold

Conversation

@Mnikley

@Mnikley Mnikley commented Jun 1, 2026

Copy link
Copy Markdown
Member

Adds a React/TypeScript based web-app with a FastAPI backend server, plus deployment tooling

New Features

  • Frontend (web/) - Vite + React + TS; Complete OntoloViz rewrite using d3.js
  • Server (src/ontoloviz_server/) - FastAPI app with health, models, OBO and ontology-handoff routes; Dockerized
  • Packaging - ontoloviz wheel for desktop + server + web; version sourced from VERSION
  • Deployment - install-service.sh and update-service.sh workflows, docs/RUNBOOK.md, .env.example

Tests

Bumped coverage by adding parse / layout / color / propagation / taper / session / store tests

Matthias added 30 commits May 22, 2026 15:15
The Dash-based web interface has been superseded by a new V2 app built on
React + D3 + Canvas (in web/) with a FastAPI backend (in server/). The
Dash port translated tkinter dialogs verbatim and never delivered a
web-native UX.

- Delete src/ontoloviz/web.py (1320 LOC)
- Drop the [web] optional-dependency group (dash, dash-bootstrap-components, pandas)
- Remove the ontoloviz-web console script
- Refresh uv.lock (22 transitive packages removed)
- Update README quickstart to point at the new V2 layout
Two-package scaffold for the V2 browser-based ontology visualizer.

Frontend (web/):
- Vite + React 18 + TypeScript (strict) + Tailwind
- D3 (d3-hierarchy, d3-scale, d3-shape) for layout math; Canvas 2D for
  rendering (WebGL only if profiling later demands it)
- Zustand for state, papaparse + SheetJS for file parsing
- Vitest + ESLint + Prettier wired up
- HealthIndicator polls /api/health via the Vite dev proxy

Backend (server/):
- FastAPI + uvicorn, uv-managed, separate pyproject.toml
- Routers: /api/health (live), /api/obo (stub), /api/models (stub)
- pytest + ruff configured; 3 smoke tests pass

Architectural seam: propagation runs in the browser (TypeScript), not on
the backend, so the future standalone interactive HTML export works
offline. The backend handles data ingestion (OBO fetch, future external
model adapters) and persistence only.

Repo plumbing:
- Top-level Makefile with make install / make dev / make test that wires
  both halves together (Vite on :5173, FastAPI on :8000)
- web/README.md and server/README.md document each half independently

The tkinter app (src/ontoloviz/) is untouched. core.py stays as the
reference implementation for upcoming TS propagation ports.
- Add .idea/ to the repo-root .gitignore so JetBrains project files
  stop showing up in git status.
- Pick up the prettier reformat the user/linter applied to
  web/src/App.tsx and web/tailwind.config.ts. No behavioral change.
Step 2 of the V2 rebuild: ingest the same TSV/XLSX formats the tkinter
app supports, in TypeScript, with parity-tested behavior against the
shipped templates.

Data model (src/lib/ontology/types.ts):
- Immutable Node, Subtree, Ontology types mirroring the Leaf/Branch
  shape from src/ontoloviz/core.py.
- Warnings collected on the Ontology, never thrown.

Parser (src/lib/ontology/parse.ts):
- Three formats with auto-detection from the header row:
  * parent-based   (ID / Parent columns)
  * separator-based MeSH (Tree ID column, '.' separator)
  * ATC (positional, variable-width step sizes per level)
- Pipe-separated multi-id rows are expanded into individual nodes.
- Missing ancestors are backfilled as synthetic nodes so every chain
  reaches its root, matching check_atc_parent / check_mesh_parent in
  the Python reference.
- Pure: no I/O, no globals.

Tests (tests/ontology/parse.test.ts):
- 21 cases covering both shipped templates (templates/atc_template.tsv,
  templates/mesh_template.tsv) plus the parent-based template literal
  inherited from the deleted Dash port.
- Covers level derivation, parent linkage, multi-id expansion,
  duplicate-id warnings, CRLF, UTF-8 BOM, and empty input.

Repo housekeeping:
- Anchor /lib/ and /lib64/ in .gitignore so the Python venv patterns
  stop swallowing web/src/lib/.
Parity tests against the real example datasets
(templates/mesh_example_pubmed_mapped.tsv,
templates/atc_example_covid_drugs_experimental.tsv) uncovered a bug
where the MeSH and ATC parsers flagged a real row as duplicate when a
synthetic placeholder for the same id had already been inserted by an
earlier descendant's backfill pass.

Example: D007239's tree id is "C01". Earlier rows reference
"C01.150...", which causes backfillAncestors to create a synthetic
"C01" node. When D007239's actual row arrives, the parser saw the
existing node and emitted a "duplicate id" warning, dropping the real
count (24636) on the floor.

Fix: only flag duplicates when an existing *non-synthetic* node is
present; otherwise the real row supersedes the placeholder.

(The parse.test.ts diff is a pure prettier reformat that came with the
change — no test logic was modified.)
Step 3 of the V2 rebuild: ports the count-propagation half of the
tk app's propagation model to TS, with a parity contract against the
Python reference.

Engine (web/src/lib/ontology/propagate.ts):
- Pure function propagateCounts(ontology, settings) covering the
  off / level / all modes. Returns a new Ontology — inputs are never
  mutated.
- Deterministic bottom-up traversal (sorted by level, descending) so
  ancestor counts truly reflect every descendant. The legacy core.py
  MeSH path iterates in dict-insertion order and only propagates one
  level per pass; the V2 port fixes this intentionally.

Unit tests (web/tests/ontology/propagate.test.ts):
- 9 cases on a synthetic 6-node parent-based tree covering every mode,
  the level-threshold matrix, and the no-mutation invariant.

Parity harness (tests/parity/generate_fixtures.py):
- Loads templates/atc_example_covid_drugs_experimental.tsv and
  templates/mesh_example_pubmed_mapped.tsv through src/ontoloviz/core.py.
- Runs the corrected (bottom-up) propagation on each tree across a
  matrix of (mode, level, enabled) combinations.
- Dumps 10 JSON fixtures under web/tests/fixtures/parity/.

Parity tests (web/tests/ontology/parity.test.ts):
- Loads each fixture, runs the TS pipeline (parseTsv → propagateCounts)
  on the source TSV, and asserts every node's count matches the
  Python-derived expectation.

Plumbing:
- New `make parity-fixtures` target regenerates the JSON fixtures.
- web/README.md gains a Parity section explaining the workflow and a
  roadmap checklist marking steps 1–2 (parsing, count propagation)
  complete.
- propagateColors(ontology, settings) covers off/specific/global/phenotype
  modes; returns a new Ontology, never mutates input
- buildColorScale ports core.py's calculate_color_scale_for_node (factor
  selection + multi-stop linear RGB interpolation, Plotly n_colors parity)
- phenotype mode walks bottom-up to color outermost nodes only; the
  legacy dict-insertion-order traversal in core.py degenerates to "color
  everything" when ancestors come before descendants — same family of
  order-dependency as the count-propagation fix
- empty TSV color cells resolve to defaultColor at propagation time so
  every node ends up renderable
- 20 unit tests on a synthetic dotted tree + RGB helpers, 11 parity
  fixtures covering all four modes plus level gates and the disabled
  no-op path on both MeSH and ATC example datasets
- tests/parity/generate_fixtures.py extended with a build_color_scale /
  propagate_colors port; emits to web/tests/fixtures/parity-color/
- web/README.md roadmap marks step 3 done; parity section documents the
  bottom-up divergence in both algorithms
…orts, and OBO backend

Web:
- D3 partition + Canvas sunburst renderer with click-to-zoom and breadcrumb trail
- Zustand store driving live count/color propagation across all surfaces
- Settings panel with mode pickers, level thresholds, and editable scale stops
- Virtualized summary grid with bidirectional hover linking and case-insensitive search
- High-DPI PNG, SVG, and self-contained HTML exports with native browser tooltips

Server:
- OBO parser handling [Term] stanzas, multi-parent placement, obsolete filtering, and cycle promotion to synthetic roots
- POST /api/obo/parse and GET /api/obo/fetch (httpx proxy with timeout and size cap)
- Typed model-adapter contract (ModelProvider, PredictRequest/Response) reserved for future external providers

Docs rewritten to describe the production app instead of a phased roadmap.
Shows a modal overlay during file upload with stage label, byte/node detail, and a determinate progress bar. Stages: read file, parse TSV, propagate counts & colors, render. requestAnimationFrame yields between stages so the overlay paints between blocking CPU work.
Replaces .eslintrc.cjs with eslint.config.js (flat config) wiring @typescript-eslint, react, react-hooks, and prettier. Adds @eslint/js and globals devDeps required by flat config.

Fixes the source issues surfaced once linting actually ran:
- SettingsPanel.tsx: import ReactNode explicitly instead of relying on the React global
- Sunburst.tsx: import MouseEvent from react instead of React.MouseEvent
- parse.ts: replace literal U+FEFF in the BOM-stripping regex with \uFEFF escape
… theming

- Switch to cohesive dark editorial theme via CSS-variable tokens
  consumed by Tailwind through `rgb(var(...) / <alpha-value>)`,
  enabling per-attribute theme swaps and proper alpha modifiers.
- Add dark/light theme toggle (ThemeToggle + lib/theme) with
  localStorage persistence and pre-paint bootstrap in index.html
  to avoid FOUC.
- Promote the header to a real toolbar: subtree picker, filename,
  health, theme, export menu, settings toggle, reset, load-new.
- Replace always-on settings sidebar with an animated slide-in
  drawer scoped to <main>, dismissable via scrim or close button.
- Replace always-on summary grid with a collapsible panel exposing
  inline-editable label/count/color cells; edits flow through a
  new store action `updateNode` that produces a fresh immutable
  ontology so propagation re-runs automatically.
- Replace inline three-button ExportBar with a header dropdown
  ExportMenu (PNG 2x / SVG / standalone HTML).
- Add real empty state with hero copy and feature cards.
- Theme native chrome: range thumbs, scrollbars, color inputs,
  selects, and `color-scheme` follow the active theme.
- Rename accent-button foreground token to `text-on-accent` so the
  same class produces dark-on-orange in dark mode and white-on-orange
  in light mode.
- Cap buildColorScale's interpolated array at 1M entries by widening
  the factor when needed, so a typo like count=1e13 no longer hangs
  the tab. Real ATC/MeSH maxima stay under the cap and parity holds.
- Wrap inline data-table edits in the existing LoadingOverlay so the
  re-propagation pass on large trees is visible instead of looking
  frozen. App passes an onEdit handler that yields two RAFs around
  the store mutation.
- Default SummaryGrid to the active subtree only and add an "all
  subtrees" toggle. Header count reflects the scoped view.
- Remove the "Reset" and "Load new…" buttons from the loaded-state
  header. Uploads happen from the landing page; clearing happens via
  the brand mark.
- Make the OntoloViz logo+title clickable when an ontology is loaded.
  Clicking opens a centered ConfirmDialog (alertdialog) that warns the
  current data and inline edits will be discarded. Backdrop click and
  Escape both cancel; Reset is the focused accent action.
- Mirror LoadingOverlay's single-child flex-center layout so the
  dialog centers reliably without an absolute-positioned backdrop
  sibling fighting the flex container.
- Replace marketing EmptyState (gradient headline, accent pill, feature
  grid) with a discreet graph_lens_lite-style landing: compact ring mark,
  short subtitle, primary "Open File" CTA, three sibling cards, faint
  footer. Subtle network-pattern overlay + soft accent radials, theme-aware
  via existing CSS vars.
- Add OBO Foundry picker modal with seven curated presets (HPO, MONDO,
  DOID, GO, ChEBI, Uberon, CL) plus a free-form .obo URL input.
- Add fetchObo() helper that calls /api/obo/fetch and converts the wire
  JSON to the frontend's Map-based Ontology shape.
- Wire "Try an Example" through the existing upload pipeline via a
  CustomEvent so the loading overlay + parser path stays single-sourced.
- Bundle atc_template, mesh_template, and one ATC COVID example under
  /templates for the download + example cards.
- Drop the header's redundant Choose-TSV button and the glowing accent
  brand mark in favor of a small square + name.
- Landing background now stretches the full main area
- ATC and MeSH templates are separate first-class cards
- COVID-19 example loads directly (no submenu)
- Bump web package to 2.0.1
Removes the duplicated literal between pyproject.toml and __init__.py so
the /api/health badge tracks the package version automatically.
…z wheel

`pip install ontoloviz` now ships the desktop GUI, the FastAPI server, and the
bundled web frontend as a single PyPI distribution — no optional extras, no
second package.

- move server/ontoloviz_server/ → src/ontoloviz_server/; both packages live
  side-by-side under one wheel
- drop server/pyproject.toml + server/uv.lock; merge fastapi, uvicorn[standard],
  httpx, pydantic and the dev extras into the root pyproject
- expose a second console script: `ontoloviz-server` (alongside `ontoloviz`)
- ontoloviz_server.__init__ now reads importlib.metadata.version("ontoloviz")
- bundle the production web build under src/ontoloviz_server/web_dist/ via
  setuptools package-data; main.py mounts it as the SPA at `/` with an /api/*
  guard, and gracefully skips when the directory is absent (dev mode)
- Makefile: `make build` runs pnpm build → embeds dist/ → uv build, producing
  wheel + sdist; `dev-server` runs `uv run ontoloviz-server` from repo root
- move server tests to tests/server/; ruff config scoped to server + tests
- README + web/README updated to drop server/ references
- ontoloviz-server now accepts --host/--port/--workers/--reload/--proxy-headers/
  --log-level (with ONTOLOVIZ_* env-var fallbacks). Defaults stay dev-safe
  (127.0.0.1, single worker, no reload). `--dev` re-enables hot reload for
  the Makefile's dev-server target.
- CORS origins read from ONTOLOVIZ_CORS_ORIGINS; defaults to the Vite dev
  ports. Same-origin prod deployments leave the middleware dormant.
- Multi-stage Dockerfile builds the web frontend, assembles the wheel, and
  installs it into a slim python:3.12 runtime. Non-root user, exposed :8000,
  /api/health healthcheck, prod-tuned ONTOLOVIZ_* defaults.
- .env.example documents every server env var.
- README gains a "Production launch" section with both bare-metal and
  container recipes.
- move 5 TSVs (atc_template, mesh_template, the 3 examples) from repo-root
  templates/ to web/public/templates/ — Vite bundles them into the SPA build,
  setuptools ships them in the wheel under web_dist/templates/, all served
  from /templates/*.tsv at runtime
- drop the two pre-rendered Plotly demos (atc_example.html, mesh_example.html);
  the live SPA renders the equivalent and the static HTML exports were 13 MB
  combined — bloat with no upside
- drop README rows for two custom_template_*.tsv files that never existed
  in the repo (broken raw.githubusercontent.com links)
- LandingPage now surfaces all three example datasets (experimental + trial
  summary ATC, PubMed-mapped MeSH) plus the two empty templates
- README "Templates and Examples" links rewritten to point at
  web/public/templates/ on master; one-line note documents the in-wheel path

Wheel grows from 794 KB → 1.5 MB.
- OBO Foundry: full-width card with accent left-bar (live-fetch action)
- Templates: demoted to inline filename links under primary CTA
- Examples: 3-up cards with per-ontology sunburst thumbnails (ATC / MeSH)
- Restore visible separators using text-subtle (footer + template links)
- Uniform left-align across example cards regardless of description length
- OboFoundryPicker: equal-height tiles, line-clamped descriptions, truncated URL
- Canonical source at branding/ (logo.svg, logo.png 256px, logo.ico
  with 16/32/48/64/128/256 sizes); generated from rsvg-convert + magick.
- Consumer locations symlinked back to branding/ so there is a single
  source of truth: web/public/logo.{svg,png}, src/ontoloviz/assets/
  logo.{svg,png}, and the root-level logo.ico used by ontoloviz.spec.
- Web: favicon + apple-touch-icon links in index.html; landing page
  crosshair replaced with the new mark.
- Desktop: Tk window sets iconphoto from assets/logo.png on startup.
- New SunburstTile reuses layoutSunburst + renderSunburst with a depth cap
  for legible previews; no breadcrumbs/tooltip, click drills into detail
- New OverviewGrid renders one tile per subtree in a responsive grid
- Store gains viewMode ("overview" | "detail"); multi-subtree ontologies
  default to overview, single-subtree skip straight to detail
- Header gets an Overview/Detail segmented toggle (hidden when only one
  subtree exists)
- layoutSunburst takes an optional maxDepth cap, composable with focusId
- parse_obo / _build_ontology accept root_id and min_node_size. With
  root_id set, direct children become subtree roots (level 0, parent="")
  and anything outside the descendant set is dropped, mirroring the
  desktop GUI's HP:0000118 / DOID:4 / CHEBI:23367 / UBERON:0000061
  defaults. Falls back to natural roots with a warning if root_id is
  absent.
- /api/obo/fetch gains rootId / minNodeSize query params; /api/obo/parse
  reads them from the body. OboPreset + fetchObo + OboFoundryPicker
  thread the overrides through.
- HPO preset now produces ~22 phenotype-system subtrees instead of one
  giant HP:0000001 sunburst.
- /api/obo/fetch memoises results in an in-process LRU+TTL cache (8
  entries, 24h) keyed on (url, root_id, min_node_size). Cap raised
  50 -> 150 MB so chebi_lite (~53 MB) fits; full chebi.obo (~260 MB)
  remains rejected — we don't visualize the structure table.
- ChEBI preset switched to chebi/chebi_lite.obo, matching desktop.
- Landing background: replace tiled network/dot pattern with a single
  centered sunburst (rings + radial dividers) that reflects the actual
  visualization. Quieter opacity (0.05 / 0.18) so it sits behind copy.
- Tests: 6 new (root_id split, missing-root fallback, min_node_size,
  parse body alias, fetch query param, cache hit, cache key isolation);
  autouse fixture resets the cache between tests.
- Compute the full partition once per subtree and project it through a
  moving FocusFrame instead of re-partitioning on every focus change.
- Tween between focus frames over 450ms with ease-out-cubic, driven by
  rAF; respects prefers-reduced-motion (snaps instead of animating).
- Hit-test against the most recently projected layout so hover/click
  track what is actually drawn mid-tween.
- Clicking the focused slice now zooms one level up to its parent (no-op
  at the root). The breadcrumb still covers any-ancestor and full reset.
Matthias added 28 commits May 26, 2026 14:18
Adds a TSV exporter that mirrors the legacy desktop app headers
(parent-based and dot-separated formats) so files round-trip through
the existing parser. The ExportMenu now accepts an optional ontology
prop and surfaces the TSV option whenever a full ontology is loaded,
including overview mode. Includes test coverage for both formats and
the template-mode reset of counts/colors.
Replaces the thin top progress bar with a rotating conic-gradient
arc plus pulsing glow around the chart panel while propagation is
recomputing. The ring lives in a new .recomputing-ring utility class
and animates its angle through a registered @Property custom value,
falling back to the always-on box-shadow pulse when @Property isn't
supported. Drives off the existing isRecomputing flag wired through
LoadedView.
Passes the propagated ontology through Header into ExportMenu so the
new OntoloViz-compatible TSV export is reachable on both detail and
overview views. Also restructures the row-collection logic into
per-format helpers with deterministic ordering, adds PNG scale options
to the export menu, and extends test coverage.
- Inline JS runtime renders sunbursts client-side: click a slice to
  re-partition with it as the new root; click the focused slice or a
  breadcrumb to zoom out.
- Overview HTML wraps each tile as a clickable group; clicking drills
  into a full-screen interactive sunburst with a back button.
- Custom HTML tooltip (id, label, count, description) replaces the
  native SVG <title> hover.
- Theme follows the app: ExportMenu snapshots useTheme() and stamps
  data-theme on <html>; runtime CSS variables drive both palettes.
- Stage letterboxes to fit viewport; overview grid only constrains
  width and scrolls vertically for large ontologies.
- Detail stage gets a framed border + soft shadow in both themes.
- New right-pane Export panel (PNG/SVG only) that takes over the main area:
  large live SVG preview + control rail with presets, scope, dimensions,
  theme overrides, per-element label styles, and burned-in title/caption.
- Presets: Publication (light, serif), Presentation (dark, sans), Web
  (mirrors the live app theme via webThemeFor + useTheme).
- ExportTheme threaded through svg.ts, png.ts, overview.ts, and render.ts;
  legacy HtmlTheme renamed so the static theme name is free.
- Overview rendering gains label-position (above/below/overlay), tileBorder
  opt-in, per-label font size / bold / alignment, dynamic label-band layout,
  and proportional slice strokes that scale with figure size.
- render.ts: background:null now preserves existing canvas pixels so the
  overview-tile bg paint isn't cleared to transparent (fixes black PNG
  interiors); strokeWidth is configurable.
- ExportMenu dropdown keeps quick HTML/TSV exports and gains an
  "Export…" entry that opens the workspace.
- useExportConfig zustand slice persists last-used config (version 3).
- Export test suite extended with light-theme, font, label-position, and
  caption burn-in cases.
- ExportPanel: lock scope to active view (overview vs detail), drop scope chip
- ExportMenu: hide subtree image rows in overview, overview image rows in detail; keep TSV in both
- Rename dropdown entry to "Customize export… · presets, dimensions, …" so the path to the figure workspace is explicit
- give each overview label (id/count/name) its own above/below/overlay
  slot via `labelPositions`, replacing the single shared `labelPosition`
- let overview exports omit selected subtrees via `excludedRootIds`
- defer config/theme/ontology inputs to the export preview so the
  control rail stays responsive; show the recompute ring on the figure
  wrapper while the deferred SVG catches up
- drop unused `format` field from ExportConfig; downloads still take
  the format directly from the clicked button
Reshape the count/color propagation controls ported from the tkinter
GUI into task-oriented language; store keys are unchanged.

- Fold the two 'Enabled' toggles into their mode pickers ('off' is now
  the first choice), removing the separate enable/mode indirection.
- Replace jargon with plain labels (Counts/Color, 'Roll children into
  parents', 'By count - per subtree', 'Neutral color', etc.) and give
  each choice a one-line explanation of its effect.
- Bound the level sliders to the loaded ontology's real depth with an
  'n / max' readout, and add a live sentence naming the affected levels
  as the thumb moves (roll-up ceiling and color-from-depth).
- Hide 'Outermost nodes only' for non-MeSH data and the color scale
  editor when colors come from the file.
Add a 'Ring thickness' settings section with one slider per ring (0.25x-3x
of the uniform baseline), letting figure authors thin a busy outer layer or
emphasize a structural one. Purely visual: the angular (count) encoding is
untouched, so quantitative meaning is preserved.

- layout: layoutSunburst takes optional ringWeights, remapping each ring's
  radial band by cumulative weight. Omitted/empty is byte-identical to before.
- store: new layout.ringWeights slice with setRingWeight (pads intervening
  rings with the baseline) and resetRingWeights.
- wire weights through every draw site for consistency: live sunburst,
  overview tiles, and all static/HTML export paths including the inline
  runtime's client-side re-partition.
- fix pre-existing ColorStopEditor lint errors (React.PointerEvent under the
  new JSX transform) so the repo lints clean.
- cover the remap and store padding/reset logic (14 new tests).
Templates were consolidated into web/public/templates in e81b64f, but the
parity, parity-color, parse tests and the fixture generator still read the
deleted REPO_ROOT/templates dir, causing ENOENT on every parity case.

Verified the consolidation was a pure move (pre/post TSVs share MD5s), so
existing fixtures remain valid; only the lookup path needed updating. Full
web suite now 157/157, tsc clean.
…port

Detail-scope (single-subtree) exports gain optional labels derived live from
the focused root node — id, header (name), and description — each with
visibility, position (above/below/overlay), size, weight, and alignment.

- Extract shared label-band geometry into lib/export/labelBands.ts so the SVG
  and canvas renderers agree on band reservation and line placement.
- Add SubtreeLabel{Flags,Positions,Styles} types + defaults (all off, so
  existing subtree exports are unchanged until opted in); bump persisted
  export-config version 6 -> 7.
- Make the panel's Labels section scope-aware: overview keeps id/count/name,
  detail gets id/header/description with full style controls. Removes the
  previously dead detail-scope toggles that the renderer never consumed.
- Cover the new geometry with unit tests and add layoutToSvg label-rendering
  integration tests.
Give deep ontologies a single 'fatten center ↔ fatten edge' knob instead
of requiring a per-ring slider drag for every layer. Because the sunburst
normalizes ring bands by cumulative weight, only the inner→outer gradient is
visible — so one taper value captures the meaningful bulk adjustment.

- taper: new pure taperRamp(t, ringCount) maps t∈[-1,1] to a centered linear
  weight ramp; t=0 is byte-identical to uniform, ±1 lands rings at 1±0.75
  (inside slider bounds, no clamping), middle ring stays on baseline.
- store: setRingWeights bulk action replaces the whole weight array in one
  update (stores a copy); exports already read ringWeights, so no export wiring
  changed.
- settings: TaperSlider above the per-ring list, shown only for ontologies with
  >=4 rings. Commits on release like the per-ring sliders; its knob is local UI
  state that re-centers whenever rings return to uniform (reset / new ontology).
- header: swap the placeholder square brand mark for the /logo.svg image.
- tests: 9 taperRamp cases (direction, symmetry, bounds, middle-ring invariance,
  clamping, degenerate counts) + 2 setRingWeights cases.
Ontology handoff:
- POST /api/ontology stores a pushed ontology under a 128-bit capability
  token (in-process TTL+LRU); GET /api/ontology/{id} retrieves it or 404s.
- Frontend resolves ?session=<id> through the standard render pipeline and
  strips the token from the URL after load.

Deployment tooling:
- Wheel-based install-service.sh / update-service.sh (systemd, single-worker,
  hardened unit, idempotent, post-deploy SPA healthcheck); host needs no Node.

Reverse-proxy sub-path support:
- Configurable Vite base (VITE_BASE) + withBase() helper routes all asset,
  /api, logo and template URLs base-relative so the SPA works under a prefix
  (e.g. /ontoloviz/). Backend unchanged — the proxy strips the prefix.

Config hygiene:
- All deployment values via ONTOLOVIZ_* env with conventional defaults
  (127.0.0.1:8000, base /); no hardcoded ports or hostnames in source.

Docs & tests:
- docs/RUNBOOK.md, docs/CONTRIBUTING.md, docs/ontology-handoff.md (+ example
  payload); README pointers. Server endpoint + frontend fetchSession suites.

Bump version to 3.0.1.
- update-service.sh --build: preflight node/pnpm/uv, then run
  'pnpm install --frozen-lockfile' before make build so updates work on a
  cold checkout and after dependency bumps.
- vite.config: read VITE_BASE / dev port / API target via loadEnv, so a
  gitignored web/.env.production.local persists the sub-path base across
  rebuilds (no need to re-pass VITE_BASE; never committed).
- install-service.sh: 'no wheel' error now documents the build-on-host flow.
- RUNBOOK: add a build-on-prod section (one-time toolchain + persisted base).
- Default Vite base to relative ('./') so one bundle serves at the root or
  any reverse-proxy sub-path (e.g. /ontoloviz/) with no build-time config;
  VITE_BASE now only forces an absolute base (CDN).
- Route the session fetch through withBase and assert it in tests.
- Approve esbuild's postinstall via pnpm.onlyBuiltDependencies so prod's
  non-interactive 'make build' (pnpm 11) no longer fails on ERR_PNPM_IGNORED_BUILDS.
- Drop the web/.env.production.local sub-path dance from RUNBOOK, handoff
  docs, install-service.sh, and update-service.sh; add web/.env.example.
pnpm 10+ no longer reads the package.json "pnpm" field, so the esbuild
build-script approval added earlier never took effect and prod kept hitting
ERR_PNPM_IGNORED_BUILDS. Move onlyBuiltDependencies to its documented home in
pnpm-workspace.yaml and drop the dead package.json field.
pnpm 11 removed onlyBuiltDependencies in favor of an allowBuilds map and
defaults strictDepBuilds=true, so the prior key was ignored and esbuild's
blocked build script made `pnpm install` exit 1 -- which in turn failed
`pnpm build`'s pre-run dependency check. Add allowBuilds: { esbuild: true }
for pnpm 11 and keep onlyBuiltDependencies for pnpm 10 dev hosts.
Confirmed on prod: pnpm 11 turned ERR_PNPM_IGNORED_BUILDS into a fatal
exit-1 that killed `pnpm build`'s pre-run dependency check. The earlier
allowBuilds/onlyBuiltDependencies keys were inert because the cached
"Already up to date" install never re-evaluated them.

Set strictDepBuilds=false so an unreviewed esbuild build script is a warning,
not a hard failure -- Vite resolves the @esbuild/<platform> binary directly,
so esbuild's postinstall is not needed to produce the bundle. Keep allowBuilds
(pnpm 11) and onlyBuiltDependencies (pnpm 10) so clean installs still approve
esbuild outright.
Under sudo, python3 resolves to root's /usr/bin/python3.13, whose ensurepip
lives in the separate python3-venv package -- so `python3 -m venv` failed on
prod even though the user's interactive python could create venvs.

Create the venv with `uv venv --seed` when uv is available: uv carries its
own pip wheels (no system ensurepip needed) and --seed keeps pip inside the
venv so update-service.sh's `pip install` path is unchanged. Resolve uv by
absolute path because sudo strips the login PATH, and fall back to
`python3 -m venv` on hosts that have python3-venv but no uv.
Behind a containerized reverse proxy (nginx-in-Docker via host.docker.internal),
a 127.0.0.1 bind is unreachable and 502s. Default ONTOLOVIZ_HOST to 0.0.0.0; set
127.0.0.1 explicitly for host-local-only deploys.

- install-service.sh: reconcile explicitly-passed HOST/PORT/PROXY_HEADERS into an
  existing /etc/ontoloviz.env. The file was write-once, so re-run overrides were
  silently ignored — the trap that stranded the service on 127.0.0.1:8000.
  Operator customizations and defaults-only re-runs are left untouched.
- update-service.sh: warn when bound to 127.0.0.1, since the host-local
  healthcheck passes even when the proxy cannot reach the service.
Bare `./update-service.sh` now does git pull + pnpm install + make build +
install + restart (previously default was wheel-only). Drop the `--build`
flag; pass a wheel path or `--wheel` to skip the build on Node-less hosts.

- flip mode selection: build is default, wheel mode is the opt-in
- update header usage, toolchain-preflight and no-wheel error messages
- sync docs/RUNBOOK.md update/rollback section
@Mnikley Mnikley merged commit 077217e into master Jun 1, 2026
7 of 8 checks passed
@Mnikley Mnikley deleted the feat/web-v2-scaffold branch June 1, 2026 13:31
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.

1 participant