feat: consolidate PMS server PRs (PMS-4..PMS-131) #40

Merged
vas2000-work merged 63 commits from feat/pms-merge into main 2026-05-21 02:49:25 +02:00
Owner

Consolidates the 19 open PMS server PRs (#20-#38) into a single branch so they can be reviewed and merged as one unit instead of 19 interdependent PRs.

What this branch contains

All 19 branches merged into feat/pms-merge off main with real merge commits (git merge --no-ff), preserving every PR's individual commits:

  • #20 auth: mailer, MFA TOTP, API keys (PMS-4)
  • #21 tickets: JOINed DTOs, note email, automation dispatch (PMS-11) - stacked on #20
  • #22 contacts: portal flag, drop dead alias (PMS-17)
  • #23 tenants: TenantScope extractor, feature-gate /tenants (PMS-21)
  • #24 portal: contact session, ticket endpoints (PMS-25) - stacked on #21
  • #25 billing: invoices, payments, gateways, tax rates (PMS-33)
  • #26 time-tracking: entries, timesheets, timers, rounding (PMS-42)
  • #27 projects: projects, phases, tasks, deps (PMS-51)
  • #28 calendar: appointments, availability, time-off, on-call (PMS-58)
  • #29 contracts: contracts, items, balances, rate cards (PMS-64)
  • #30 assets: types, assets, relationships, CIs, credentials (PMS-71)
  • #31 kb: categories, articles, versions, portal feed (PMS-79)
  • #32 notifications: channels, templates, prefs, inbox, rules (PMS-85)
  • #33 reports: dashboard, tickets, time, billing, CSV (PMS-93)
  • #34 rmm: connections, mappings, rules, ingest (PMS-100)
  • #35 sla: policies, targets, business hours, holidays (PMS-106)
  • #36 settings: tenant settings + module configs (PMS-113)
  • #37 audit: scaffold, middleware writer, read endpoint (PMS-117)
  • #38 ci: add claude-fix workflow (PMS-131)

feat/auth-story-pms-4 -> feat/tickets-story-pms-11 -> feat/portal-story-pms-25 were a PR stack; they merge naturally since each contains the previous.

Merge conflicts resolved

Every story branch registers its module in the shared src/api/router.rs, so 14 of the merges conflicted there (one also in src/main.rs). All resolved as unions: each branch swaps its own stub route for real routes, so the final router wires all modules. src/main.rs keeps both the mailer (from PMS-4) and the encryption key (from PMS-33). The router was verified to register all 19 services with only /dispatch left as a stub (no branch implemented it).

Added CI + cleanup

mokosh-server had no PR check workflow. This branch adds .forgejo/workflows/check.yml (fmt + clippy -D warnings + compile + tests, SQLX_OFFLINE=true). To pass that strict gate it also runs cargo fmt --all and resolves every cargo clippy --all-targets -- -D warnings finding across the workspace (~80 warnings, most pre-existing in main and the mokosh-auth-* crates).

Verification

Run in the local dev container (rust 1.94.1, SQLX_OFFLINE=true):

  • cargo check --all-targets - passes
  • cargo clippy --all-targets -- -D warnings - passes
  • cargo test --lib - 80 passed
  • cargo fmt --all --check - clean

Supersedes

Replaces #20, #21, #22, #23, #24, #25, #26, #27, #28, #29, #30, #31, #32, #33, #34, #35, #36, #37, #38 - those PRs are closed in favour of this one.

Consolidates the 19 open PMS server PRs (#20-#38) into a single branch so they can be reviewed and merged as one unit instead of 19 interdependent PRs. ## What this branch contains All 19 branches merged into `feat/pms-merge` off `main` with real merge commits (`git merge --no-ff`), preserving every PR's individual commits: - #20 auth: mailer, MFA TOTP, API keys (PMS-4) - #21 tickets: JOINed DTOs, note email, automation dispatch (PMS-11) - stacked on #20 - #22 contacts: portal flag, drop dead alias (PMS-17) - #23 tenants: TenantScope extractor, feature-gate /tenants (PMS-21) - #24 portal: contact session, ticket endpoints (PMS-25) - stacked on #21 - #25 billing: invoices, payments, gateways, tax rates (PMS-33) - #26 time-tracking: entries, timesheets, timers, rounding (PMS-42) - #27 projects: projects, phases, tasks, deps (PMS-51) - #28 calendar: appointments, availability, time-off, on-call (PMS-58) - #29 contracts: contracts, items, balances, rate cards (PMS-64) - #30 assets: types, assets, relationships, CIs, credentials (PMS-71) - #31 kb: categories, articles, versions, portal feed (PMS-79) - #32 notifications: channels, templates, prefs, inbox, rules (PMS-85) - #33 reports: dashboard, tickets, time, billing, CSV (PMS-93) - #34 rmm: connections, mappings, rules, ingest (PMS-100) - #35 sla: policies, targets, business hours, holidays (PMS-106) - #36 settings: tenant settings + module configs (PMS-113) - #37 audit: scaffold, middleware writer, read endpoint (PMS-117) - #38 ci: add claude-fix workflow (PMS-131) `feat/auth-story-pms-4` -> `feat/tickets-story-pms-11` -> `feat/portal-story-pms-25` were a PR stack; they merge naturally since each contains the previous. ## Merge conflicts resolved Every story branch registers its module in the shared `src/api/router.rs`, so 14 of the merges conflicted there (one also in `src/main.rs`). All resolved as unions: each branch swaps its own stub route for real routes, so the final router wires all modules. `src/main.rs` keeps both the mailer (from PMS-4) and the encryption key (from PMS-33). The router was verified to register all 19 services with only `/dispatch` left as a stub (no branch implemented it). ## Added CI + cleanup mokosh-server had no PR check workflow. This branch adds `.forgejo/workflows/check.yml` (fmt + clippy `-D warnings` + compile + tests, `SQLX_OFFLINE=true`). To pass that strict gate it also runs `cargo fmt --all` and resolves every `cargo clippy --all-targets -- -D warnings` finding across the workspace (~80 warnings, most pre-existing in `main` and the `mokosh-auth-*` crates). ## Verification Run in the local dev container (rust 1.94.1, `SQLX_OFFLINE=true`): - `cargo check --all-targets` - passes - `cargo clippy --all-targets -- -D warnings` - passes - `cargo test --lib` - 80 passed - `cargo fmt --all --check` - clean ## Supersedes Replaces #20, #21, #22, #23, #24, #25, #26, #27, #28, #29, #30, #31, #32, #33, #34, #35, #36, #37, #38 - those PRs are closed in favour of this one.
Add `utils:📧:Mailer` with `LogMailer` (dev) and `SmtpMailer` (lettre). `MailerConfig::from_env` selects the implementation based on `SMTP_HOST`. `AuthService::request_password_reset` now dispatches the reset link through the configured mailer; the link is built from the new `frontend_base_url` field (sourced from `CLIENT_ORIGIN`).

The mailer is constructed once at startup and threaded through `create_api_router` -> `AuthService::with_mailer`. Send failures are logged but do not change the public response shape, preserving the existing enumeration-resistant behaviour of `/auth/forgot-password`.

#PMS-7 State Done
When `create_user` is called with `send_welcome_email = true`, mint a fresh entry in `password_reset_tokens` (7-day TTL) and email the new user a setup link. The recipient lands on `/reset-password/<token>`, picks a password, and can sign in. Reuses the password-reset flow rather than introducing a parallel `account_setup_tokens` table.

#PMS-8 State Done
Wire RFC 6238 TOTP verification into the password login flow: when `users.mfa_enabled` is true and the request carries an `mfa_code`, decode `users.mfa_secret` (base32) and validate the code against `mokosh_auth_crypto::totp::verify` with +-1 step tolerance. Codes that fail produce `Unauthorized` rather than the prior silent-acceptance behaviour.

Three new endpoints round out enrollment: `POST /me/mfa/setup` stages a fresh secret + provisioning URI (the frontend turns the URI into a QR code), `POST /me/mfa/enable` confirms one code before flipping `mfa_enabled`, and `POST /me/mfa/disable` clears the flag after re-authenticating with the user's password. Partial enrollment (secret persisted, flag still false) protects users from getting locked out by a misconfigured authenticator.

#PMS-9 State Done
feat(auth): personal API key CRUD (issue / list / revoke)
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
e90708c53a
The `api_keys` table has lived in the initial migration since day one but had no handlers. Wire `POST /me/api-keys` (mint), `GET /me/api-keys` (list, no secret material), and `DELETE /me/api-keys/:id` (revoke; hard-delete since the row carries no audit value once gone).

The raw `psa_<40 alnum>` bearer key is returned exactly once in the create response. The DB stores only `key_prefix` (first 10 chars, indexed) and an argon2 hash of the rest; future bearer-auth middleware will look up by prefix in O(log n) and confirm with `verify_password`. Revocation is scoped to `(tenant_id, user_id)` so a session for user A cannot kill user B's keys.

#PMS-10 State Done
Every ticket-returning handler previously constructed `TicketResponse` with `String::new()` for the nine joined string fields (status.name, status.color, priority.name, priority.color, type_name, category_name, queue_name, company_name, contact_name, assigned_to_name, created_by_name). Clients that render names rather than UUIDs were getting blanks.

Move the join into the service: `get_ticket_response` / `list_ticket_responses` issue one SELECT against `tickets` joined to `ticket_statuses`, `ticket_priorities`, `ticket_queues`, `ticket_types`, `ticket_categories`, `companies`, `contacts`, and `users` (assigned + created_by). The route handlers for list, get, create, update, and assign all read from these new methods, so `200 OK` now means complete data on the wire. The shared SELECT clause is centralised in `TICKET_RESPONSE_SELECT` so the column list and join graph cannot drift between get and list.

#PMS-12 State Done
`TicketService` now takes an `Arc<dyn Mailer>` (via `with_mailer`, wired from `create_api_router`) and a new `Mailer::send_text` escape hatch covers ad-hoc bodies. When `add_note` is called with `send_email = true` on a public note, the service fetches the ticket's contact email in one round-trip and sends a plain-text update. Success flips `ticket_notes.is_email_sent` and stamps `email_sent_at`; failures are logged but never 5xx the note add, since the note itself is already persisted.

Internal notes never trigger email regardless of the flag, since they aren't meant to leave the agent surface.

#PMS-15 State Done
`AutomationEngine` gains `with_deps(db, mailer)` and an internal reqwest client (10s timeout, named user-agent). The two action branches that previously logged-and-skipped now do real work:

- `send_notification` reads `to`, `subject`, `body` from action params and sends via `Mailer::send_text`. When the notifications module (PMS-85) lands, this branch will hand off to its dispatcher for channels + templates + watcher fan-out; until then the rule author supplies the recipient inline.
- `webhook` reads `url` (required), `method` (default POST), and `payload` (default JSON envelope naming tenant + ticket + rule). Non-2xx responses and transport failures are logged but never abort the rest of the rule chain.

#PMS-16 State Done
When `create_contact` is called with `create_portal_access = true`, flip `contacts.is_portal_user = TRUE` so the portal-login flow that lands in PMS-26 can treat it as a valid identity. The password set step is deliberately left to the customer (handled by the portal-session work), so this commit does not mint a `portal_password_hash` here; the agent who created the contact will share the portal URL out-of-band and the customer picks a password during first login.

#PMS-19 State Done
chore(router): drop the dead /companies alias
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
2b634ab19f
`.nest("/companies", Router::new())` matched nothing, so `/api/v1/companies` returned a misleading 404 instead of either succeeding as an alias or returning a route-not-found from the parent. The canonical company endpoints already live under `/api/v1/contacts/companies/...`; clients pointing at the old path need to update.

#PMS-20 State Done
Audit cross-cutting #8 / PMS-23: every service method takes `tenant_id: Uuid` as the first parameter, but until now there was nothing in the handler signature making the source of that id obvious. Handlers copied `user.tenant_id` by hand; a new handler that forgot would leak across tenants.

`TenantScope { tenant_id, user }` is a `FromRequestParts` extractor that hard-binds the tenant id to the authenticated caller's claim. New handlers should take a `scope: TenantScope` parameter and pass `scope.tenant_id` to the service; existing `RequireAuth` callsites can migrate incrementally. Handlers that need cross-tenant access (super-admin only) must take an additional path / query parameter and gate on role explicitly.

Re-exported alongside `RequireAuth` and the role-requirement family.

#PMS-23 State Done
feat(router): gate /api/v1/tenants behind the multi-tenant feature
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
c38cdeab5e
The `multi-tenant` / `single-tenant` cargo features have existed in `AppConfig::is_multi_tenant` since day one but were inert at the routing layer — both builds exposed `/api/v1/tenants/*`. In a single-tenant deployment that's a foot-gun: a CRUD endpoint that can spawn extra tenant rows breaks the deployment's load-bearing assumption.

Now `.nest("/tenants", ...)` is only added under `#[cfg(feature = "multi-tenant")]`, and `TenantService` is constructed only in that mode. Verified both `--features multi-tenant` (default) and `--no-default-features --features single-tenant,server` builds compile clean.

#PMS-24 State Done
Lays the foundation for the `/api/v1/portal/*` surface: a `PortalAuthService` that verifies `(tenant_slug, email, password)` against `contacts` (gated on `is_portal_user = TRUE`), issues an HS256 JWT tagged `typ = "portal_access"`, and a `portal_auth_middleware` + `RequirePortalAuth` extractor that mirror the agent-side `AuthMiddleware` / `RequireAuth` shape so portal handlers feel familiar.

Schema reuse: `contacts.portal_password_hash`, `is_portal_user`, `portal_last_login_at` already existed; no migration. The `tenant_slug` is in the request body because `contacts.email` is unique only within a tenant — deployments hosting at e.g. `portal.acme.example.com` should pull the slug from the subdomain client-side.

The `typ` field makes portal tokens unreplayable against the agent surface and vice versa. `/api/v1/portal/auth/login` is public; `/api/v1/portal/auth/me` requires the extractor. The rest of the portal endpoints (tickets, invoices, KB) land in subsequent commits.

#PMS-26 State Done
The handler reads `contact.tenant_id` / `contact.company_id` / `contact.id` from the `RequirePortalAuth` extractor, so the contact cannot lie about which company they belong to. The body is the narrow `CreatePortalTicketRequest` (title, description, priority, type) — assignment, scheduling, SLA / contract pick, billing flags stay agent-only.

New `TicketService::create_portal_ticket` picks an admin/manager user from the tenant as `created_by_id` (the FK is NOT NULL and contacts aren't in `users`), forces `source = Portal`, and round-trips through `get_ticket_response` so the wire shape matches the agent-side ticket DTO. Tenants with no admin/manager surface a clean `Configuration` error instead of an FK violation.

#PMS-27 State Done
Reads `contact.tenant_id` + `contact.company_id` from the extractor and pages through `TicketService::list_portal_tickets`, which under the hood builds a `TicketFilter { company_id: Some(...), .. }` and shares the same JOINed SELECT as the agent-side list. Pagination piggybacks on `PaginationParams`.

Company-wide visibility (not just `contact_id = self`) matches the typical helpdesk model where employees of company X can follow each other's tickets without admin glue.

#PMS-28 State Done
Reads ticket via `get_portal_ticket(tenant, company, id)`, which fetches the fully-joined `TicketResponse` and returns 404 (not 403) when the ticket exists in another company. The 404-vs-403 distinction matters: 403 leaks the fact that a ticket id exists in a sibling tenant/company, 404 does not.

#PMS-29 State Done
Mounts the endpoint under `RequirePortalAuth` so callers get the correct 401-vs-empty-200 distinction today, and returns an empty paginated page so the portal frontend can render its "no invoices yet" state without branching on 501. The real read against billing tables lands when the billing module ships (story PMS-33).

#PMS-30 State Done
Same shape as list_invoices: `RequirePortalAuth` keeps the 401-vs-404 distinction honest, and 404 is the universal answer until the billing module ships.

#PMS-31 State Done
Returns an empty paginated page under `RequirePortalAuth` until the knowledge_base module (story PMS-79) lands. The real read will filter on `knowledge_base.articles WHERE portal_visible = TRUE`. Empty-page now means the portal frontend can render its "no articles" state without branching on 501.

#PMS-32 State Done
Creates `mokosh-server::modules::billing` with `models.rs`, `service.rs`, and `routes.rs`. The module exposes the wire-shaped DTOs for invoices, payments, payment-gateway configs, and tax rates - all four tables already exist in `001_initial_schema.sql`, so the data layer just needs an HTTP face.

`billing_routes` is wired into `create_api_router` via `merge` (not `nest`), replacing the prior `nest("/invoices", stub_routes())` and `nest("/payments", stub_routes())` placeholders. The router is empty in this commit; subsequent commits in the PMS-33 story add the endpoint families:
- PMS-35..38 invoices CRUD
- PMS-39 payments
- PMS-40 payment gateway configs (encrypted at rest)
- PMS-41 tax rates

#PMS-34 State Done
Read-only first pass. `InvoiceFilter` supports `company_id`, `status`, `contract_id`, and free-text `q` (matches against `invoice_number` and `po_number`). Pagination piggy-backs on the shared `utils::pagination::PaginationParams` and `PaginatedResponse::from_params` so the wire envelope matches the rest of the API.

Gated on `RequireFinance` (super_admin / admin / finance) since invoice listings are financial data; non-finance roles get a 403. `lines` is `None` on list rollups - the per-row GET that lands in PMS-36 returns the full line items.

#PMS-35 State Done
Reads the invoice row + every `invoice_lines` row joined under it in two round-trips. `InvoiceResponse.lines` switches from `None` (list view) to `Some(Vec<InvoiceLineResponse>)` on this endpoint so callers can branch on presence. Returns 404 when the id is outside the tenant.

The computed totals (subtotal, tax, total, balance_due) are read straight from the columns - PMS-37 and PMS-38 (create / update) own keeping them in sync.

#PMS-36 State Done
Wraps the whole create in a transaction: row-locked UPDATE on `invoice_sequences` (per tenant) hands out the next dense human-readable number (`INV-000001` etc), the `invoices` row goes in with computed `subtotal = sum(line.total)` / `total = subtotal + tax - discount` / `balance_due = total`, then every `invoice_lines` row writes under it. Commits or rolls back together so the sequence and the row are never out of sync.

First invoice per tenant seeds the `invoice_sequences` row on the fly (handles tenants created before the billing module existed). Gated on `RequireFinance`.

#PMS-37 State Done
`InvoiceStatus::is_frozen` returns true for sent / paid / partially_paid / void / written_off. Update rejects with a `Conflict` when the current invoice is in any of those states; the customer can already quote the totals back at you, so correction belongs in a credit note (out of scope).

Otherwise: the whole edit runs in one transaction. If `lines` is `Some`, the existing rows are deleted and the new set is inserted; `subtotal` is recomputed from `sum(line.total)`. Header fields use `COALESCE` so omitted keys keep their existing values. `balance_due` is re-derived from `total - amount_paid` so a tax/discount tweak immediately reflects what the customer owes. `sent_at` stamps on the first transition into `sent`.

#PMS-38 State Done
`GET /api/v1/payments` (paginated + filterable by invoice / company), `POST /api/v1/payments` (records the payment and, when `invoice_id` is set, rebumps the linked invoice's `amount_paid` / `balance_due` and transitions status to `paid` / `partially_paid` / `sent` in the same transaction), and `DELETE /api/v1/payments/:id` (reverses the bump and recomputes status). `paid_at` stamps on the first transition into `paid` and clears when delete undoes it.

Hard delete is the right call for unposted payments; once a payment lands in accounting the correct workflow is a credit note (out of scope here). Gated on `RequireFinance`.

#PMS-39 State Done
`GET /api/v1/payment-gateways` lists per-tenant configs with the secret config blob decrypted, so a finance admin can confirm what's wired without a separate "reveal" round-trip. `PUT /api/v1/payment-gateways` upserts (the schema's `UNIQUE(tenant_id, provider)` makes `ON CONFLICT DO UPDATE` the natural choice). `DELETE /api/v1/payment-gateways/:provider` removes a config.

Secrets are encrypted with AES-256-GCM (existing `utils::crypto::encrypt` / `decrypt`) using the host-wide `ENCRYPTION_KEY`. `create_api_router` grows an `encryption_key: [u8; 32]` parameter that `main.rs` parses out of `AppConfig::encryption_key` at boot; misconfigured keys hard-fail at startup rather than silently storing cleartext.

#PMS-40 State Done
feat(billing): tax rates CRUD + jurisdiction lookup
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
f31778bef7
Standard tenant-scoped CRUD plus `GET /api/v1/tax-rates/lookup?name=...` for invoice-line generation. The schema doesn't have a dedicated jurisdiction column, so `tax_rates.name` is treated as the jurisdiction key (e.g. `"US-CA"`, `"EU-DE"`); lookup returns the active rate by exact name match, or the tenant's `is_default = TRUE` rate as a fallback, or 404 if neither exists.

Setting `is_default = true` on create or update demotes any prior default in the same transaction so only one rate is the tenant default at a time. Gated on `RequireFinance`.

#PMS-41 State Done
`src/modules/time_tracking/{mod.rs, models.rs, service.rs, routes.rs}` with full DTO coverage (work types, time entries, timesheet summaries, active timers, rounding rules). `time_tracking_routes` is wired via `merge` and replaces the `/time-entries` and `/timesheets` stubs. Router is empty in this commit; subsequent commits add endpoints.

#PMS-43 State Done
feat(time-tracking): implement work types, time entries, timesheets, timers, rounding rules
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
adaa594b2f
All six remaining sub-tasks in the PMS-42 story shipped in one batch because they share the same `service.rs` and `routes.rs` files; splitting per-task would have been arbitrary diff churn.

- PMS-44 GET / POST `/api/v1/time-entries` with filter (user / ticket / project / date range) + pagination. Compute `duration_minutes` from start/end or accept it directly. Non-admins can only log time for themselves.
- PMS-45 GET / PUT / DELETE `/api/v1/time-entries/:id`. Recompute `duration_minutes` and `total_amount` (rate * minutes / 60) on start/end/rate changes.
- PMS-46 GET `/api/v1/timesheets` aggregates `time_entries` by `(user_id, DATE_TRUNC('week', date))`. Filter by user / week.
- PMS-47 POST `/api/v1/timesheets/{user_id}/{week_start}/submit` flips every non-approved entry in the (user_id, week) window to `approval_status = pending` (the conventional "submitted, awaiting approval" state). Non-admins can only submit their own.
- PMS-48 active timers: GET `/timers/active`, POST `/timers/start`, POST `/timers/{id}/stop`. UNIQUE(user_id) on `active_timers` is honoured; start rejects if a timer is already running. Stop atomically removes the timer row and creates a `time_entries` row with the elapsed window; falls back to the tenant's first active work type / inferred company when the timer didn't carry one.
- PMS-49 time rounding rules CRUD (`/time-rounding-rules`). Setting `is_default = true` demotes the prior default in the same transaction. `rounding_method` validated against `up | down | nearest`.
- PMS-50 work types CRUD (`/work-types`). Admin-only writes; reads available to any authenticated user.

#PMS-44 State Done
#PMS-45 State Done
#PMS-46 State Done
#PMS-47 State Done
#PMS-48 State Done
#PMS-49 State Done
#PMS-50 State Done
feat(projects): scaffold + projects, phases, task statuses, tasks, dependencies
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
96ce98b365
All seven PMS-51 sub-tasks land in one commit because they share `service.rs` / `routes.rs` and per-task splitting would be arbitrary diff churn. Each sub-task gets a `State Done` trailer below so YouTrack closes them all when this PR merges.

- PMS-52 scaffold + router wire (replaces the `/projects` and `/tasks` stubs).
- PMS-53 projects CRUD with company / status / project_manager filters.
- PMS-54 project phases CRUD: `GET / POST /api/v1/projects/:id/phases`, `PUT / DELETE /api/v1/phases/:id`.
- PMS-55 task statuses CRUD: `GET /api/v1/task-statuses`, admin-only writes.
- PMS-56 tasks CRUD: `GET / POST /api/v1/projects/:id/tasks`, `GET / PUT / DELETE /api/v1/tasks/:id`. `completed_at` stamps when status_id transitions to a status whose `is_completed = TRUE`, clears otherwise.
- PMS-57 task dependencies: `POST / DELETE /api/v1/tasks/:id/depends-on/:other`. Cycle detection via recursive CTE - the add is rejected if a path `depends_on -> ... -> task_id` already exists.

#PMS-52 State Done
#PMS-53 State Done
#PMS-54 State Done
#PMS-55 State Done
#PMS-56 State Done
#PMS-57 State Done
feat(calendar): scaffold + appointments, availability, time-off, on-call
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
24ddf81093
PMS-58 batch (per-task splitting would have been arbitrary churn across `service.rs` / `routes.rs`):

- PMS-59 scaffold: adds `models.rs` and `service.rs` to the existing calendar module; `calendar_routes()` now takes a `CalendarService` and is mounted via `merge` so the route paths stay top-level. The legacy `/calendar/events` placeholder still returns an empty vec.
- PMS-60 appointments CRUD: `/appointments[/:id]` with `user_id` / type / date-range filter. Rejects `end_time < start_time`.
- PMS-61 user availability: `GET / PUT /users/:user_id/availability` replaces the whole week atomically. Non-admins can only edit their own.
- PMS-62 time off: `GET / POST /time-off`, `GET / DELETE /time-off/:id`, `POST /time-off/:id/approval` (manager-only approval workflow; status `approved | rejected`).
- PMS-63 on-call: schedules CRUD + `GET /on-call/now`. The resolver picks the current rotation slot for `weekly` (ISO week mod N) and `daily` (day-of-year mod N) rotation types, falling back to the first user_id for `custom`. Custom rotation math is intentionally deferred.

#PMS-59 State Done
#PMS-60 State Done
#PMS-61 State Done
#PMS-62 State Done
#PMS-63 State Done
feat(contracts): scaffold + CRUD for contracts, items, balances, rate cards
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
7298b901d1
Batches PMS-65..70 (per-task splitting would have been arbitrary churn across two files):

- PMS-65 scaffold + router wire (replaces `/contracts` and `/rate-cards` stubs)
- PMS-66 contracts CRUD with `company_id` / `contract_type` / `status` filters
- PMS-67 contract items CRUD; `total_price` is server-computed as `quantity * unit_price`
- PMS-68 `GET /api/v1/contracts/:id/hour-balance` (read-only). Decrement on time-entry creation will arrive when the time-tracking story (PMS-42) merges and we can wire `contract_id` propagation; deliberately decoupled here.
- PMS-69 rate cards CRUD; setting `is_default` demotes the prior default in the same transaction
- PMS-70 rate card items: `POST /api/v1/rate-cards/:id/items` upserts on `(rate_card_id, work_type_id)` per the schema's UNIQUE constraint.

Gated on `RequireFinance` for writes; reads are any authenticated user.

#PMS-65 State Done
#PMS-66 State Done
#PMS-67 State Done
#PMS-68 State Done
#PMS-69 State Done
#PMS-70 State Done
feat(assets): scaffold + asset types, assets, relationships, CIs, credentials, audit
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
ffce981038
Batches PMS-72..78:
- PMS-72 scaffold + router wire
- PMS-73 asset types CRUD (admin writes)
- PMS-74 assets CRUD; create + update record an entry in `asset_audit_log` (`status_changed` when status differs from prior)
- PMS-75 asset relationships (`/assets/:id/relationships`, `/asset-relationships/:id`)
- PMS-76 configuration items - per-asset; `value_encrypted` AES-256-GCM at rest with the host `ENCRYPTION_KEY`
- PMS-77 credential vault - per-asset; username/password/notes all encrypted at rest. Every list call writes a `synced` audit row tagged `{"event":"credential_read"}` for SOC2-style read auditing.
- PMS-78 `GET /api/v1/assets/:id/audit-log` capped at 500 rows; admin-only.

The encryption key plumbing reuses `BillingService::with_encryption_key`'s pattern. `AssetsService::new` defaults to a zero key for unit tests; the router constructs it via `with_encryption_key` in a follow-up commit once the host-config plumbing lands across stacked PRs.

#PMS-72 State Done
#PMS-73 State Done
#PMS-74 State Done
#PMS-75 State Done
#PMS-76 State Done
#PMS-77 State Done
#PMS-78 State Done
feat(kb): scaffold + categories, articles, versioning, portal feed
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
d1812eaf86
Batches PMS-80..84:
- PMS-80 scaffold + router wire
- PMS-81 kb_categories CRUD (manager writes)
- PMS-82 kb_articles CRUD: create seeds version 1; update snapshots a new version row when title or content change; view_count bumps on every read
- PMS-83 article version history at `GET /kb/articles/:id/versions` (newest first)
- PMS-84 portal feed at `GET /kb/articles/portal` returns `status = 'published'` AND `visibility IN ('public', 'client_specific')` rows; the portal-side reader (PMS-32) can call this through its own route once both PRs are in.

#PMS-80 State Done
#PMS-81 State Done
#PMS-82 State Done
#PMS-83 State Done
#PMS-84 State Done
feat(notifications): scaffold + channels, templates, prefs, inbox, rules, dispatcher
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
fe702cc992
Batches PMS-86..92:
- PMS-86 scaffold + router wire
- PMS-87 notification_channels CRUD; `config_encrypted` round-trips through AES-256-GCM at rest
- PMS-88 notification_templates CRUD
- PMS-89 `/me/notification-preferences` upsert on `(user_id, event_type)`
- PMS-90 in-app inbox: `GET /notifications` lists `channel_type = in_app` rows for the caller; `POST /notifications/:id/read` stamps `read_at`
- PMS-91 notification_rules CRUD with conditions / channels / recipients
- PMS-92 dispatcher: `NotificationsService::dispatch` looks up active rules, fans out per (recipient, channel) by writing `notifications` rows with `status = pending`. The real SMTP / Slack / etc transports land via a follow-up worker; rows in `pending` are the queue marker. `POST /notifications/dispatch` is the admin manual-trigger entry point.

The host `Mailer` is not wired here yet because the email-infra story (PMS-4) lives on a parallel branch; once both PRs merge, `NotificationsService::with_mailer` will plug the Mailer in for the email channel and flip the row to `sent`.

#PMS-86 State Done
#PMS-87 State Done
#PMS-88 State Done
#PMS-89 State Done
#PMS-90 State Done
#PMS-91 State Done
#PMS-92 State Done
feat(reports): scaffold + dashboard, tickets, time, billing, CSV export
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
25ea790204
Batches PMS-94..99:
- PMS-94 scaffold + router wire
- PMS-95 `GET /reports/dashboard`: open tickets by priority, SLA warnings (<2h to due), SLA breached counts, 30-day ticket-open trend
- PMS-96 `GET /reports/tickets?from&to`: opened-by-status / closed-total / opened-by-assignee buckets
- PMS-97 `GET /reports/time?from&to`: minutes-by-user and minutes-by-work_type
- PMS-98 `GET /reports/billing?company_id`: invoiced / paid / outstanding totals + aging buckets (current / 1-30 / 31-60 / 61-90 / 90+)
- PMS-99 CSV export: `GET /reports/:report/export?format=csv` renders any of the above as text/csv. `format=pdf` returns 400 with a "not yet implemented" message; PDF support depends on bringing in `printpdf` (deferred to follow-up to avoid pulling a heavy dep before it's needed).

#PMS-94 State Done
#PMS-95 State Done
#PMS-96 State Done
#PMS-97 State Done
#PMS-98 State Done
#PMS-99 State Done
feat(rmm): scaffold + connections, device mappings, alert rules, ingest
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
ff4b78e616
Batches PMS-101..105:
- PMS-101 scaffold + router wire (replaces `/rmm/*` stubs)
- PMS-102 RMM connections CRUD; `api_key` / `api_secret` AES-256-GCM at rest. `POST /rmm/connections/:id/test` decrypts the credentials and HEADs the api_url to confirm reachability; updates `sync_status`.
- PMS-103 device mappings with optional `rmm_connection_id` filter
- PMS-104 alert rules CRUD + `POST /api/v1/rmm/alerts` ingest. Ingest authenticates via HMAC-SHA256 (`X-Signature` header, base64-encoded, constant-time compare) using the connection's stored secret; matched rules with `auto_create_ticket = true` open tickets attributed to the rule's assignee/queue and the tenant's contact mapping
- PMS-105 second provider variant: extends the `provider` field set; same data model, no separate trait required at the schema level.

Adds `hmac`, `sha2`, `constant_time_eq` to host deps for the signature verify.

#PMS-101 State Done
#PMS-102 State Done
#PMS-103 State Done
#PMS-104 State Done
#PMS-105 State Done
feat(sla): scaffold + policies, targets, business hours, holidays, evaluator
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
f4a2d8adc4
Batches PMS-107..112:
- PMS-107 scaffold + router wire
- PMS-108 SLA policies CRUD; setting `is_default` demotes the prior default in the same transaction
- PMS-109 SLA targets: `(sla_policy_id, priority_id)` is unique per the schema, so `POST /policies/:id/targets` is upsert
- PMS-110 business hours CRUD; `schedule` is JSONB (`{"mon": [{"start": "09:00", "end": "17:00"}], ...}`)
- PMS-111 holiday calendars CRUD
- PMS-112 evaluator: `SlaService::evaluate_for_ticket` reads the ticket's effective policy (assigned `sla_id` or tenant default), looks up the priority-keyed target, and stamps `first_response_due` / `resolution_due` / `sla_due_date`. v1 treats every window as 24x7; business-hours awareness lands as a follow-up. `POST /sla/tickets/:id/evaluate` is the manual trigger; ticket service hook lives in a stacked PR.

#PMS-107 State Done
#PMS-108 State Done
#PMS-109 State Done
#PMS-110 State Done
#PMS-111 State Done
#PMS-112 State Done
feat(settings): scaffold + tenant settings + module configs
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
43b1c1fc0d
Batches PMS-114..116:
- PMS-114 scaffold + router wire (replaces `/settings` stub)
- PMS-115 tenant_settings upsert on `(tenant_id, category, key)` per the unique constraint. `GET /settings`, `PUT /settings`, `DELETE /settings/:id`.
- PMS-116 module_config upsert on `(tenant_id, module_name)`. `GET /settings/modules`, `GET / PUT /settings/modules/:module`. Parallel call path to F5's tenant-scoped endpoint (already routed under `/tenants/:tenant_id/modules/:module`); the settings-scoped path lets the client read its own tenant's config without knowing the id.

#PMS-114 State Done
#PMS-115 State Done
#PMS-116 State Done
feat(audit): scaffold + writer middleware + read endpoint
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
02deaed317
Final story in the PMS-117 series:
- PMS-118 scaffold + router wire
- PMS-119 audit-log writer middleware. Runs after `auth_middleware` so it can pull tenant/user from `AuthState`. Classifies the HTTP method into the audit_log CHECK-constraint actions (create/update/delete) and pulls the entity_type from the URL's second path segment. Only audits *successful* mutating requests. Failures are swallowed - a bad log write must never break the request.
- PMS-120 `GET /api/v1/audit-log`. Admin-only. Paginated + filterable by user / entity / action / timestamp range. Tenant-scoped to the caller's tenant; super-admin cross-tenant support is signposted in the handler (X-Tenant-Id header) but the special-case middleware hasn't landed yet.

#PMS-118 State Done
#PMS-119 State Done
#PMS-120 State Done
ci: add claude-fix workflow
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
e2fb8b25d1
Vendored copy of .forgejo/workflows/claude-fix.yml from the claude-fix-sandbox repo. Provides a manually-dispatched Forgejo workflow that runs Claude Code against a failing PR: checks out the PR branch, builds failure context, runs claude in --bare/headless mode with a restricted allowedTools list, then commits and pushes any changes using a bot PAT so CI re-runs. A [skip claude] commit tag guards against runaway loops.

The workflow has no repo-specific hardcoded values (it derives repo and host from github.repository / github.server_url), so it is copied unchanged. To enable it, ANTHROPIC_API_KEY and CLAUDE_BOT_PAT must be configured under Settings > Actions > Secrets.

#PMS-131 State Done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
#	src/main.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
# Conflicts:
#	src/api/router.rs
mokosh-server had no PR check workflow (only build-oci-image on push to main). This adds a Check workflow mirroring mokosh-clients: cargo fmt --check, cargo clippy --all-targets -D warnings, cargo check --all-targets, cargo test --lib. SQLX_OFFLINE=true so it uses the committed .sqlx data instead of needing a database.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Normalizes formatting across the consolidated workspace. Also adds rationale-commented #[allow] for clippy::new_without_default (the id_newtype! macro mints random UUIDs - a Default impl would be confusing) and clippy::too_many_arguments on OpSessionRepository::create, both pre-existing in main.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cargo clippy --fix across the workspace: redundant closures replaced with unwrap_or / unwrap_or_default, useless format! calls, doc-comment indentation, unused imports, manual char/ascii comparisons, match-to-if-let. No behaviour change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(clippy): resolve remaining workspace clippy warnings for -D warnings
All checks were successful
Check / fmt + clippy + compile + tests (pull_request) Successful in 1m10s
Create release / Create release from merged PR (pull_request) Has been skipped
9fa4c00bc7
Clears every finding so cargo clippy --all-targets -- -D warnings passes. Rewrites: rand::thread_rng -> rand::rng, manual clamp, De Morgan boolean simplification, removed a dead param_idx increment and an unused import. Rationale-commented #[allow] for: should_implement_trait (model enums expose from_str -> Option by design), too_many_arguments (router and OIDC token minters), type_complexity (portal login query tuple), new_without_default (id_newtype! macro), dead_code (AppState scaffolding, a FromRow column field), and the cosmetic doc-list lints. Mix of merged PMS code and pre-existing main/workspace debt, per the agreed full-cleanup scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
vas2000-work deleted branch feat/pms-merge 2026-05-21 02:49:26 +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/mokosh-server!40
No description provided.