chore(ci): push bunyip images to psa-systems-private + tag-driven builds #2

Merged
David merged 49 commits from chore/bunyip-packages-to-psa-systems-private into main 2026-05-17 03:30:12 +02:00
Owner

Brings bunyip's CI workflow in line with mokosh-server and mokosh-clients:

  • Pushes to dev.a8n.run/${PSA_SYSTEMS_PRIVATE_PACKAGE_OWNER}/ instead of dev.a8n.run/psa-systems/
  • Adopts the existing get-tags.nu scripts for version-aware tagging
  • Triggers on v* tag pushes so git tag v0.2.0 && git push --tags produces a properly tagged image
  • Adopts buildx with container-driver builder and registry-backed cache

🤖 Generated with Claude Code

Brings bunyip's CI workflow in line with mokosh-server and mokosh-clients: - Pushes to `dev.a8n.run/${PSA_SYSTEMS_PRIVATE_PACKAGE_OWNER}/` instead of `dev.a8n.run/psa-systems/` - Adopts the existing `get-tags.nu` scripts for version-aware tagging - Triggers on `v*` tag pushes so `git tag v0.2.0 && git push --tags` produces a properly tagged image - Adopts buildx with container-driver builder and registry-backed cache 🤖 Generated with Claude Code
feat: scaffold Bunyip frontend MVP with seeded mock backend
Some checks failed
build / Lint and type-check (pull_request) Failing after 15s
build / Build and push OCI image (pull_request) Has been skipped
903d54885e
Bunyip is the new SaaS / business / billing layer for Mokosh PSA. This first cut ships a fully-wired Dioxus frontend backed by an in-memory mock store (loaded from `seeds/*.json` on container boot). Every interactive element posts to the API and reflects the change; state resets when the container restarts. Real persistence + crypto land in Mokosh Server post-MVP — see `For AI/bunyip-feature-sso-port-notes.md` for the eventual port shape.

Workspace layout: top-level crates `bunyip-api` (Axum) and `bunyip-web` (Dioxus 0.7), helper `crates/bunyip-mocks` for models + seed loader. Dev stack runs as two containers via `just dev` or `docker compose --file compose.dev.yml up --build`, matching the mokosh-clients pattern (rust:1-slim-trixie base, non-root UID/GID via `HOST_UID`/`HOST_GID`, cargo-watch / dx serve, named cargo-target + node-modules volumes).

Phases delivered: 1) audit docs in `For AI/` (gitignored), 2) workspace scaffold + Dockerfiles (dev + oci-build) + Forgejo CI + justfile matching sibling repos, 3) auth + SSO UX (signup, login, TOTP, magic link, password reset, email verification - all wired to mock backend with session cookie), 4) orgs / members / invitations (list, invite, revoke, accept, role change, leave), 5) Stripe billing UX (current plan card, tier picker, mock checkout, cancel/uncancel), 6) feedback launcher + `/feedback` public page + `/admin/feedback` inbox - matches saas FeedbackLauncher + FeedbackPage pattern (color-coded tag chips, optional name/email/subject, honeypot, prefilled when signed in).

Seeded demo accounts (all use `MOCK_PASSWORD=demo`): admin@a8n.systems (platform admin), owner@example.com (owner of Example MSP, early adopter trial), pastdue@example.com (Acme Tech, past_due dunning demo), member@example.com (Example MSP member), lifetime@a8n.systems (Lifetime LLC, lifetime tier).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a class-based dark theme (Tailwind v4 `@custom-variant dark`) with a sun/moon toggle in the public nav, auth shell, and dashboard headers. Theme is resolved synchronously in `index.html` from `localStorage['bunyip-theme']` (falling back to `prefers-color-scheme`) so the splash and first paint already match the chosen mode — no flash of unstyled light content on dark systems.

Dark variants applied to the core surfaces: body, landing (hero + features + CTA strip + footer), pricing-adjacent reed gradient, auth shell + auth inputs, dashboard (welcome card + stat cards + org list), AppShell sticky header. Splash overlay flips colour too. `<meta name="theme-color">` ships both light + dark variants for mobile address bars.

Toast viewport, feedback launcher/modal, members table, billing page, admin feedback inbox, and public feedback page are still light-themed and want a follow-up pass — tracked in `For AI/bunyip-progress.md`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat: extend dark mode across remaining pages
Some checks failed
build / Lint and type-check (pull_request) Failing after 31s
build / Build and push OCI image (pull_request) Has been skipped
70da00e5fa
Bulk-adds dark variants to the pages that the first dark-mode commit didn't touch: pricing, public feedback, orgs list + members, billing, admin feedback, invitation accept, catch-all placeholder, 404, feedback launcher, toast. Applied via a perl pass that adds `dark:` siblings to common patterns without double-prefixing already-themed strings.

