Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
18294be
repo: prefer jj for VCS, ignore TODO.txt
passcod Apr 29, 2026
95d78c8
refactor: rename project branding strings from Tamanu Meta to Canopy
passcod Apr 29, 2026
b61fbc9
doc: rename project to Canopy in docs and CI config
passcod Apr 29, 2026
ad5cfe9
ci: rename test postgres role/db from meta to canopy
passcod Apr 29, 2026
998d7b6
refactor!: rename ServerKind::Meta to Canopy and MetaTicket to Canopy…
passcod Apr 29, 2026
0032a22
plan: private-web frontend migration
passcod Apr 29, 2026
f6bd69a
plan: note that dev auth bypass already exists via debug_assertions
passcod Apr 29, 2026
1e61b83
refactor: switch admin server fns to JSON input
passcod Apr 29, 2026
f2eebad
refactor: switch commons server fns to JSON input
passcod Apr 29, 2026
aae516d
refactor: switch bestool server fns to JSON input
passcod Apr 29, 2026
e3c2255
refactor: switch sql server fns to JSON input
passcod Apr 29, 2026
894aceb
refactor: switch servers server fns to JSON input
passcod Apr 29, 2026
baff1a3
refactor: switch device server fns to JSON input
passcod Apr 29, 2026
6050a22
plan: target React 19 and avoid writing our own emotion code
passcod Apr 29, 2026
0e36ea7
refactor: switch status server fns to JSON input
passcod Apr 29, 2026
21d4fa0
refactor: switch version server fns to JSON input
passcod Apr 30, 2026
7d1b0eb
doc: update test guideline for JSON server fn inputs
passcod Apr 30, 2026
0c14b4a
feat: scaffold private-web React + MUI + Vite frontend
passcod Apr 30, 2026
decd52a
feat: just recipes + AGENTS doc for private-web dev workflow
passcod Apr 30, 2026
b9ef057
feat: status page in private-web
passcod Apr 30, 2026
70db32d
feat: Phase 3.5 Playwright + switch tooling to npm
passcod Apr 30, 2026
1ecb9bb
feat: admins page in private-web
passcod Apr 30, 2026
c658318
feat: versions list page in private-web
passcod Apr 30, 2026
3aadcc9
feat: versions detail page in private-web (read-only)
passcod Apr 30, 2026
05bd5d9
feat: admin actions on version detail page
passcod Apr 30, 2026
24fc86c
feat: artifact CRUD on version detail page
passcod Apr 30, 2026
3ff50a9
feat: servers list page in private-web
passcod Apr 30, 2026
a5252c5
feat: server detail page in private-web (read-only)
passcod Apr 30, 2026
f1619f5
feat: server edit page in private-web
passcod Apr 30, 2026
13de967
feat: devices index, list, and search pages in private-web
passcod Apr 30, 2026
4abc0ae
feat: device detail page in private-web
passcod Apr 30, 2026
69cd3c0
feat: bestool snippets pages in private-web
passcod Apr 30, 2026
84a0298
feat: SQL playground page in private-web
passcod Apr 30, 2026
1850626
feat: TimeAgo component, used for relative timestamps
passcod Apr 30, 2026
fa8ce31
style: theme primary=leaf-green, secondary=sky-blue from original fav…
passcod Apr 30, 2026
13749f5
feat: bring favicon over from the Leptos static dir, with yellow swap…
passcod Apr 30, 2026
b1c127e
style: refresh buttons icon-only on the device detail page
passcod Apr 30, 2026
4d21bec
fix: prevent the Add admin button from wrapping on narrow widths
passcod Apr 30, 2026
c0f0cd1
style: make device list rows fully clickable, not just the name
passcod Apr 30, 2026
37e5722
style: add vertical margin between minor-version rows on the versions…
passcod Apr 30, 2026
c117faa
style: drop the Range column on the artifacts table
passcod Apr 30, 2026
8e7dd5a
feat: CodeMirror SQL editor for snippets and the SQL playground
passcod Apr 30, 2026
db8bbc5
style: swap favicon colours so the ring is blue and the plus is green
passcod Apr 30, 2026
ca2c5ed
style: drop list-row hover shadows in favour of a subtle background tint
passcod Apr 30, 2026
995b646
style: add the favicon as a logo in the AppBar, linked to /
passcod Apr 30, 2026
00d68e8
feat: per-page document title with interpunct, e.g. 'Status · Canopy'
passcod Apr 30, 2026
4e712b4
fix: drop the resting + expanded box-shadow on the versions accordion
passcod Apr 30, 2026
4546c37
style: shrink the server-title status dots to 0.8em
passcod Apr 30, 2026
b904efc
refactor!: drop Leptos, embed React SPA, simplify URL prefix
passcod Apr 30, 2026
3b02950
chore: prune deps + dead CSS now that Leptos is gone
passcod Apr 30, 2026
95d690e
ci: build private-web as part of build-binaries, drop the separate fr…
passcod Apr 30, 2026
41135a3
unplan
passcod Apr 30, 2026
b22f151
refactor: prune unused server-fn endpoints
passcod Apr 30, 2026
c9d5ded
style: pretty-print version-range comparators with Unicode in artifac…
passcod Apr 30, 2026
2aa2281
refactor: drop Arc<T> from server-fn response types
passcod Apr 30, 2026
e714b51
refactor: collapse bestool.create_snippet + update_snippet into save_…
passcod Apr 30, 2026
32a8165
refactor: standardise pagination args to { offset, limit? } across fns
passcod Apr 30, 2026
c40b8ad
refactor: collapse count + list pairs into a paginated Page<T> response
passcod Apr 30, 2026
a90d2fd
ci(dev): split watch-private into build + api recipes
passcod May 1, 2026
704add2
clarify favicon elements
passcod May 1, 2026
a65355c
style: lengthen the favicon cross arms
passcod May 1, 2026
be24ab3
style: rewrite favicon with primitive shapes
passcod May 1, 2026
cdf3b26
it's a tree on an island seen from above
passcod May 1, 2026
ae87fbe
various ui fixes
passcod May 1, 2026
f9eb081
fix indicator colours
passcod May 1, 2026
8920309
feat: replace Bulma with hand-rolled public.css
passcod May 1, 2026
ef5391d
chore: drop Bulma submodule + unused Leptos-era CSS
passcod May 1, 2026
1cf88eb
chore: drop remaining Leptos-era references
passcod May 1, 2026
57e58a6
docs: drop "post-Leptos" qualifier from AGENTS.md heading
passcod May 1, 2026
de5645a
test(private-web): self-host private-server + Vite from the e2e fixture
passcod May 1, 2026
61598d3
ci: add Playwright job for private-web e2e
passcod May 1, 2026
8d0a8c0
chore(deps): bump the deps group across 1 directory with 12 updates
dependabot[bot] May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/Dockerfile.native
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
fi && \
rm -rf /tmp/bins

