Skip to content

Rename to Canopy, move the UI to React#111

Merged
passcod merged 78 commits intomainfrom
react
May 5, 2026
Merged

Rename to Canopy, move the UI to React#111
passcod merged 78 commits intomainfrom
react

Conversation

@passcod
Copy link
Copy Markdown
Member

@passcod passcod commented May 1, 2026

This is 80 commits because I tell AI to commit gradually but this was super fast, the bulk of the work was done in a couple hours in the background, and then I asked for a bunch of tweaks.

Before:
image

After:
image

(keeping in mind the colours are still there, the DB is just outdated so everything is "off")

The icon change's idea is "a tree on an island in the ocean, seen from the sky", which is in keeping with the "overwatch" aspect of Canopy.

passcod added 30 commits April 29, 2026 23:09
…Ticket

Rust enum variant ServerKind::Meta -> ServerKind::Canopy, and the
on-disk/serialized string changes from "meta" to "canopy". MetaTicket
is renamed to CanopyTicket throughout. Migration updates the seeded nil
server row from kind='meta', name='Meta Server' to kind='canopy',
name='Canopy'.
server_grouped_ids takes no arguments; tests now post {} as the body
since with input = Json the macro deserialises an empty object.
New TypeScript SPA at private-web/, dev-only for now (not built into
the container image). Targets React 19 and MUI v9. Vite proxies
/api/private_server to the running Leptos backend so the frontend
can call existing server functions during the migration.