Verified in the browser at /pricing, /feedback, /dashboard, /admin/feedback, /settings/orgs/example-msp/members, /settings/orgs/example-msp/billing - all render legibly under the dark theme. The "forbidden" cards in the screenshots above are expected (admin user isn't a member of example-msp), not a dark-mode bug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix: dark variants on feedback launcher
Some checks failed
build / Lint and type-check (pull_request) Failing after 2s
build / Build and push OCI image (pull_request) Has been skipped
02fd47b7f4
The bulk perl pass mishandled `bg-white/90` (mid-string slash broke the word-boundary anchor), leaving the launcher with `bg-white dark:bg-bunyip-reed-800/90` - the transparency ended up on the dark variant only. Cleaned up to:

- `bg-white/90` + `dark:bg-bunyip-reed-800/90` (proper light/dark transparency)
- `hover:bg-white` + `dark:hover:bg-bunyip-reed-800`
- icon gradient adapts: `from-bunyip-reed-100 to-bunyip-water-100` + dark equivalents
- smile icon text color flips for contrast

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix: dark-mode contrast pass across all pages
Some checks failed
build / Lint and type-check (pull_request) Failing after 2s
build / Build and push OCI image (pull_request) Has been skipped
7d2fc91989
WCAG-AA audit at 4.5:1 for body text, 3:1 for large text. Walked every
authenticated and public route in both themes with a JS contrast scanner
and fixed the genuine low-contrast spots:

- dashboard SignedOutNotice: light-on-light headings (`text-bunyip-reed-900` / `text-bunyip-reed-700`) given dark variants
- auth.rs reset-password helper text + verify-email NoToken state + magic-link sent state were missed by the first perl pass; ran perl again on auth.rs / landing.rs / dashboard.rs to catch the remaining `text-bunyip-reed-{600,700,800,900}` without dark variants
- feedback page TAGS array: Bug / Feature / Flow / Idea idle + selected classes given full light/dark color pairs so chips stay legible in both modes
- feedback page decorative blob in the top-right corner was a bright `bg-bunyip-water-100` blur that read as a spotlight in dark mode - dimmed to `dark:bg-bunyip-water-700/30` + `dark:opacity-40`
- landing hero `PSA` highlight: bumped underline-pill colour to `water-700/60` and the text to `water-100` in dark so it has standalone contrast even without the bar

Remaining auditor noise on the landing CTA strip (white text on `bg-gradient-to-br`) is a false positive - the scanner walks `background-color` only, so it can't see CSS gradients. Verified visually that both themes render correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the per-page / per-route / per-API-module decisions from `docs/bunyip/01-merge-the-foundation.md`'s audit checklist so subsequent phases have a single source of truth. Lists which migrate-branch surfaces stay, which get rewired to mokosh-server endpoints, which get deleted (login-totp / magic-link / verify-email / feedback / admin-feedback), and which routes are net new (auth/callback, settings/{profile,security,sessions}, admin/{users,audit-logs}, signup/:token).

Also lands `bunyip-web/bun.lock` so the tailwind CLI install is reproducible across machines. The lockfile was generated as part of running `bun run build:css` during the phase-01 build verification.
Removes the same-origin /v1/... dev-proxy assumption from the API client. Every helper now hits the absolute mokosh-server URL configured at build time via `BUNYIP_OIDC_ISSUER` (`option_env!`). Bearer auth on authed calls; the access token lives in-memory in `stores::tokens::CURRENT_ACCESS_TOKEN` and is mirrored to localStorage under `bunyip.tokens` for refresh-on-reload. `credentials: 'include'` is intentionally absent: mokosh-server's CORS is `allow_origin(Any)` and incompatible with credentials. The Bearer-token-on-fetch model is what crosses origins.

New SPA modules:
- `stores/config.rs` - `OidcConfig::from_env()` reads BUNYIP_OIDC_{ISSUER, CLIENT_ID, REDIRECT_URI, SCOPES} at compile time, defaults SCOPES to `openid email offline_access`.
- `stores/tokens.rs` - `Tokens` + `StoredTokens` + the in-memory access-token cell + localStorage save/load/clear under `bunyip.tokens`.

API client surgery (`api/mod.rs`):
- `request` is replaced by `base_builder` + typed `{get,post,put}_authed` helpers. The old `request("DELETE", ...)` shape is preserved as a backwards-compat shim so `api/orgs.rs` + `api/feedback.rs` still compile until they get rewired in phases 04 / 05.
- Error envelope now decodes mokosh-server's `{ error, error_description }` shape (was the migrate-branch's `{ error: { code, message } }`).
- `me::fetch_me` now calls `/v1/auth/me` and adapts the wire `MeView` into the SPA's existing `MeResponse` (memberships left empty, filled in by phase 04; `email_verified_at` synthesized as "verified" until /me grows the field).

AuthContext (`stores/auth.rs`) hydrates the in-memory access token from localStorage on mount before the first /me fetch; clears tokens on 401 and collapses to SignedOut.

Plumbing:
- `Dioxus.toml` drops the dev `[[web.proxy]]` blocks (no longer needed; the SPA hits the absolute issuer URL).
- `.env.example` documents the four new BUNYIP_OIDC_* knobs.
- `compose.dev-sso.yml` (new) puts bunyip-web behind Traefik at `${USER}-bunyip.a8n.run` and surfaces the OIDC env vars to the build environment. `just dev-sso` is the entry point.
- mokosh-server's `justfile` gains a `register-bunyip-client` recipe that mirrors the existing `register-client` recipe but targets bunyip-web's host and prints the UUID for `bunyip/.env`.

cargo check passes on both targets (`just check-compile`, `just check-web`).
bunyip-web now owns the auth UX, wired to mokosh-server's `/v1/auth/*` and `/oauth2/*` surfaces. Visual design (AuthShell + AuthInput + SubmitButton) preserved from the migrate branch; functional pages rewritten.

New SPA modules: `modules/oidc/` (mod.rs, flow.rs, pkce.rs, storage.rs, tokens.rs). Lifted from mokosh-clients's `modules/oidc/` with the BUNYIP_OIDC_* env prefix substituted and `current_access_token` sourced from `crate::stores::tokens`. `Tokens` + `StoredTokens` re-exported from `stores::tokens` so the rest of the SPA can read them without pulling the OIDC module in. PKCE storage key is `bunyip_oidc_flow_v1`.

LoginPage: single component state machine. Password form -> on success store tokens + nav to Dashboard / return_to; on `mfa_required` flip to MFA prompt view with auto-submit on the 6th all-digit char, Trust-this-browser checkbox, friendly retry errors ("That code didn't match. Try again.", "Your code prompt expired", "Too many attempts"). Reads `?return_to=` from the URL with a strict allowlist (must look like an OIDC authorize query) and round-trips it via `window.location.set_href` after a successful sign-in for the cross-app SSO bridge (phase 07).

SignupPage: emits `POST /v1/auth/signup { email }`, shows the enumeration-resistant "we've sent a link" copy.
SignupCompletePage (`/signup/:token`): previews via `GET /v1/auth/signup/by-token/:token`, then `POST .../complete` with `{ password, first_name, last_name }`.
ForgotPasswordPage: `POST /v1/auth/password-reset { email }` + the same "we've sent a link" copy.
ResetPasswordPage (`/reset-password/:token`): previews, then `POST .../complete` with password + confirmation.
AuthCallbackPage (`/auth/callback`): consumes the OIDC code via `oidc::complete_login`, persists tokens, refreshes AuthContext, navigates to return_to or Dashboard.