# Copy frontend assets (same for all platforms)
COPY --chown=tamanu:tamanu frontend/target/site /home/tamanu/target/site
COPY --chown=tamanu:tamanu frontend/static /home/tamanu/static
# Public-server's Tera templates link to /static/public.css and /static/images/...
# (private-server has its React bundle embedded in the binary).
COPY --chown=tamanu:tamanu static /home/tamanu/static

USER tamanu
ENV HOME=/home/tamanu
Expand Down
70 changes: 9 additions & 61 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ permissions:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
LEPTOS_OUTPUT_NAME: private-server
SERVER_FN_MOD_PATH: "true"
DISABLE_SERVER_FN_HASH: "true"

jobs:
build-binaries:
Expand All @@ -37,8 +34,6 @@ jobs:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v6
with:
submodules: recursive

- name: Configure toolchain
run: |
Expand All @@ -54,6 +49,8 @@ jobs:
with:
key: ${{ matrix.arch }}-release

# The runner image ships with Node + npm; private-server's build.rs
# uses them to produce the embedded private-web/dist/ bundle.
- name: Build binaries
run: just build-servers-release ${{ matrix.target }}
env:
Expand All @@ -70,55 +67,15 @@ jobs:
path: artifacts/${{ matrix.arch }}/
retention-days: 1