Includes a single Hello route that calls commons.is_current_user_admin
to verify the proxy round-trip end to end.
watch-private-api binds the Leptos server fns to 127.0.0.1:8081 (Node's
vite-proxy can't resolve [::1] literals so we go IPv4 in dev).
watch-private-web runs Vite on :8090 with the API proxy.

Round-trip verified: a POST to localhost:8090/api/private_server/fns/...
forwards to the backend and returns 200.
First migrated page: status dashboard with release summary, server
cards grouped by rank, and version/status legends. Includes an auto-
reload tick (60s, plus visibilitychange recovery and a custom
canopy-reload-status document event for manual reload).

Adds reusable VersionSquare/VersionIndicator/StatusDot components,
Legends, the useReloadInterval hook, and the wire-shape types for
the statuses fns.

App.tsx now has a top nav with Status as the only entry; / redirects
to /status.
Two related changes folded together because the text edits overlap:

1. Switch private-web from pnpm to npm. The earlier scaffold and dev-
   workflow commits assumed pnpm because I (incorrectly) inferred it
   from a sibling project. Replace pnpm-lock.yaml with package-lock.json,
   update the justfile recipe, AGENTS.md, and the plan.

2. Add Playwright e2e harness per the plan's new Phase 3.5: tests live
   in private-web/e2e/, run via 'npm run test:e2e', start Vite via
   webServer, and assume the operator already has the API running.
   Includes two smoke tests for the status page (chrome + backend
   round-trip).

Made AppBar 'Canopy' title an h1 so the smoke test can query it by
heading role.
Migrates the admins management page. Shows the current list, supports
adding via the form (with empty/invalid validation) and deleting per
row. Errors render inline; successful adds show a transient snackbar
(replacing the Leptos toast pattern, scoped to the page for now).

Adds /admins to the top nav.

E2e: four playwright tests covering listing seeded admins, the delete
button, the add flow, and the empty-email error path. Tests use
unique emails per run to avoid collisions in the shared dev DB.
Migrates /versions: collapsible accordion grouped by minor release,
each minor expands to show its patch versions linking to the (not
yet migrated) detail page. Drafts and yanked versions are tinted in
the row background and show a status chip.

Adds a VersionStatusChip component (replaces the Leptos
VersionStatusBadge), wire-shape types for the versions module, and
two smoke e2e tests.
Migrates /versions/:version as a read-only view: header with version
number and status chip, info panel (created, last updated, chrome
support), artifacts table, markdown-rendered changelog, and the
related lower patches in the same minor.

Adds a small Markdown component (react-markdown + remark-gfm) for
the changelog rendering, replacing pulldown-cmark + dangerouslySet-
InnerHTML.

Admin actions (status change, changelog edit, artifact CRUD) follow
in subsequent commits.
Adds the admin-gated controls: status select with a Change button (with
the same can't-revert-to-draft-unless-latest-in-minor rule as the
Leptos version), and an Edit button on the Changelog section that
swaps the rendered markdown for a textarea.

Both refetch the version detail on success rather than reloading the
whole page (cleaner than the Leptos behaviour, and quicker).

Adds a useApiAction hook for write-only fns alongside the existing
useApi for reads.
Adds the admin-only artifact create/edit/delete flow with the same
lock-unlock pattern as Leptos: Unlock toggles per-row Edit/Delete
buttons; Create reveals an inline form. Edit swaps the row contents
for input fields. Delete shows a Really-delete confirm step.

E2e: a smoke test for the detail page that tolerates either render
of the version data or an error alert (real DB state often won't
have version rows for a hard-coded test version).
Migrates /servers and /servers/facilities (Central / Facility tabs).
Each tab paginates 10 per page; uses MUI Pagination instead of the
Leptos PaginatedList component.

Adds a ServerShorty React component (replaces the Leptos one). Layout
is a vertical stack of bordered rows showing name, rank chip, kind
chip, and host. Three smoke e2e tests covering tab presence, switch,
and round-trip.
Migrates /servers/:id read-only: header with rank/kind chips, name +
status dots for self and children, optional Edit button (admin), URL
+ Device cards, info panel (last-seen, platform, timezone, version
indicator, postgres/nodejs/chrome versions, mobile listing flag,
parent link, location), child servers list, and the version/status
legends.

Extracts ServerKindChip and ServerRankChip from the existing
ServerShorty so the detail page can reuse them.

Bumps tsconfig.app target/lib to ES2023 for Array.findLast support.

Smoke e2e test that the page mounts on any id and surfaces either the
heading or an alert.

Edit, import, and geo pages follow as separate commits.
Migrates /servers/:id/edit. Form fields: name, host, kind (central/
facility select), rank (production/clone/demo/test/dev/unranked
select), device id, parent server, location (cloud/on-premise/unknown
select), lat/lon, and the 'Available in Tamanu Mobile app' checkbox
for centrals.

Parent server uses MUI Autocomplete with debounced search via
servers.search_parent. Pasting a UUID directly is also possible by
typing it. The cloud-region preset (lat/lon-from-region) is deferred
until the geo page is migrated; for now lat/lon are plain inputs.

On save, navigate to the detail page. Smoke e2e covers mount.
Migrates /devices (Search), /devices/untrusted, and /devices/trusted.
Search exposes a debounced search box hitting devices.search and an
admin-only Import Ticket dialog that mirrors the Leptos modal,
including client-side base64 decoding of the Canopy ticket to extract
kind/rank hints (which lock the corresponding selects).

Adds DeviceShorty React component and the parseCanopyTicket helper.
Three smoke e2e tests covering tabs, search input, and untrusted-tab
round-trip.

Detail and history pages follow next.
passcod added 27 commits May 1, 2026 07:12
- devices.get_device_name_by_id: was for the Leptos breadcrumb, React
  derives the name client-side from get_device_by_id.
- servers.list_all/list_centrals/list_facilities: convenience wrappers
  for list_some that the React side never calls directly.
- versions.get_artifacts_by_version_id: showed every configured artifact
  including ones a public client wouldn't see; the detail page now uses
  versions.get_version_artifacts (the deduplicated public-API view) so
  the admin sees the same as the public.

Also restores commons.public_url and commons.server_versions_url to
the React AppBar — the right-side 'Public' / 'Server Versions' external
links got dropped during the migration and now render again, gated on
the corresponding env var being set on the backend.

Updates the artifacts test to assert deduplication on the new endpoint.
…t captions

>= becomes ≥, <= becomes ≤, != becomes ≠. So 'Applies to: >=2.44.2'
now reads 'Applies to: ≥2.44.2'.
The Arcs were a Leptos-era ergonomic for sharing values across signals
on the SSR side. Axum just serialises whatever's in Json(...), so the
wire shape is identical with or without them — the Arc only added a
clone gate in handler code that we no longer need.

Touches DeviceInfo, ServerDetailData, and the list_some/list_trusted/
list_untrusted/search return types. JSON serialisation is unchanged
(serde transparently serialises Arc<T> as T), so no client change.
…snippet

Both endpoints called BestoolSnippet::create with the same body — only
the supersedes field differed. Single endpoint takes an optional
supersedes uuid; absent for a brand-new snippet, set to the prior id
when editing (which records the new snippet as superseding the old).

Always returns the saved BestoolSnippetDetail. The create flow on
the React side still just refreshes the list; the edit flow uses the
returned id to navigate to the new version's detail page.
Devices was the outlier — it had `{ limit?, offset? }` (both optional,
limit listed first). The rest already used `{ offset, limit? }` with
offset required and limit optional. Switching devices to match.

The React side already always sends both, so no client-side change is
needed. limit defaults stay per-endpoint (10 for device lists, 100 for
connection history).
Introduces a small Page<T> { items, total } wrapper at crate::fns::Page
(and a matching Page<T> in private-web/src/types.ts). Five list endpoints
now embed their own count, removing the separate count fns:

- devices.list_untrusted (drops count_untrusted)
- devices.list_trusted (drops count_trusted)
- bestool.list_snippets (drops count_snippets)
- servers.list_some (drops count_some)
- sql.get_query_history (drops get_query_history_count)

Each was a guaranteed second round-trip from the React side anyway —
both calls fired in parallel from the same component on the same render
— so this is one fewer request per list page. The React useApi calls
become a single fetch with .data.items / .data.total instead of
juggling two states.
- watch-private-build: cargo build --bin private-server, watching crates/
- watch-private-api: runs target/debug/private-server directly, watching
  the binary path so it restarts on a fresh build instead of going
  through cargo run (which doesn't propagate watchexec's signals to
  its grandchild — left zombie servers holding the port)

Both recipes pass -I so watchexec doesn't apply its default ignore
filters.
The ring becomes a single stroked circle (r=40.55, stroke-width=8.9
gives the same outer-45/inner-36.1 it had before). The cross becomes
two overlapping rectangles. Three elements, no path arithmetic, easy
to tweak the next time.
The public-server's tera templates were the only consumer of Bulma and the
Leptos-era /static/private/main.css. The private side has moved to React+MUI
and no longer needs either. Replace both with a single hand-rolled stylesheet
covering only the primitives the templates use.
The Bulma submodule, _copy-bulma justfile recipe, static/bulma/ and
static/private/ directories, and the unreferenced static/bestool.css are no
longer needed: the public-server's templates now ship with a hand-rolled
public.css, and the private-server has its React bundle embedded in the
binary.
cargo-leptos is no longer pulled in by 'just install-deps' (and hasn't been
since the React migration); LEPTOS_OUTPUT_NAME, SERVER_FN_MOD_PATH and
DISABLE_SERVER_FN_HASH are no longer consumed by anything in CI; and
playwright.config.ts described the API server in stale terms.
The Playwright suite no longer assumes 'just watch-private-api' is running.
A worker-scoped fixture spawns target/debug/private-server on a dynamic
port against a freshly-migrated canopy_e2e_<random> database, starts Vite
on its own dynamic port with VITE_PROXY_TARGET pointed at the API, waits
for both, and tears the lot down (incl. dropping the database) when the
worker finishes.

This means each Playwright worker gets a fresh, isolated stack — no port
collisions with the operator's running dev servers, and no shared state
between workers.

Override CANOPY_E2E_ADMIN_DATABASE_URL (default postgres://localhost/postgres)
if your local Postgres uses different credentials. Set CANOPY_E2E_VERBOSE=1
to stream backend/frontend logs through the test runner.
Builds target/debug/{private-server,migrate}, runs npm ci in private-web,
caches the Playwright browser bundle keyed on the lockfile, and runs
npm run test:e2e against the same canopy/canopy Postgres role the rust
test job uses. Uploads the playwright-report on failure.
Was installed for the Leptos browser bundle. The post-React workspace
has no wasm crates.
The crate existed solely to implement tachys::view::{Render, RenderHtml}
on shared types (VersionStr, ServerKind, AppError, etc.). After the React
migration nothing consumes those impls, but they still pulled in tachys,
wasm-bindgen and web-sys for every workspace build.

Drops the seven render_as_string!() invocations across commons-types and
commons-errors, removes the commons-macros workspace member, and drops
the dependency from both consuming crates.
The feature existed for the Leptos era when these types compiled into a
WASM client too. The post-React server-only world enables 'ssr' everywhere
unconditionally, so the gates are dead weight: they obscure what's actually
in scope at any given line.

Inlines all the previously-optional deps as required, drops the seven
#[cfg(feature = "ssr")] gates and 'ssr'-conditional derive attributes
across commons-types/src/{device,geo,server/kind,server/rank,version}.rs
and commons-errors/src/lib.rs, and removes the now-unused features = ["ssr"]
from private-server's commons-* deps.
Workspace bumps include three majors that needed adapting:
  axum-test    18 -> 20  (TestServer::new no longer returns Result)
  diesel-async 0.7 -> 0.9
  reqwest      0.12 -> 0.13  (rustls-tls feature renamed to rustls)
plus a long tail of compatible bumps (axum, clap, diesel, jiff, lloggs,
pulldown-cmark, serde, tera, tokio, tracing) and per-crate non-workspace
deps (axum-client-ip, axum-server-timing, bestool-postgres,
diesel_migrations, hostname, ipnet, qrcode, rcgen, rfc2047-decoder,
rust-embed, serde_json, subtle, thiserror, time, tokio-postgres,
tower-http, tracing-subscriber, url, uuid, x509-parser).
jsdom ^29.0.2 -> ^29.1.1
vite  ^8.0.9  -> ^8.0.10

Both within the existing caret ranges, so 'npm install' is a no-op against
the lockfile. Every other dep was already at latest.
@dannash100
Copy link
Copy Markdown
Contributor

cool icon concept! 🌴

@passcod passcod merged commit aec2606 into main May 5, 2026
4 checks passed
@passcod passcod deleted the react branch May 5, 2026 00:56
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