Conversation
…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.
- 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.
Same behaviour, modern equivalent.
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.
Contributor
|
cool icon concept! 🌴 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:

After:

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