Deleted as flows but kept as one-line redirect stubs to /login: `LoginTotpPage` (merged into LoginPage's MFA view), `MagicLinkPage`, `VerifyEmailPage`. The Route enum entries stay so bookmarks do not 404; the redirect lands users in the right place.

api/auth.rs rewritten to call mokosh-server: signup_start / signup_preview / signup_complete / forgot_password / reset_preview / reset_complete / logout. The old `login` / `signup` / `totp_verify` / `magic_link_request` helpers are gone; password_login + mfa_verify live in `modules/oidc/flow.rs` because they produce `Tokens` bundles that the AuthContext consumes.

Plumbing: `main.rs` calls `modules::oidc::snapshot_initial_search()` before `dioxus::launch` so Dioxus's router cannot erase the OIDC callback query string before AuthCallbackPage reads it. `components/layout.rs::AuthShell` now takes `title: String, subtitle: String` (was `&'static str`) so the SignupComplete / ResetPassword pages can fill in the previewed email. New Cargo deps: base64, sha2 (workspace), rand 0.9 (overridden from workspace 0.8 to match the getrandom 0.3 wasm story), js-sys, thiserror.

`just check-web` is green.
Adopts the same recipe set + structure mokosh-clients uses, adapted for bunyip's workspace (api native + web wasm + mocks):

- `install-hooks` writes a `.git/hooks/pre-commit` stub that execs `just pre-commit`. Bypass with `git commit --no-verify`.
- `pre-commit` runs the same checks as the Forgejo `check.yml` job inside the rust-builder-glibc image: fmt, clippy (workspace), native check (workspace minus bunyip-web), wasm check (bunyip-web only), test (workspace minus bunyip-web).
- `ensure-npm` + `css-build` + `css-watch` for the Tailwind step. Bunyip's `package.json` lives in `bunyip-web/`, so the recipes `cd` into it.
- `dev` now uses `with-env` to inject `BUNYIP_HOST_BIND_IP` / `HOST_UID` / `HOST_GID` / `USER` into the compose run rather than rewriting `.env` on every invocation. Same end result; cleaner state.
- `dev-sso` likewise uses `with-env` plus a defensive `docker network create` for the external `dev-bunyip-private-${USER}` network the base compose declares.
- Unified `down` recipe stops both LAN-IP and SSO modes regardless of which `just dev*` was used to start them, with `--remove-orphans` for cleanup of the previous mode's containers.
- `dev-down` / `dev-sso-down` stay as targeted equivalents.
- All existing bunyip-specific recipes preserved (check-compile, check-web, check-clippy, check-fmt, fmt, test, build, build-web, check-seeds, check-docker-api, check-docker-web, build-docker, create-release).

cargo-target / cargo-registry volumes are now `dev-bunyip-cargo-target` / `dev-bunyip-cargo-registry` (matching the existing per-user `dev-bunyip-cargo-target-${USER}` volume from compose.dev.yml, just sans the per-developer suffix for the CI-style hook image).
Phase 04 from docs/bunyip/04-account-pages.md. Ports the three signed-in account-management pages from mokosh-clients into bunyip-web and re-skins them to bunyip's AppShell + SettingsCard layout.

- pages/profile.rs: Profile form (first_name/last_name/timezone/avatar_url) wired to GET /v1/auth/me and PUT /v1/auth/me/profile, plus a Change-password card on POST /v1/auth/me/password.
- pages/security.rs: MFA state machine (Loading/Disabled/Enrolling/Enrolled) on /v1/auth/mfa/status with three enrolment sub-steps (ScanQr -> ConfirmCode -> SaveCodes), step-up prompt for Regenerate/Disable, and an InlineNotice for the already-enrolled race.
- pages/sessions.rs: Active-session list on /v1/auth/sessions with ua_short_label heuristic, inline rename via display_name, and revoke with current-session handling (clears tokens then redirects to /login).
- modules/oidc/flow.rs: add issuer_post_authed_empty for revoke-style calls that take no body.
- modules/oidc/mod.rs: re-export the new helper alongside the existing flow exports.
- routes.rs + pages/mod.rs: wire /settings/profile, /settings/security, /settings/sessions.
- Workspace-wide cargo fmt --all run sweeps in the formatting churn that accumulated through phases 01-03.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Phase 05 from docs/bunyip/05-admin-pages.md. Ports the admin-only surface from mokosh-clients into bunyip-web and re-skins it to the AppShell + bunyip-reed palette.

- api/admin.rs: typed wrappers for list_users / suspend / reactivate / force_disenroll_mfa / list_invites / issue_invite / resend_invite / revoke_invite / list_audit_logs, plus the AUDIT_EVENT_KINDS canonical list mirrored from mokosh-auth-storage.
- pages/user_management.rs: user table with role/status/MFA badges, Suspend/Reactivate per row, and a Disenroll-MFA confirm modal that captures the required reason. The current user's own row hides destructive actions (server also enforces).
- pages/invite_create.rs: invite-issuance form with client-side looks_like_email check before submit; renders the shareable accept_url + clipboard-copy on success.
- pages/invite_list.rs: pending-invites table with Resend + Revoke per row, toast-driven feedback.
- pages/audit_logs.rs: paginated table with severity badge, prev/next navigation, and a dropdown of canonical event_kind values plus a Custom... fallback for power users.
- stores/auth.rs: use_require_role hook that redirects non-admins to /dashboard and signed-out users to /login.
- routes.rs + pages/mod.rs: wire /admin/users, /admin/users/invite, /admin/users/invites, /admin/audit-logs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Phase 06 from docs/bunyip/06-app-launcher.md (frontend half). Reshapes the post-sign-in dashboard from "welcome + stats" to "pick which app to sign in to."

- api/apps.rs: typed wrapper for the new GET /v1/auth/apps endpoint.
- modules/oidc/flow.rs: start_login_for(cfg, client_id, redirect_uri). Same PKCE + state + nonce dance as start_login, but uses the supplied client_id / redirect_uri instead of bunyip's own. No PendingFlow is stored locally; the target app owns the callback half.
- pages/dashboard.rs: rewritten. Fetches the apps list and renders one tile per row, split into "Your apps" (is_first_party) and "Connected third-party apps" groups. Clicking a tile kicks off the OIDC flow and the browser leaves bunyip. Empty list renders a "no apps yet" empty state; the loading / error / empty branches all match bunyip's design language.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Phase 07 from docs/bunyip/07-sso-redirect-bridge.md (bunyip half). Bunyip already had the return_to round-trip wired up from phase 03; this commit tightens the open-redirect guard so the only values that ride through are recognisable authorize queries.

- read_safe_return_to splits into a thin reader + an is_safe_return_to predicate that requires response_type= as a prefix, client_id= somewhere in the string, and rejects CR/LF.

The client_id / redirect_uri ACL stays on mokosh-server's authorize endpoint: replaying it client-side would need a synchronous client registry. The SPA's job is to keep the value structurally sane; defense in depth happens server-side.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Compose was refusing to reuse the network that the `dev-sso` recipe pre-creates because the network's labels did not match the project compose was trying to attach it to. Mark it external (the recipes already ensure existence via `docker network inspect || docker network create`) so compose just attaches instead of attempting to manage or relabel it.

Same idempotent pre-create step added to `just dev` (LAN-IP mode) so that recipe does not regress now that compose no longer creates the network.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Three fixes around the post-sign-in surface, paired with the credentialed-CORS change in mokosh-server.

1. Launcher hides bunyip's own client_id. The "Your apps" grid would otherwise list a tile that "launches" the hub the user is already inside - a no-op at best, a confusing re-auth round-trip at worst. pages/dashboard.rs filters the apps list against OidcConfig::from_env().client_id before splitting first-party / third-party.

2. The /v1/auth/login fetch now carries credentials: 'include'. The same goes for /v1/auth/mfa/verify and every other api::* call (set globally in api/mod.rs::base_builder). Without it the browser was dropping the OP session cookie that mokosh-server sets in the login response, so a subsequent click on a launcher tile found no OP session and got 302'd back to /login. Required tower's CORS layer on the server to switch to AllowOrigin::mirror_request() + allow_credentials(true), shipped separately on mokosh-server.

3. Logout + back-button no longer flashes a stale dashboard. stores::auth::use_bfcache_invalidator listens for the `pageshow` event with persisted=true (browser is restoring from bfcache) and forces a full reload. The DashboardPage also now `nav.replace`s to /login on SignedOut instead of rendering a "You're signed out" notice, so the bfcache restore path always lands on /login. SignedOutNotice removed (dead code).

web-sys gains the PageTransitionEvent + RequestCredentials features.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Pairs with the mokosh-clients AuthGuard change. PKCE requires the code_verifier to live on the same origin as the /auth/callback page that exchanges the code; bunyip's old launcher generated PKCE locally and bounced to /oauth2/authorize, so by the time the code landed on mokosh-clients/auth/callback there was no PendingFlow to exchange it - the user saw "storage: no pending OIDC flow".

Fix: launcher just navigates the browser to the target's own /dashboard URL (derived from the registered redirect_uri's origin). The target's AuthGuard runs start_login locally, storing PendingFlow in the target's sessionStorage, and authorize 302s back to /auth/callback carrying the OP cookie that was set when the user signed in on the hub.

- pages/dashboard.rs: new launch_url(redirect_uri) helper; launch callback set_hrefs to that URL instead of calling start_login_for.
- Cargo.toml: web-sys `Url` feature for the parser.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
The DashboardHeader's Sign-out handler called refresh_auth(auth) after POSTing /v1/auth/logout. refresh_auth refetches /v1/auth/me to materialise the new state - but the Bearer token in localStorage is a stateless JWT that /v1/auth/logout does not revoke, so /me succeeded and refresh_auth set the signal right back to SignedIn. End result: clicking Sign-out toasted "Signed out." and then dropped the user back on /dashboard.

Switched to the stores::auth::sign_out helper, which clears localStorage tokens + the in-memory access_token holder AND drops the signal to SignedOut in one synchronous step. The dashboard's existing use_effect bounces to /login on SignedOut, completing the actual logout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Sequences `down` then `dev-sso` so a single command tears the stack down and brings the rebuilt one up. Useful after pulling code or editing compose env vars where you need the new state inside running containers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Implements the bunyip half of docs/migration/settings-split.md. Adds the missing account-management landing page and brings the tenant switcher over from mokosh-clients (the active-tenant claim is identity-level, not PSA-domain, so it belongs next to the rest of the auth surface).

- api/tenants.rs: list_memberships() + switch_active_tenant() wrap GET /v1/auth/memberships and POST /v1/auth/active-tenant. The switch helper persists the freshly-issued token bundle to localStorage so the in-memory access token stays in sync with the new mokosh_active_tenant claim.
- pages/settings.rs: card-grid hub at /settings. Three groups:
  - "Your account" - Profile, Security, Sessions, Switch tenant (only rendered when the user has >= 2 memberships).
  - "Organisations" - Org list.
  - "Admin" - User management, Pending invites, Audit logs, Feedback inbox. Only rendered when me.user.role == Admin.
- pages/active_tenant.rs: ported from mokosh-clients. Same UX shape (one row per membership, Current badge on the active one, Switch button reissues tokens + window.location.reload).
- components/layout.rs: AppShell header gains a "Settings" Link next to the existing "Orgs" Link so the hub is one click from anywhere in the app.
- routes.rs + pages/mod.rs: wire /settings and /settings/active-tenant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
The dashboard renders its own DashboardHeader rather than AppShell, so my previous Settings-link addition only landed on the inner pages (profile, security, etc.) - not on /dashboard, which is exactly where the user actually looks for it. Mirrored the link into DashboardHeader so the entry point is one click from the launcher.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Reload caused an apparent logout because use_auth_provider:
1. Called clear_tokens() on ANY /me failure, including transient ones (CORS preflight blip, network glitch, decode hiccup), so a single bad fetch made the localStorage bundle disappear.
2. Did not try to refresh the access token before fetching /me. Short-lived JWTs (10-30 min) expired between visits, /me 401'd, see point 1.

Rewrote the boot path:

- maybe_refresh: if the stored access_token is expired or within 30s of expiry, refresh it BEFORE fetching /me. Uses the existing refresh_tokens helper in modules::oidc::flow.
- /me success -> SignedIn.
- /me returns 401 -> one refresh-and-retry. If that also fails, then clear tokens.
- /me returns anything else (Network, Decode, etc.) -> SignedOut but tokens stay in localStorage so the next reload can retry cleanly. Most "reload logged me out" reports trace back to transient errors that should not invalidate the session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
When an access token expires, the user should be silently refreshed (good UX) - and when the refresh chain finally fails (refresh token expired or revoked), they should land on /login automatically, without having to click anything or trigger another fetch.

Added a background loop inside use_auth_provider that:

- Sleeps until ~30 seconds before the current access token expires (`gloo_timers::future::TimeoutFuture` so the wakeup is wasm-friendly and not tied to the request lifecycle).
- Refreshes via the existing flow::refresh_tokens, persists the new bundle to localStorage, and reschedules.
- On refresh failure (no refresh token, refresh token expired/revoked) clears tokens and drops the signal to SignedOut. The existing per-page `nav.replace(LoginPage) on SignedOut` use_effects pick that up and redirect the user.

The loop is owned by the same use_future that initialises auth state, so it lives for the lifetime of the App component. New deps: gloo-timers (futures feature) for the wasm setTimeout primitive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
If the user has a still-valid refresh chain (use_auth_provider just exchanged it for a fresh access token) and they hit /login - typically because some other first-party RP bounced them here via the SSO bridge - they should NOT see the form. They should just complete what they were trying to do.

Added a use_effect on LoginPage that fires when auth flips to SignedIn:

- If the URL carries `?return_to=` (SSO bridge), top-level navigate to ${issuer}/oauth2/authorize?<return_to> so mokosh-server can mint a code for the originating RP. The OP cookie is already set (it survives across origins under the .a8n.run cookie domain), so authorize closes the loop without prompting.
- Otherwise nav.replace to /dashboard so a direct visit to /login while signed-in doesn't trap the user on a form.

Combined with the background refresh loop, the result is: a user who signed in to bunyip yesterday and goes straight to mokosh-clients today never sees a sign-in form - the OP cookie + refresh chain handle everything.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
feat(web): canonical /logout endpoint that kills the cross-origin OP session
Some checks failed
build / Lint and type-check (pull_request) Failing after 2s
build / Build and push OCI image (pull_request) Has been skipped
5074086c13
mokosh-clients's Logout button used to just clear its own local tokens and redirect to bunyip's /login. Two problems with that:

- The .a8n.run-scoped OP session cookie on mokosh-server was never cleared, so a follow-up visit to mokosh-clients would silently sign the user back in via the SSO bridge.
- Bunyip's own localStorage tokens stayed live: the next time the user visited https://${hub}, they were "remembered" even though they had just logged out.

Centralised the teardown on bunyip's /logout page. Any first-party RP redirects there on logout and bunyip:

  1. POSTs /v1/auth/logout (credentials:include via base_builder makes the browser apply the Set-Cookie that clears the OP session cookie on .a8n.run).
  2. Clears localStorage tokens + drops the auth signal to SignedOut via the existing stores::auth::sign_out helper.
  3. nav.replace(/login).

mokosh-clients's UserMenu now redirects to ${hub}/logout instead of ${hub}/login, after first clearing its own local state. Bunyip's in-app sign-out (the DashboardHeader + AppShell buttons) is unchanged - it already POSTs /v1/auth/logout from this origin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
feat(web): demo-feedback polish pass (sign-out + signup UX + nav + cards + dead-button audit)
Some checks failed
build / Lint and type-check (pull_request) Failing after 5s
build / Build and push OCI image (pull_request) Has been skipped
13cd5d96e7
Bundles the seven post-demo follow-ups so the SaaS shell feels stable before more flows land on top. Each block below is independent in spirit; commits were merged into one because most affect overlapping files in components/layout.rs and pages/dashboard.rs.

1. Sign-out unified through `/logout` (pages/logout.rs is the only handler; layout.rs AppShell and pages/dashboard.rs DashboardHeader now `nav.replace(LogoutPage)` instead of duplicating teardown). Adds `web_sys::console::log_1` traces in logout.rs so a stuck sign-out is visible in DevTools without a rebuild. pages/sessions.rs current-session revoke also routes through `/logout` instead of `window.location.replace`, so the auth signal moves to SignedOut deterministically rather than relying on a hard reload.

2. Reactive password-strength checklist on signup-by-token and reset-password (components/password_checklist.rs, mounted in pages/auth.rs SignupCompletePage + ResetPasswordPage). Rules mirror `mokosh_auth_core::policy::validate_password_strength` exactly. Submit stays disabled until every rule is green. api/mod.rs now parses `{ "error": ..., "details": { "password": ... } }` and surfaces the field-level reason via the existing InlineError slot, so the user sees "password must be at least 12 characters" instead of a bare 400.

3. SafeImage component (components/image.rs) - thin <img> wrapper with onerror fallback to a children slot. Empty/whitespace src renders the fallback synchronously (no failed network request). Wired into pages/dashboard.rs AppTile so OIDC client icon 404s fall back to the app's first-letter glyph instead of Chrome's broken-image icon.

4. AppShell now accepts an optional `back_to: Option<Route>` plus `back_label`, rendering a "← Back to {label}" link in the page header. Wired across all nested settings/admin pages: profile, security, sessions, active_tenant, orgs (list + members + billing), invite_create/list, audit_logs, user_management, admin/feedback. Inline `Link { to: ... "← Back to ..." }` rows in orgs/billing/invite_create/invite_list removed in favour of the shared AppShell-owned control. Browser back still works because the back link is a real router Link.

5. Brand image centering. BrandMark SVG gets `dark:text-bunyip-reed-200` so the reed icon stays visible on the dark AuthShell/AppShell. BunyipMascot face shifted up ~30 units in the 400x400 viewBox so the eyes sit near the geometric center instead of crowding the bottom third; ripples, head, eyes and tagline all moved together so the composition still reads. Tagline pill switched from `-bottom-2 right-2` to `-bottom-2 left-1/2 -translate-x-1/2` so the badge actually sits on the disc's centerline.

6. Uniform grid-of-cards. Three card grids equalised: HubSection (settings.rs), AppTile grid (dashboard.rs), FeatureCard grid (landing.rs). Each grid gets `auto-rows-fr`, each card gets `h-full flex flex-col` so the tallest card sets the row height and CTAs (`Open →`, `Launch →`) pin to the bottom via `mt-auto` regardless of description length.

7. Dead-button audit. AppHeader (components/layout.rs) was unused dead code that contained three onclick-less buttons - removed entirely. orgs.rs "+ New org" stubbed with `disabled + title="Coming soon - org creation API not wired yet"`. pricing.rs tier CTAs converted from no-op buttons to `Link` to SignupPage (matches landing-page CTA pattern). Calendar prev/next/today wired client-side via chrono (mokosh-clients side); event-create / Week / Day toggles stubbed with `disabled + title`. Time-sheet prev/next-week wired similarly. Notifications bell in mokosh-clients top-bar stubbed pending the notifications API.

Verified: `cargo check --package bunyip-web --target wasm32-unknown-unknown` clean (28 pre-existing warnings unchanged); `cargo fmt --all` applied. Browser walk-through pending the next `just dev-sso` boot.
docs: switch comments to American English spellings
Some checks failed
build / Lint and type-check (pull_request) Failing after 5s
build / Build and push OCI image (pull_request) Has been skipped
bad6b060ff
fix(web): Organisations -> Organizations in settings hub (American spelling)
Some checks failed
build / Lint and type-check (pull_request) Failing after 2s
build / Build and push OCI image (pull_request) Has been skipped
24917a2b33
Two unrelated fixes for items that surfaced from the demo walk-through.

1. Sign-out flakiness. The previous flow routed every sign-out click through `nav.replace(LogoutPage)` and let LogoutPage's `use_future` run the network call + signal flip + final redirect. That works in principle but races against Dioxus router transitions: when the navigation rehydrated the new route, the future occasionally got cancelled or never spawned, leaving the user stuck on "Signing you out..." until they clicked again.

Switched the in-app sign-out handlers (AppShell + DashboardHeader) to do the teardown directly in the click handler:
  - synchronous `stores::auth::sign_out` (clears localStorage + drops the auth signal to SignedOut)
  - synchronous `nav.replace(LoginPage)` so the UI moves immediately
  - background `spawn` of `api::auth::logout()` so the OP cookie still gets cleared, but the UI never waits on it

The /logout route stays in place for the CROSS-ORIGIN sign-out path (mokosh-clients -> hub); only the in-app buttons skip the indirection.

2. Audit-log rows now read as English. Three additions to `pages/audit_logs.rs`:
  - `event_label(kind)` maps every canonical `event_kind` to a human label. Unknown kinds fall back to title-case so new kinds remain readable.
  - `event_details(kind, metadata)` pulls per-kind fields out of the metadata blob (login_failed -> reason+email; invite_issued -> email+role; totp_disenrolled -> disenrolled_by+reason; etc.) and joins them with " · ".
  - `short_actor(id)` shows the first 8 hex chars; the full UUID is in the title attribute on hover. Same treatment for the raw `event_kind` next to its label.

Table grew a "Details" column so the metadata is actually visible without DevTools-ing the JSON.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
The signed-in user's name in the navbar was rendered with the same `text-bunyip-reed-700` colour as the Settings / Orgs Link components next to it, so users were clicking it expecting a menu / profile dropdown. Muted the text to `text-bunyip-reed-500` (light) / `bunyip-reed-400` (dark) and added `cursor-default select-text` so the cursor stays an arrow and a hover does nothing. Also bumped the size down to `text-xs` so it reads more like a metadata badge than a peer of the action links.

Applied symmetrically to AppShell (layout.rs) and DashboardHeader (pages/dashboard.rs); same identity, two render sites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
The signed-in user name was sitting in the nav row on the right, between Orgs and Sign out. Moving it into the org pill on the left so the navbar reads as "where am I + who am I" in one place, and the right side is purely actions.

Rendered as `[● Personal | Yousif]` with a thin divider between the org name and the user name; user name is `text-xs` and muted so the org leads. Both AppShell (layout.rs) and DashboardHeader (pages/dashboard.rs) updated symmetrically. Right-side render of user_name removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Implements docs/bunyip-settings/04-ui.md against the new mokosh-server endpoints from the matching 03-server-additions.md.

api/admin.rs:
- UserListFilter (search/role/status/mfa/limit/offset) with to_query() URL encoder.
- UserListPage envelope ({users, total, limit, offset}); list_users() now takes a &UserListFilter and returns the envelope.
- UserDetail envelope ({user, available_role_transitions}); new get_user(id) helper.
- New mutation helpers: change_user_role, delete_user, resend_verify, admin_trigger_password_reset.

pages/user_management.rs (rewritten):
- Filter row: search input + role/status/mfa dropdowns + Reset button. Every change resets offset to 0.
- Pagination strip at the bottom of the table with prev/next; "Showing N-M of TOTAL" readout.
- Each row's name + email is now a Link to /admin/users/:id.
- Inline suspend/reactivate + force-disenroll buttons preserved for one-click admin tasks.

pages/user_detail.rs (new, ~700 lines):
- Five sections: Profile (read-only header w/ status + role badges), Role + status (legal-only role buttons computed from server's available_role_transitions + suspend/reactivate), Security (force-disenroll MFA, resend verification when pending, admin-trigger password reset when verified), Audit cross-link, Danger zone (delete user).
- ChangeRoleModal: confirm with optional reason; if promoting to admin, embeds StepUpVerifyForm and disables Save until a step_up_token is obtained.
- DisenrollMfaConfirm: reason-gated.
- DeleteUserConfirm: requires typed-email match AND step-up token AND reason before enabling the final Delete button.
- StepUpVerifyForm: POSTs /v1/auth/mfa/step-up/start to issue a challenge, then /verify with the admin's TOTP code; surfaces the resulting step_up_token to the parent modal.

routes.rs + pages/mod.rs: wire /admin/users/:user_id (AFTER the literal /invite + /invites subpaths so dioxus picks them first).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
The list page's filter inputs (search/role/status/mfa) were updating signals but the table did not change. Root cause: use_future was only reading bump+filters INSIDE the async block, which is too late for Dioxus 0.7's signal tracking - the closure body has to read the signals synchronously for the future to subscribe to them.

Refactored to snapshot every filter signal at the top of the outer closure body, then capture the snapshots into the async block. This makes use_future re-run on any filter change. Dropped the explicit `bump` indirection from filter inputs (they no longer call refetch_first_page - signal changes do it for them) but kept `bump` for explicit refetches after mutations (suspend, reactivate, force-disenroll-MFA). Renamed the offset-reset callback to `reset_offset` since that is all it does now.

Server-side filter was verified correct via curl: `?search=admin` returns total=1 while the unfiltered list returns total=8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Two follow-ups on the user-management surface.

1. The search + role/status/MFA filters did nothing. Root cause: `use_future` in Dioxus 0.7 is NOT reactive - it runs ONCE on mount via use_hook + Callback, and never re-runs from signal changes regardless of where the read happens. The reactive variant is `use_resource`, which wires its closure into a ReactiveContext and re-evaluates whenever any signal it reads changes.

Converted both:
- pages/user_management.rs: list fetch is now use_resource. Filter signals read directly inside the future trigger re-runs automatically. Mutation handlers call `page.restart()` to refresh after suspend / reactivate / disenroll.
- pages/user_detail.rs: per-user fetch is now use_resource; `detail.restart()` after role / status / verify / reset / disenroll mutations.

Bump signals + manual page.set/None scaffolding removed in both files.

2. Self-view on /admin/users/:id was rendering "Role and status / You cannot change your own role or status. Ask another admin." That's noise - admins can't mutate themselves anyway, and the section title alone makes them think a control should be there. The whole section is now `if !is_self`-gated, so self-view shows Profile + Security + Audit + Danger zone only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Doc 01 from /docs/mokosh-fixes. Same `use_future`-not-reactive bug we just fixed on /admin/users: Dioxus 0.7's `use_future` runs once on mount and ignores signal changes; `use_resource` is the reactive variant that re-runs when signals it reads change. The Filter button was bumping a signal that the future never observed, so the `kind` query parameter was forever empty (audit URL was `?limit=50&offset=0&`).

Converted `pages/audit_logs.rs` from use_future + bump + manual data.set to a single use_resource closure. Filter / Prev / Next callbacks now just mutate the offset / kind_select signals; the resource re-evaluates automatically.

Verified: changing the dropdown to login_failed now produces `?limit=50&offset=0&kind=login_failed` in the network panel, and the table re-renders with only login_failed rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: YousifShkara <yousif@niceguyit.biz>
Replaces raw `Request failed (404)` red banners on `/settings/orgs`, `/settings/orgs/:slug/members`, `/settings/orgs/:slug/billing`, and `/admin/feedback` with a shared `ErrorCard` component that classifies API errors into ComingSoon (404), Retryable (5xx / network), or HardError (everything else). Retryable variant offers a Retry button that calls back into the page's resource.restart().

Doc 02 of docs/mokosh-fixes/. Per-panel loading refactor in dashboard.rs is deferred: the only Splash in the codebase is on the dashboard page itself for first-load auth gating, and does not flash on internal navigation while signed in.
UI side of doc 03. Pairs with mokosh-server commits 0708bad / 8346c9e / 97cef49 (schema + Pg impls + 8 /v1/orgs handlers).

- `+ New org` button on `/settings/orgs` now opens a `CreateOrgModal` that POSTs `/v1/orgs` with `{name, slug?}` (slug optional; server auto-derives from name). On success: toast, refresh the list, navigate to the new org's members page so the founder lands ready to invite.
- New `components/org_switcher.rs::OrgSwitcher` replaces the static `[● Personal | User]` pill in both `AppShell` (settings + admin pages) and the dashboard header. Click the pill -> dropdown of `/v1/auth/memberships`. Picking another tenant POSTs `/v1/auth/active-tenant` to reissue the token bundle, then reloads so every downstream resource refetches under the new scope. Dropdown footer links to `/settings/orgs`.
- `api/orgs.rs`: new `CreateOrgRequest` + `create_org()` helper.

`just check-web` is green.
SPA side of doc 05. Pairs with mokosh-server 22196f8 (schema + read endpoints + trial-on-org-create).

- `pages/pricing.rs`: dropped the leaked "Phase 5 will load these from the live /v1/billing/tiers endpoint" sentence (audit finding #6). The hardcoded tier cards remain; live-tier rewire of the public pricing page is a follow-up.
- `pages/billing.rs::OrgBillingPage`: cancel / uncancel / TierPicker Select buttons rendered as disabled-with-tooltip ("Coming soon - Stripe integration not wired yet"). The handler closures are dropped; they'll be rewritten against the real Stripe endpoints when that surface lands.

Per-org billing already calls `GET /v1/orgs/:slug/billing` and `GET /v1/billing/tiers` via api/billing.rs; with the server endpoints now live, the page populates with the trial subscription that org-create plants.

just check-web is green.
Subset of docs/mokosh-fixes/06-polish.md. Pairs with mokosh-server 23b91cf.

- pages/sessions.rs: `friendly_ip()` helper labels RFC1918 / loopback / IPv6 ULA addresses as "Local network" or "This device" alongside the raw IP. IP-to-geo lookup intentionally omitted (per-deploy decision; MaxMind GeoLite is the cheap path when it lands).
- pages/sessions.rs: "Sign out of all other sessions" bulk-revoke button at the top of the list. POSTs the new server endpoint /v1/auth/sessions/revoke-others (server-side: leaves the current sid alive; revokes every other op_session + bound refresh family).
- pages/profile.rs: timezone picker is now a curated `<select>` of common IANA zones rather than a free-text input. Avoids "Plotkin/Cumberland" typos landing on the server. Power users with niche zones can still inspect-edit.

Out of scope here (called out in the polish doc, deferred to follow-ups):
- Audit-log date-range / actor / search / severity filters + CSV export. The CSV export alone is ~half a day; lives in its own PR.
- Recovery-code "last generated" + low-count warning (needs a new /v1/auth/mfa/recovery-codes/status endpoint).
- Invite list "Copy link" affordance — the list endpoint returns hashed tokens, so a copy-link button would need a server change to surface the accept URL on listing.

Focus-ring audit: every `focus:outline-none` in bunyip-web/src/ already has a paired `focus:ring-2` companion (verified with grep). No further sweep needed.
Dashboard launcher tile click now fires POST /v1/auth/audit/launched-app before navigating to the target app's origin. Pairs with mokosh-server 9a98878.

Fire-and-forget; the browser races set_href against the audit POST, so the row may not always land. Acceptable trade-off for a per-click signal vs blocking the user with a sync round-trip.
Two stacked bugs surfaced on /settings/orgs: the page rendered "Email or password didn't match" inside a HardError card even though the user was signed in.

1. api/orgs.rs, api/billing.rs::get_billing, and api/feedback.rs::list_admin used `get_json` (unauthed) instead of `get_authed`. The Bearer header never reached the server, every admin-gated GET 401'd. Same pattern for the create_invitation / change_member_role / leave_org / accept_invitation POSTs.

2. api/mod.rs::ApiError::user_message mapped every 401 to "Email or password didn't match" - a stale label written for the login form that leaked into every other surface as a HardError. Replaced with "Your session expired. Please sign in again." The login page already has its own friendly_login_error path (pages/auth.rs:313) keyed off FlowError, so this doesn't regress sign-in copy.

Server side never had a bug here.
- Remove the standalone 'Pending invites' card from /settings hub. The link from /admin/users (user management) is the single entry point.
- Revert invite_list back button to point at user management rather than settings; nested page, nested back nav.
Replaces the PlaceholderPage 'Coming soon' that emails were landing on with a real accept page.

- new api/auth.rs helpers: invite_preview (GET /v1/auth/invites/by-token/:token) and invite_accept (POST .../accept) matching mokosh-server's InvitePreview / AcceptInviteBody shapes.
- new pages/invite_accept.rs renders two branches off the preview's `kind`:
  - new_account: collects first name, last name, password (with the PasswordChecklist); POST creates the user under SERIALIZABLE.
  - join_tenant: confirms add-membership for the existing account; no password field.
- route `/invite/:token` -> InviteAcceptPage. Sits above the catch-all PlaceholderPage so the email's deep link resolves correctly.

Distinct from pages/invitations.rs (org-scoped invitations at /invitations/accept); this is the admin-invite flow that mints fresh user accounts.
fix(user-detail): friendlier step-up start error when admin lacks MFA
Some checks failed
build / Lint and type-check (pull_request) Failing after 2s
build / Build and push OCI image (pull_request) Has been skipped
87b3bd7491
Server returns {"error": "not_enrolled"} when the calling admin doesn't have MFA enrolled, but the raw body was being displayed verbatim ('step-up start: network: {error: not_enrolled}'). Map to: 'You need to enable MFA on your own account before performing this action. Set it up under Settings -> Security.'

Also translates rate_limited to a tighter message. Other failures fall through to the prior 'Could not start step-up: <e>' format.
chore(ci): rewrite OCI build workflow to match mokosh pattern
Some checks failed
build / Lint and type-check (push) Failing after 2s
build / Build and push OCI images (push) Has been skipped
build / Lint and type-check (pull_request) Failing after 4s
build / Build and push OCI images (pull_request) Has been skipped
d6bbc6673f
Brings bunyip's CI into line with mokosh-server and mokosh-clients:
- Pushes to dev.a8n.run/${PSA_SYSTEMS_PRIVATE_PACKAGE_OWNER}/ (was dev.a8n.run/psa-systems/)
- Uses the per-image get-tags.nu scripts for version-aware tagging
- Triggers on v* tag pushes so `git tag v0.1.0 && git push --tags` produces a versioned image
- Adopts buildx with container-driver builder and registry-backed cache

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ci: don't gate image build on the trixie check job
Some checks failed
build / Build and push OCI images (pull_request) Has been skipped
build / Lint and type-check (pull_request) Failing after 2s
build / Lint and type-check (push) Failing after 3s
build / Build and push OCI images (push) Successful in 4m2s
e9f6e1a7cc
The Lint and type-check job (runs-on: docker + container: rust:1-slim-trixie)
has been failing on every push/PR run in this repo for a while - likely
a runner-label mismatch with the forgejo runner pool. Gating image builds
on it via `needs: check` means the build job gets skipped even when the
code compiles cleanly, which blocks the v0.2.0 release.

Dropping the dependency. The build job runs the same cargo workspace
through buildx, so a real compile failure still fails the build job.
Fix for the check job itself is out of scope for this PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ci: only build images on push to main (or v* tag), not on feature branches
Some checks failed
build / Build and push OCI images (pull_request) Has been skipped
build / Lint and type-check (pull_request) Failing after 2s
cfcb81da6e
Packages should be cut at merge time, not on every push to a long-lived
feature branch. Drops the legacy chore/initial-setup trigger; main +
v* tags are now the only image-publishing paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ci: drop tag trigger - packages only on main push
Some checks failed
build / Build and push OCI images (pull_request) Has been skipped
build / Lint and type-check (pull_request) Failing after 2s
1a12f777d4
Tag pushes fire workflows regardless of which branch the tagged commit
is on, which lets feature branches publish images by accident. Only
push to main (which is what a PR merge produces) creates packages now.

Tagging a release still works: tag the merge commit on main, push the
tag - get-tags.nu reads `git describe` at the next main build to apply
the version tag to the image.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
docs: milestone-1 handoff for the PSA Systems v0.1.0 cutover
Some checks failed
build / Build and push OCI images (pull_request) Has been skipped
build / Lint and type-check (pull_request) Failing after 2s
1c5c3fd061
First doc in this repo's dev-docs/. Captures session state across all
four PRs (bunyip, mokosh-server, mokosh-clients, docker) plus the
SOPS/OAuth/DNS work still pending, the three-way URL split, the
image-tag policy, and the deploy order. Mirror copies in
mokosh-server/dev-docs and mokosh-clients/dev-docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
David merged commit e007e52938 into main 2026-05-17 03:30:12 +02:00
David deleted branch chore/bunyip-packages-to-psa-systems-private 2026-05-17 03:30:12 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
psa-systems/bunyip!2
No description provided.