Mokosh (mythological/Slavic): The earth-mother goddess of weaving and women's work. Mascot: a slender figure at a loom under spring rain. Tagline: Weaves it all together.
  • Rust 98%
  • Shell 0.6%
  • Just 0.5%
  • CSS 0.4%
  • Dockerfile 0.3%
  • Other 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
longjacksonle 7312f0819b
All checks were successful
Check / fmt + clippy + tests (push) Successful in 1m43s
Build OCI container / Build and push mokosh-www image (push) Successful in 3m23s
Merge pull request 'Move the Recently sent description into the card header (PMS-765)' (#520) from fix/PMS-765-card-subtitle into main
Reviewed-on: #520
2026-08-12 05:52:14 +02:00
.devcontainer feat: Split from Dioxus PSA 2026-05-03 20:58:31 -04:00
.forgejo/workflows ci(theme): fail the build on a colour nothing defines 2026-08-11 22:24:07 -04:00
.idea refactor: rename mokosh-client to mokosh-apps 2026-05-29 14:50:58 +02:00
assets fix(spa): clean up three CSP + manifest console errors on msp.a8n.systems 2026-07-03 13:38:52 +02:00
docs feat(update): show an app-wide reload banner on a new deploy 2026-08-09 15:00:18 -04:00
oci-build fix(csp): allow the API origin in img-src 2026-08-10 14:19:04 -04:00
scripts ci(theme): fail the build on a colour nothing defines 2026-08-11 22:24:07 -04:00
src test(card): keep a subtitle out of the card body 2026-08-11 23:48:04 -04:00
.dockerignore feat: Dockerized dev server, login bypass, README 2026-05-03 23:28:41 -04:00
.env.example feat: Dockerized dev server, login bypass, README 2026-05-03 23:28:41 -04:00
.gitignore docs: add 2026-06-06 platform audit to dev-docs/audits 2026-06-11 03:31:29 +02:00
build.rs fix(version): declare CARGO_PKG_VERSION as an explicit build rerun trigger 2026-06-16 15:11:51 +02:00
bun.lock feat: Split from Dioxus PSA 2026-05-03 20:58:31 -04:00
Cargo.lock Release v0.11.0 2026-08-07 18:49:10 +02:00
Cargo.toml fix(auth): restart login for recoverable callback errors 2026-08-11 08:20:33 -04:00
compose.dev-sso.yml feat(dev-sso): point the SPA OIDC issuer at bunyip-api 2026-06-05 13:34:01 -04:00
compose.yml refactor: rename mokosh-client to mokosh-apps 2026-05-29 14:50:58 +02:00
Dioxus.toml fix(dev): point SPA API proxy at server:8080 not 4301 2026-06-29 22:20:07 -04:00
Dockerfile chore(infra): clean up Dockerfiles, CI workflows, justfile, packaging 2026-06-13 02:48:10 -04:00
index.html fix(spa): clean up three CSP + manifest console errors on msp.a8n.systems 2026-07-03 13:38:52 +02:00
input.css fix(kb): declutter the article screen and fix Cancel 2026-08-07 07:10:10 -04:00
justfile ci(theme): fail the build on a colour nothing defines 2026-08-11 22:24:07 -04:00
package.json Release v0.11.0 2026-08-07 18:49:10 +02:00
README.md chore(infra): clean up Dockerfiles, CI workflows, justfile, packaging 2026-06-13 02:48:10 -04:00

Mokosh Client

Cross-platform Dioxus client for the Mokosh Platform. Compiles to WebAssembly and runs in the browser.

Tech stack

  • Dioxus 0.7 (Rust UI framework, web + router features)
  • wasm32-unknown-unknown target
  • Tailwind CSS v4 via Bun (bun x @tailwindcss/cli)
  • just task runner
  • Docker Compose for the dev server
  • Caddy to serve the built bundle in production (see oci-build/)

Prerequisites

Install on the host:

  • Rust (stable)
  • wasm32-unknown-unknown target: rustup target add wasm32-unknown-unknown
  • Bun
  • just
  • Docker with the Compose plugin
  • Nushell 0.112.2 (used by the dev and create-release recipes)

The dev server itself runs inside Docker, so the host does not need dioxus-cli installed.

Quick start

just dev

This recipe:

  1. Detects the host LAN IP from the first non-loopback IPv4 interface (en*/eth*/br*/wlan*) via sys net.
  2. Exports it as HOST_IP.
  3. Runs docker compose up --build, which starts dev-mokosh-apps and binds port 4301 to ${HOST_IP}:4301.

Open the printed URL (e.g. http://172.16.100.120:4301). Hot reload is enabled.

Binding to the LAN IP rather than 0.0.0.0 keeps the dev server off the public internet when the host is a VPS.

Reaching the dev server from another container

Either:

  • Use the host LAN IP: http://${HOST_IP}:4301.
  • Or join the dev-mokosh-private-${USER} Docker network and use http://dev-mokosh-apps-${USER}:4301.

Justfile recipes

just                  # list recipes
just dev              # run the dev server in Docker (see above)
just css-build        # one-shot Tailwind build
just css-watch        # Tailwind watch mode
just check            # check-web, check-clippy, check-fmt
just check-web        # cargo check --target wasm32-unknown-unknown
just check-clippy     # cargo clippy --all-targets
just check-fmt        # cargo fmt --all --check
just fmt              # cargo fmt --all
just test             # cargo test
just build            # release WASM bundle (dx build --release)
just check-docker     # build the production OCI image as :check
just build-docker     # build the production OCI image as :local
just create-release   # cut a release branch and bump versions (see below)

Project layout

src/
  main.rs           # WASM entry point
  lib.rs
  components/       # button, card, form, icons, layout, modal, table
  hooks/
  modules/          # auth, contacts, tenants, tickets
  pages/            # admin, billing, calendar, contracts, dashboard, ...
  utils/

assets/             # built CSS and static assets (styles.css is generated)
input.css           # Tailwind entry; compiled into assets/styles.css
index.html          # HTML template for the WASM bundle

Cargo.toml          # Rust workspace + crate config
Dioxus.toml         # dx serve / dx build config (port 4300, name, bundle)
package.json        # Bun deps (Tailwind v4)
justfile            # task runner
compose.yml         # dev server stack
Dockerfile          # dev image (dx serve with hot reload)
oci-build/          # production image (Caddy serving the built bundle)

Ports

Port Where What
4300 Dioxus.toml [server] port used by dx for the runtime server config
4301 compose.yml dx serve dev port, published as ${HOST_IP}:4301:4301

Login bypass (dev only)

Copy .env.example to .env and set both ADMIN_EMAIL and ADMIN_PASSWORD. When both are set and non-empty at compile time, the WASM client starts pre-authenticated as that admin user and the /login route redirects straight to /dashboard.

The values are baked into the bundle by option_env! at build time, so changing them requires a rebuild. build.rs declares cargo:rerun-if-env-changed for both vars, so editing .env (followed by re-running just dev) invalidates the cache and rebuilds.

This is dev-only and the boundary is enforced by Docker:

  • compose.yml loads .env via env_file and forwards ADMIN_EMAIL / ADMIN_PASSWORD into the dev container, where dx serve bakes them into the WASM bundle.
  • The bypass branch is gated behind #[cfg(debug_assertions)] and is compiled out of release builds entirely. dx build --release (and the oci-build/Dockerfile image) emits a WASM bundle that has no bypass code at all.
  • .dockerignore excludes .env from every Docker build context, so the file never reaches the production builder even if it exists in the working tree.

Leave both vars unset to use the normal login screen.

Cargo features

  • web (default) - WASM/web build.
  • multi-tenant (default) - multi-tenant build.
  • single-tenant - single-tenant build (mutually exclusive with multi-tenant).

Production build

Two options:

Local WASM bundle:

just build

Output lands under target/dx/.

Production OCI image (Caddy + WASM bundle):

just build-docker        # tags as mokosh-apps:local
just check-docker        # tags as mokosh-apps:check (smoke build)

The image is built from oci-build/Dockerfile and serves the bundle with oci-build/Caddyfile.

Self-host deployment

Self-hosters can pull the pre-built multi-arch image from the public registry and run it with the reference compose file. No build, no registry login required.

cp oci-build/compose.example.yml compose.yml
# Edit the four MOKOSH_* env vars in compose.yml to point at your own
# mokosh-server API + OIDC issuer.
docker compose up --detach

The image is dev.a8n.run/psa-systems-public/mokosh-www. Tags:

  • :vX.Y.Z - pin to a specific release.
  • :latest - rolling, advanced on every push to main.

The image supports linux/amd64 and linux/arm64; compose pulls the variant matching the host kernel automatically.

Runtime config is supplied via env vars on the container, so a single image works across staging, production, and self-host:

Env var Purpose
MOKOSH_API_BASE API base URL the SPA calls (https://api.example.com/api/v1).
MOKOSH_OIDC_ISSUER OIDC issuer the SPA authenticates against.
MOKOSH_OIDC_CLIENT_ID Public-client ID registered with mokosh-server.
MOKOSH_HUB_BASE_URL Origin of the Bunyip hub for legacy login bookmarks.

The container's entrypoint writes a tiny /_mokosh_config.js from these on each start; the SPA reads it before falling through to its compile-time defaults. Restart the container to pick up changed values.

To apply an available update, bump the tag in compose.yml and run docker compose pull && docker compose up --detach. The SPA is stateless so there's no migration step.

Releases

create-release bumps the version in both Cargo.toml and package.json, commits to a release/vX.Y.Z branch, pushes, and prints the PR URL:

just create-release major     # X.0.0
just create-release minor     # 0.X.0
just create-release hotfix    # 0.0.X

The recipe refuses to run on a dirty tree, switches to main, rebases against origin/main, and aborts if the two version fields disagree. After the PR is merged, the create-release workflow tags and releases the version automatically.

Troubleshooting

Address already in use (os error 98) when starting just dev: Something else is bound to ${HOST_IP}:4301. Find it with ss --tcp --listening --numeric --processes 'sport = :4301' (use sudo to see the owning process) or docker ps --format 'table {{.Names}}\t{{.Ports}}' | grep 4301. Stop the conflicting process or change the port in compose.yml and Dockerfile.

HOST_IP is empty: The dev recipe reads sys net | where name =~ 'eth0|br0'. If neither interface has an IPv4 address, the recipe fails. Check ip --brief address show and edit the regex to match the right interface.