build-frontend:
name: Build Leptos frontend
runs-on: ubuntu-24.04
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v6
with:
submodules: recursive

- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
rustup target add wasm32-unknown-unknown

- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq

- uses: taiki-e/install-action@v2
with:
tool: just,cargo-leptos

- uses: Swatinem/rust-cache@v2
with:
key: frontend-release

- name: Build frontend
run: just build-frontend-release

- uses: actions/upload-artifact@v5
with:
name: frontend
path: |
target/site/
static/
retention-days: 1

build-images:
name: Build container images
runs-on: ubuntu-24.04
needs: [build-binaries, build-frontend]
needs: [build-binaries]
steps:
- name: Login to ghcr.io
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin

- uses: actions/checkout@v6
with:
submodules: recursive

- name: Extract version
id: version
Expand All @@ -136,39 +93,30 @@ jobs:
name: binaries-arm64
path: .github/build-context/arm64/

- name: Download frontend
uses: actions/download-artifact@v6
with:
name: frontend
path: .github/build-context/frontend/
# Stage the public-server's static assets into the build context.
- name: Stage static assets
run: cp -r static .github/build-context/static

- name: Debug build context structure
run: |
echo "=== Build context structure ==="
ls -laR .github/build-context/
echo "=== AMD64 binaries ==="
ls -lh .github/build-context/amd64/
echo "=== ARM64 binaries ==="
ls -lh .github/build-context/arm64/
echo "=== Frontend assets ==="
ls -lh .github/build-context/frontend/

- name: Setup buildkit
uses: docker/setup-buildx-action@v3

- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/beyondessential/tamanu-meta
images: ghcr.io/beyondessential/canopy
tags: |
type=semver,value=v${{ steps.version.outputs.version }},pattern={{version}}
type=semver,value=v${{ steps.version.outputs.version }},pattern={{major}}.{{minor}}
type=semver,value=v${{ steps.version.outputs.version }},pattern={{major}}
labels: |
org.opencontainers.image.vendor=BES
org.opencontainers.image.title=Tamanu Meta Server
org.opencontainers.image.title=Canopy
org.opencontainers.image.url=https://www.bes.au/products/tamanu/
org.opencontainers.image.source=https://github.com/beyondessential/tamanu-meta-server/
org.opencontainers.image.source=https://github.com/beyondessential/canopy/
org.opencontainers.image.version=${{ steps.version.outputs.version }}
org.opencontainers.image.licenses=GPL-3.0-or-later

Expand Down
76 changes: 66 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ concurrency:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
LEPTOS_OUTPUT_NAME: private-server
SERVER_FN_MOD_PATH: "true"
DISABLE_SERVER_FN_HASH: "true"

jobs:
test:
Expand All @@ -26,8 +23,6 @@ jobs:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
Expand All @@ -37,12 +32,12 @@ jobs:
tool: cargo-nextest
- run: |
sudo systemctl start postgresql.service
sudo -u postgres psql --command="CREATE ROLE meta SUPERUSER LOGIN PASSWORD 'meta'"
sudo -u postgres createdb --owner=meta meta
sudo -u postgres psql --command="CREATE ROLE canopy SUPERUSER LOGIN PASSWORD 'canopy'"
sudo -u postgres createdb --owner=canopy canopy
- uses: Swatinem/rust-cache@v2
- run: cargo nextest run
env:
DATABASE_URL: postgresql://meta:meta@localhost:5432/meta
DATABASE_URL: postgresql://canopy:canopy@localhost:5432/canopy

clippy:
name: Clippy
Expand All @@ -51,8 +46,6 @@ jobs:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
Expand All @@ -63,3 +56,66 @@ jobs:
tool: just
- uses: Swatinem/rust-cache@v2
- run: just lint

