NEXT predates the access work, so the identity layer follows master: access comes from configured groups, instance administrators replace the site administration, and accounts are created by SSO or by an administrator. Kept from NEXT: manual publication with the Review changes panel and shared generator drafts, portable import/export (meguri/v2), the layered generation map with binding provenance, JSM provider changes, contact/approval semantics and the account-owned Person. Dropped: site administration, local sign-up, and the mandatory unique email with its contact/account migrations. The manual-publication migration is renumbered m_0010 after m_0009_access. Accounts without an email still get a Person in every workspace they reach, a changed sign-in email updates the account's person, and an account that loses access keeps its linked person so schedules stay valid. Instance administrators may use Sync and shared drafts. Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
25 KiB
Markdown
129 lines
25 KiB
Markdown
# Development
|
||
|
||
Requirements: Rust 1.97 (Cot itself requires 1.94), PostgreSQL 17+, and a browser. Node is only needed for browser automation or upgrading vendored browser assets; serving the application has no Node build step.
|
||
|
||
Use `.env.example` as the list of supported settings. Environment files are not loaded automatically. Start a dedicated PostgreSQL database and either run the disposable demo (`MEGURI_SEED_DEMO=true`, loopback only, built-in configuration, see below) or write an instance configuration file (start from `deploy/instance-config.example.yaml`; `auth.password_login: true` plus `auth.bootstrap_admin` gives a local administrator without an identity provider), set `DATABASE_URL` and `MEGURI_CONFIG_FILE`, then run `cargo run`. Startup fails without a configuration file. Database migrations and the configuration sync run before the server accepts requests; the generated bootstrap password is logged. There is no registration: create further password users on the instance page, or sign in through an OIDC provider.
|
||
|
||
For an isolated Windows development database, use `scripts/dev.ps1`. It uses installed PostgreSQL binaries, creates data under ignored `target/`, binds the database and app to loopback, and starts disposable demo mode. The demo password is generated on every start; it appears on the login screen and in `target/local/app.log`. `scripts/stop-dev.ps1` stops only the processes started for this workspace.
|
||
|
||
Required checks:
|
||
|
||
```text
|
||
cargo fmt --check
|
||
cargo clippy --all-targets -- -D warnings
|
||
cargo test
|
||
```
|
||
|
||
PostgreSQL integration tests require `MEGURI_TEST_DATABASE_URL` pointing at a disposable database with CREATEDB (lifecycle and configuration-sync tests create and drop uniquely named databases, because sync disables every unconfigured workspace). Run `cargo test --locked --tests -- --ignored --test-threads=1`; this covers `tests/integration.rs`, `tests/access.rs`, `tests/oidc.rs` and `tests/demo_lifecycle.rs`. Most scheduling tests create uniquely keyed workspaces and users through the real configuration and group path; lifecycle tests test protection, reset, retention, and reseeding. Never use a production database.
|
||
|
||
CI uses an explicit Debian Node job container and the PostgreSQL service hostname `postgres`, for both Gitea Actions and GitHub Actions. Loopback inside the job does not reach the database service. No host database port is published. A bounded authenticated `psql SELECT 1` check runs before compilation; database networking/authentication failures stop the job rather than producing a pool timeout in every integration test. The application and browser tests still share job-container loopback.
|
||
|
||
Browser checks: sign in with a demo account; inspect the calendar; create a request as one user and approve it as another; verify the publication status separately; verify that a user outside the workspace cannot read it. Use both desktop and narrow viewports. A test failure is not a reason to relax an authorization or scheduling invariant.
|
||
|
||
## Operational limitations
|
||
|
||
When configuring JSM, copy the entire team identifier between `/teams/` and `/on-call` in the Operations URL, including `og-` when present. Cloud ID and Meguri workspace ID are separate UUIDs. Operations rejects collection page sizes above 50; directory search has its own pagination contract.
|
||
|
||
Provider display names use the resource name followed by a stable ownership marker. Do not remove the marker manually; it supports recovery after a lost checkpoint. Existing technical names migrate on a successful publication, subject to the ordinary drift policy.
|
||
|
||
An exact-time JSM schedule can show a base rotation containing the current assignee (or `noone` before coverage begins) and generated future overrides in the provider UI. These are the exported effective assignments, not additional approval requests. A previously failed native-rotation write with no checkpoint is not automatically adopted: inspect it and explicitly overwrite the owned test schedule when repairing it. A successful remote create alone is never a synchronized publication.
|
||
|
||
For automated browser checks, run `npm ci`, `npx playwright install chromium`, then `npm run test:browser` against a freshly started demo (`MEGURI_SEED_DEMO=true`). The suites read the generated password from `/api/session` (`tests/support/browser.cjs`), so they need no configuration; set `MEGURI_BROWSER_URL` for a non-default address. By default the tests use installed Chrome; set `MEGURI_BROWSER_CHANNEL=chromium` for downloaded Chromium or `MEGURI_BROWSER_EXECUTABLE` for a specific binary. Workspaces cannot be created from the UI or API, so suites that need an isolated workspace create a fresh password user through the instance administrator API with groups from the demonstration configuration. `tests/browser-access.cjs` covers the login guide, absence of registration, the SSO error banner, the instance page and its read-only rules, employee permission editing (including force without approval), employee and outsider views, and workspace isolation. `tests/browser-calendars.cjs` checks email participants/absences, country defaults, reactive CSV import, workspace working hours, policy flows, and display-timezone isolation. `tests/browser-configuration.cjs` checks removal dependencies/redirects, native routing drag/drop, saved positions, insertion/removal renumbering, and mobile controls. Screenshots go under ignored `test-results/`. The suites modify demo data (some empty a demo workspace); restart the demo before repeating a run.
|
||
|
||
Windows development needs complete PostgreSQL server binaries and their `share/` data files, not a client-only installation. Pass `-PostgresBin` to use a portable distribution. The loopback-only local database uses trust authentication and is not a production configuration. The script serves a copy of the executable so Cargo can rebuild while it runs.
|
||
|
||
Read [testing.md](testing.md) when changing rules and [deployment.md](deployment.md) before preparing GKE deployment.
|
||
|
||
## Demo lifecycle
|
||
|
||
`MEGURI_SEED_DEMO=true` enables disposable demo mode, not just seed insertion. It is limited to loopback and a single `all` or `web` process, uses the built-in configuration `src/demo-config.yaml` (any `MEGURI_CONFIG_FILE` is ignored) and needs no OIDC. Startup verifies the previous database mode before migrations, resets only positively marked demo application data, migrates, synchronizes the built-in configuration and seeds four examples. Normal startup records `normal`, including migration/worker roles. A missing or unreadable provenance marker never grants deletion authority. The existing development `meguri` DB will therefore be preserved on its first transition to this mode.
|
||
|
||
When the configured DB is protected, Meguri selects a separate `meguri_demo_<stable fingerprint>_<slot>` DB. The application login needs CREATEDB, or an administrator must supply an empty `MEGURI_DEMO_DATABASE_URL` with suitable permissions. Credentials are never shown in the browser/log notice. Protected fallback candidates are skipped, never dropped or overwritten. Isolation failure aborts startup. Do not manually mark a production database as demo. Another active client blocks reset; stop the old demo process before restarting.
|
||
|
||
The permanent header explains creation-based expiry and any protected-DB fallback. Defaults: workspaces 24 hours; accounts created by an administrator during the run 48 hours; requests 2 hours; audit/history 6 hours; physical purge 10 minutes after soft deletion. Configure seconds with the `MEGURI_DEMO_*` settings in `.env.example` (60 seconds–30 days). Sweeps run every minute and also re-run the configuration sync and seeding. Nested configuration expires with its owning aggregate to preserve dependencies. The seeded accounts remain; baseline workspaces are recreated and reseeded after expiry. No cleanup runs in normal mode. Demo rejects real provider integrations/publication.
|
||
|
||
Accounts (one password per start, shown on the login screen and in the log; there is no registration): `admin` is the instance administrator (a bootstrap account), `alice` administers all four workspaces, and `bob`, `carol`, `dave`, `erin` are employees. Groups are assigned as an instance administrator would assign them, and the built-in configuration is in the production format. Bob belongs to Engineering and Regional Platform; Carol to Engineering, Support and Commerce; Dave and Erin to Engineering. Engineering uses `projects`, `responsible`, and `on_call`; Support has independent `queues`, `agents`, and `lead` variables. All generated state is activated through the regular preview/commit code path with the mock provider.
|
||
|
||
Additional browser suites: `browser-absence.cjs` checks collapsed holidays, DD-MM-YYYY input, live bulk absence impact, range replacements and force; `browser-generation.cjs` checks demos, their permanent notice and isolated memberships, visual data/binding editing, early errors, preview without writes, stable generated IDs, and mobile forms. Restart after browser tests to return to the four clean examples.
|
||
|
||
Authentication is OpenID Connect plus optional password accounts; see [deployment](deployment.md#instance-configuration) for configuration and Keycloak setup. No external JSM account is needed to run the mock provider. JSM writes require explicit integration configuration and workspace-scoped environment credentials. Contract tests use a fake HTTP server, never a production account.
|
||
|
||
Workspace configuration edits take effect when saved. Historical queries use saved configuration revisions. There is no user-configurable delayed activation of a whole configuration revision in the first release.
|
||
|
||
`tests/browser-cover.cjs` exercises daily volunteering/approval and optional comments in a separate disposable workspace, with desktop and mobile screenshots. Configuration navigation is grouped under Setup.
|
||
|
||
Navigation and language coverage: tests/browser-navigation.cjs checks direct menu/workspace links, reload and browser history, saved language versus browser preference, Russian/French display, safe interpolation, catalog completeness and the prohibition on literal template copy. It runs in npm run test:browser.
|
||
|
||
|
||
Demo workspaces import `src/demo-holidays-2026.csv` through the regular holiday service: 15 Cyprus dates supplied by the project owner, the 8 England and Wales bank holidays of 2026 from https://www.gov.uk/bank-holidays.json (verified 2026-10-07), and the 13 statutory Armenian public holidays/days off of 2026 (listed from the Labour Code, not re-verified against an official calendar). This is a fixed 2026 example, not a recurring calendar or an annual-date extrapolation. Demo accounts also differ in scheduling country so that country holidays are visible: alice and dave Cyprus, bob and erin United Kingdom (`GB`), carol Armenia (`AM`).
|
||
|
||
Demo workspaces: Acme Engineering (project priorities + weekly cover), Customer Support (daily queues), Regional Platform (dictionary regions → nested enabled services, region-specific handovers/rotation lengths, shared commander), Commerce Operations (product → tier dictionary lookup, variable escalation delays/repeats/working windows, shared weekly fallback). Alice administers all four; Bob is an employee in Acme/Regional, Carol in Acme/Support/Commerce. Regional times use the workspace scheduling zone, not a per-region zone. Disabled Sandbox and Legacy export illustrate filtering; enabling the latter requires adding its missing retired tier or choosing an existing tier. All examples use normal generation preview/apply and demo cleanup; provider remains local preview.
|
||
|
||
The main Acme Engineering demo contains six services (Payments, Search, Billing, Identity, Data platform, Developer portal), each with two or three ordered owners drawn from five login members: alice, bob, carol, dave, erin. All five share the weekly Monday 11:00 Time off rotation. Dave and Erin only access Acme; Carol also has Support/Commerce access through her groups. Other demo workspaces remain separate. The login password is the one generated for the run.
|
||
|
||
## Single workspace schema
|
||
|
||
Migration 8 replaces the former tenant/team tables with workspaces and workspace memberships. It requires an empty legacy schema; it refuses to erase a populated non-demo database. Demo startup resets only provenance-verified demo data before migrations and recreates four independent workspaces. No history conversion or runtime compatibility layer is provided. All settings and generation state share the workspace revision. API paths are `/api/workspaces/{workspace}`; navigation uses `?workspace=...`. PostgreSQL integration tests use the dedicated `meguri_workspace_test` database.
|
||
|
||
The default Engineering example stores projects as one list of dictionaries: each entry has `name` and an ordered `responsible` person list. The Project iteration reads both fields directly from its current entry; no parallel responsible lookup variable is needed. The independent `on_call` list still defines the shared weekly rotation.
|
||
|
||
Generation map uses locally vendored Cytoscape.js 3.33.1 (`static/vendor/cytoscape.js`, adjacent MIT license), with no CDN requests or Node build requirement. Its preset lane layout and inspector live in `static/generation-map.js` / `.html`; engine trace data is returned by the authorized generation-preview endpoint. Run `node tests/browser-generation-map.cjs` against the disposable demo for nested iterations, filtering, diagnostics, read-only behavior, canvas interactions and mobile layout.
|
||
|
||
## Browser-check sandboxes
|
||
|
||
Browser suites that change data cannot reuse the example workspaces: approved replacements are permanent history, so a schedule that has one can never be removed. Start the demo with `MEGURI_DEMO_SANDBOXES=14` to add empty `Sandbox 1..14` workspaces, each with its own administrator group `sandbox-N-leads` and employee group `sandbox-N-staff`. Every mutating suite creates a throwaway password user in its own sandbox through the instance-administrator API. Restart the demo before each full run; sandboxes are not reset.
|
||
|
||
Deleting an exported resource in JSM does not remove the desired configuration in Meguri: the next publication recreates it and its managed coverage, without requiring overwrite. To remove it permanently, remove it in Meguri. Existing external edits still obey the drift policy.
|
||
|
||
## Explicit publication and shared drafts
|
||
|
||
Saving workspace configuration, approving replacements, importing data and editing generator drafts never authorize provider writes. These changes persist in PostgreSQL and are visible to other authorized members. Ordinary unfinished modal forms remain local until Save. Generator drafts autosave separately, including invalid intermediate values, with their own optimistic revision and workspace scope.
|
||
|
||
The top Review changes panel compares desired resources with the last successfully published document. Its Sync button is yellow when unpublished changes exist. Sync validates/applies the shared generator draft and explicitly authorizes a specific workspace revision; stale reviews fail. Provider status, errors and create/update/remove counts live in this panel. Failed authorized publications may retry the same snapshot; newer saved changes must never enter those retries. Successful jobs do not automatically refresh their horizon: the displayed coverage end requires a further explicit sync to extend it.
|
||
|
||
The worker persists the authorized document and publishes that snapshot, not the newest workspace document. A click while a publication lease is active is rejected. Existing jobs are paused by the manual-publication migration until an explicit click. Published snapshots survive history pruning. Tests cover no writes before a click, pinned retries across later edits, no periodic writes after success, scoped shared-draft persistence, stale concurrent edits, resource review against the published baseline, and the browser workflow across sessions.
|
||
|
||
## Compact recurring provider rotations
|
||
|
||
A plain base rotation whose effective published coverage matches its ordered calendar-day cycle is exported to JSM as one finite recurring rotation named after the schedule. The engine supplies the current cycle phase, ordered participants and calendar-day shift length; the provider only translates these fields to daily/weekly API values. The remote schedule ID and ownership marker stay unchanged. A publication replaces the old per-shift rotations rather than accumulating new ones. Native boundary precision must be representable; irregular effective coverage and replacements retain the exact interval export. The canonical expanded timeline remains available for verification and preview. Tests cover mid-shift starts, pre-anchor phases, participant ordering, DST, replacements, migration of an existing owned schedule and idempotent repeated publication.
|
||
|
||
When an owned JSM schedule has one current and one desired rotation, edits preserve the rotation ID through the dedicated rotation PATCH endpoint. Schedule metadata is patched without a rotations array. Verified intermediate writes are checkpointed; a durable pending-step snapshot recovers a lost response without mistaking that exact planned state for external drift. Tests model nested rotation replacement by schedule PATCH and require stable rotation identity across participant reorder, rename, retry and unchanged republish. Historical rows already retained by JSM are not deleted by this repair.
|
||
|
||
Workspace state save/reload controls live beside Sync in the top Review changes panel. The localized hint distinguishes shared draft autosave, validated local application, and explicit publication; Sync includes application, so a separate save is optional. Reload retrieves the shared draft, not a historical rollback. Removing a variable or generation rule requires confirmation before mutating or autosaving the draft. Browser regression covers cancellation, confirmation, top-panel controls and narrow layout.
|
||
|
||
JSM coverage rotation names use the schedule name, including priority coverage and the exact-time base rotation. Multiple finite rotations append the interval start in the schedule IANA timezone with date, time and UTC offset to distinguish DST folds. This is presentation only; ownership markers, coverage, checkpoints and explicit publication remain unchanged. Existing rotation names update on the next authorized synchronization.
|
||
|
||
Manual and generated routing editors expose the existing greater-than and less-than operations. Priority follows alert severity (P1 highest); P3 and above is represented as less-than P3 with negate=true, including the boundary. The editors show comparison controls without an additional priority instruction; the provider wire contract is unchanged.
|
||
|
||
Publication status exposes the lease expiry and a server-calculated publication_busy flag. Sync is disabled for an active lease, not merely the persisted publishing state. The review panel explains restart recovery and expired leases; normal polling refreshes eligibility. Restart never clears a live lease. PostgreSQL regression checks active-lease rejection and explicit reauthorization after expiry.
|
||
|
||
Nested binding paths retain the raw input text while typing, including trailing spaces and slash separators. The parsed path still updates live for shared drafts and server previews. Reopening shows the normalized path; changing source resets it. Browser regression types a nested list path character by character and checks its resolved value, edits, reopening and source reset.
|
||
|
||
JSM omits `repeat` when escalation repeats are disabled. Read-back and drift comparison accept an absent repeat field only for an expected escalation with `waitInterval: 0` (the provider disabled representation). Present repeat objects and enabled repeats remain strictly compared. Fake-server regressions reproduce omission, recovery without a checkpoint, idempotence and detection of externally enabled repeats. No overwrite policy is needed to recover an otherwise matching escalation.
|
||
|
||
Sync immediately shows submitting feedback, then stays disabled for an authorized pending job as well as an active publication lease. Pending publication has a visible queue message. The UI polls every two seconds while queued or leased and keeps the twenty-second idle refresh; expired leases remain manually recoverable. A successful sync request updates local queue status before refreshing the workspace. Browser regression holds the queued response and verifies disabled state and automatic completion without reload.
|
||
|
||
Generated escalation steps support Everyone on the team in addition to schedules and people. The optional team_all flag defaults to false for existing drafts; team recipients do not resolve an individual binding. They compile to Recipient::TeamAll and JSM uses the configured workspace integration team ID with notifyType=all. Delay and acknowledgment condition remain explicit. Generator, wire-contract and browser regressions cover the 30-minute team step and legacy recipients.
|
||
|
||
## Layered generation map and value provenance
|
||
|
||
The read-only map uses three separated horizontal bands: variables, generators, resources. Generator columns follow saved execution order; variables sort by first consumer, and generated resources occupy their producer column. Manual resources are included in the resource band with explicit manual attribution. Shapes distinguish bands; resource colors distinguish schedules, escalations and routes. Expanding output groups retains these bands.
|
||
|
||
Server preview provides binding provenance using the same resolver as generation: source bindings, iteration sources, lookup keys, list items, intermediate path values, final values, node identities and errors. The browser presents this captured snapshot rather than re-evaluating scheduling logic. An inspector popover supports history, source navigation and per-pass examples. Dotted address segments are independently colored, with neutral separators; quoted keys containing dots remain intact. Generated names retain separate bound and literal affix segments, and resource fields link to their binding or explicit default/manual origin. Clicking the map backdrop closes it; clicks on the canvas only clear selection. Tests cover provenance, layout, manual resources, source exploration and safe text rendering.
|
||
|
||
The value explorer renders source traversal as ordered monospace steps with neutral arrows and a selected-step indicator. Source navigation is a separate footer action; producer fields are independent choices rather than sequential path steps. Browser coverage checks stage values and separate source navigation.
|
||
|
||
## Portable generation configuration
|
||
|
||
[meguri/v2](portable-config.md) defines compact JSON import/export (with legacy v1 import support) for variables and generators only. Merge by name retains omitted inputs; nested identity keys preserve resource identity across editing and repeated imports. People transfer by email, manual resource references by exact name, and generated links by generator/block. The server provides a read-only parsed/expanded preview and revalidates before saving the shared draft with workspace and draft revisions. Provider writes require a later explicit Sync. Tests cover cross-workspace remapping, repeated imports, unknown fields, scoped permissions, stale previews, email contacts, parse errors and browser application.
|
||
|
||
### Compact portable configuration
|
||
|
||
`meguri/v2` is the default export: ordinary JSON variables, typed wrappers only for special scalar types, compact binding references, flattened output fields, readable output keys and omitted semantic defaults. The v1 importer remains supported. Inactive binding metadata must never be exported. Human output keys persist in shared drafts; list entries reconcile by unique name/value first and position for unmatched edits. The preview remains authoritative for identity-changing edits. Regression checks compare expanded resources, repeat imports and actual size reductions.
|
||
|
||
The Import / Export dialog includes a localized Guide tab with a complete compact v2 reference, defaults, binding scope, operations, limits, merge semantics and a runnable example covering all output types. Copy guide copies every section as Markdown for hand editing or LLM use, without workspace data. Switching tabs preserves import text and does not apply it. Browser checks verify copying, sample preview, tab preservation and mobile layout.
|
||
|
||
To exercise random team escalation, select A random team member in a generator or import `{"after":30,"team":"random"}` in meguri/v2. No person binding is required. Use the local fake provider for verification; notification selection belongs to the external provider.
|
||
|
||
Email-only contacts do not approve replacements. Test ordinary requests, swaps and absence groups with account/contact combinations, including immediate application with no approvers and retained approval for real members. OIDC/email account linking remains deferred; never infer membership from an email match.
|