e2e:
name: Playwright
runs-on: ubuntu-latest
# The fixture (private-web/e2e/fixture.ts) spawns the private-server
# binary plus a Vite dev server itself, so this job builds the rust
# binaries the fixture expects, installs the frontend deps + chromium,
# and runs `npm run test:e2e`.
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
cache-dependency-path: private-web/package-lock.json
- name: Start Postgres
# Mirrors the canopy/canopy role + admin DB used by the rust test job.
run: |
sudo systemctl start postgresql.service
sudo -u postgres psql --command="CREATE ROLE canopy SUPERUSER LOGIN PASSWORD 'canopy'"
sudo -u postgres createdb --owner=canopy canopy
- name: Build private-server + migrate
# SKIP_FRONTEND_BUILD avoids private-server/build.rs running an npm
# install + vite build to embed dist/ — the e2e fixture uses Vite
# at runtime so the embedded bundle isn't needed here.
env:
SKIP_FRONTEND_BUILD: "1"
run: cargo build --bin private-server --bin migrate --locked
- name: npm ci
run: npm ci
working-directory: private-web
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('private-web/package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
working-directory: private-web
- name: Playwright tests
run: npm run test:e2e
working-directory: private-web
env:
CANOPY_E2E_ADMIN_DATABASE_URL: postgresql://canopy:canopy@localhost:5432/postgres
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
private-web/playwright-report
private-web/test-results
retention-days: 7
if-no-files-found: ignore
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ Rocket.toml
*.crt
*.key
identity.*.pem
/static/bulma
TODO.txt
/private-web/node_modules
/private-web/dist
/private-web/test-results
/private-web/playwright-report
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

73 changes: 37 additions & 36 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Agent Rules for tamanu-meta-server
# Agent Rules for canopy

Avoid writing large summaries of actions taken when done.

## Project Structure Overview
- **Database crate**: Models, migrations, and database logic
- **Public server**: Internet-exposed API endpoints for device registration and updates
- **Private server**: Admin web interface using Leptos (SSR + hydration)
- **Private server**: Admin HTTP/JSON API in axum, plus an embedded React SPA (in `private-web/`) served at the root in production
- **Commons**: Shared utilities, authentication, error handling

## Database Connection
Expand All @@ -23,31 +23,27 @@ Avoid writing large summaries of actions taken when done.
- Use `commons_tests::db::TestDb::run()` for database-only tests
- Use `commons_tests::server::run()` for HTTP endpoint tests
- Use `commons_tests::server::run_with_device_auth()` for authenticated device tests in the public server
- Admin pages: Restrict frontend with `is_current_user_admin()` resource and use `admin_guard()` in server functions
- Admin endpoints take a `TailscaleAdmin` axum extractor; the React UI gates with `commons.is_current_user_admin`

## Leptos Private Server Architecture
- **Frontend/Backend Separation**: Code must build in both SSR and hydrate modes
- **SSR Pattern**: Put database/backend imports in `#[cfg(feature = "ssr")]` sections, following `fns/statuses.rs` structure
- **Server Functions**: Use `#[server]` functions that delegate to `ssr::` module functions
- **Data Structures**: Create separate serializable structs for frontend (no direct database types)
- **Admin Guard**: Use `crate::fns::commons::admin_guard()` for admin authentication
- **Page Structure**: Follow pattern in `app/statuses.rs` and `app/admins.rs`
## Private server architecture
- **Server fns** under `crates/private-server/src/fns/<module>.rs` are bare axum handlers with `(State, [auth extractor], Json<Args>) -> Result<Json<T>>` signatures.
- Each module exposes `pub fn routes() -> Router<AppState>` and is mounted under `/api/<module>` by `crate::fns::routes()`.
- The SPA fallback (`crate::spa::handler`) serves the embedded React bundle from `private-web/dist/` for any path the API doesn't claim.
- `build.rs` runs `npm install --frozen-lockfile && npm run build` in `private-web/` before embedding. Set `SKIP_FRONTEND_BUILD=1` to skip (`just`'s recipes already do this for dev workflows).

Example Leptos server function pattern:
Example axum handler pattern:
```rust
#[server]
pub async fn my_function() -> Result<MyData> {
ssr::my_function().await
}

#[cfg(feature = "ssr")]
mod ssr {
use database::MyModel; // SSR-only imports here
#[derive(Deserialize)]
pub struct AddArgs { pub email: String }

pub async fn my_function() -> Result<MyData> {
let db = crate::fns::commons::admin_guard().await?;
// Implementation here
}
pub async fn add(
State(state): State<AppState>,
TailscaleAdmin(_): TailscaleAdmin,
Json(args): Json<AddArgs>,
) -> Result<Json<()>> {
let mut conn = state.db.get().await?;
database::admins::Admin::add(&mut conn, &args.email).await?;
Ok(Json(()))
}
```

Expand All @@ -62,27 +58,32 @@ mod ssr {
- For database tests, use direct model functions instead of HTTP endpoints
- Always include `use database::ModelName;` imports in test files
- Do not include `_test` suffix or prefix in test filenames in `tests/` directory
- Calling Leptos Server Functions in private-server tests:
- Server functions are exposed at `/api/private_server/fns/<module>/<function>` (e.g., `/api/private_server/fns/statuses/server_ids`)
- Use `.form(&[("param_name", "param_value")])` for parameters (not `.json()`)
- Use `.post()` without body for functions with no parameters
- Calling private-server endpoints in tests:
- Endpoints are at `/api/<module>/<function>` (e.g. `/api/statuses/server_grouped_ids`)
- Pass parameters via `.json(&serde_json::json!({"param_name": value}))`
- For functions with no parameters, still send an empty body: `.json(&serde_json::json!({}))`

## React frontend (`private-web/`)
The React + MUI + Vite frontend lives at `/private-web/` and is embedded into the private-server binary at build time via `rust-embed`.

## Frontend Development
- Pages go in `crates/private-server/src/app/`
- Server functions go in `crates/private-server/src/fns/`
- Common components go in `crates/private-server/src/components/`
- CSS files go in `static/private/`
- Add routes in `crates/private-server/src/app.rs`
- Use Leptos signals for state management
- Handle async operations with `Resource` and `Action`
Local dev workflow (two terminals):
- `just watch-private-api` runs the private-server binary on `127.0.0.1:8081`. (We bind to IPv4 because Node's vite-proxy can't resolve `[::1]` literals.) `SKIP_FRONTEND_BUILD=1` is set so `cargo run` doesn't reinvoke `npm run build` on every iteration.
- `just watch-private-web` runs Vite at `:8090`, proxying `/api` to the API.

Open `http://localhost:8090/`. The Vite proxy makes the React app same-origin with the API, so no CORS plumbing is needed.

End-to-end tests use Playwright. Run with `npm run test:e2e` from `/private-web/`. The fixture (`e2e/fixture.ts`) spawns its own private-server + Vite pair against a freshly-migrated `canopy_e2e_<random>` Postgres database per worker, so the operator does not need to keep `just watch-private-api` running. Build the binaries first with `cargo build --bin private-server --bin migrate`. Override the admin connection used to create/drop the throwaway DB with `CANOPY_E2E_ADMIN_DATABASE_URL` (default `postgres://localhost/postgres`); set `CANOPY_E2E_VERBOSE=1` to stream backend/frontend logs. The first run on a fresh checkout needs `npx playwright install chromium`.

## Development Workflow
- Always check: `just check` for basic compilation
- Always check: `just build-frontend` for frontend compatibility
- Run full test suite: `just test`
- Run specific tests: `just test-name <test_name>`
- Verify no compilation warnings in tests and main code

## Version Control
- If the working copy is a jujutsu repo (a `.jj` directory exists at the repo root), prefer `jj` commands over `git` for VCS operations (status, diff, log, commit/describe, etc.). The repo may be colocated with git, but `jj` is the source of truth for local work.
- If there is no `.jj` directory, use `git` as normal.

## Troubleshooting and common mistakes
- Always use just for tests, never use `cargo test`.
- Unless you've done wide-ranging changes, prefer to test specific packages with `just test-package <package_name>` instead of the full test suite.
Expand Down
2 changes: 1 addition & 1 deletion COPYRIGHT
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Short version for non-lawyers:

Tamanu Meta Server is licensed under GPL 3.0 and later.
Canopy is licensed under GPL 3.0 and later.
Commercial licensing is available via BES.


Expand Down
Loading
Loading