M4: sync pull and push (E-31, E-32), audit writer and diff, FCM sender, push batching, scheduler, pruning, rotate-epoch
- src/sync/: pull with UNION ALL paging and cursor_ahead; push processing of SPEC §14.4.4 steps 1 to 12 in one BEGIN IMMEDIATE with a savepoint per mutation, LWW with delete-wins, tombstones, clock_skew, replay from sync_mutations, sync_batch for more than 3 notifiable changes; 90-day pruning. - src/audit/: SqlAuditWriter and the §14.4.7 diff (now the default in AppState::new). - src/push/: bounded PushHub (100, drop oldest), FCM HTTP v1 client with service-account JWT (RS256, jsonwebtoken on aws-lc-rs) and token cache, sender with 1/5/25 s retries and token clearing; disabled without credentials. - src/tasks/: start installs push and spawns the 06:00 generation (and at start) and 04:00 pruning loops on the injectable clock. - cli: rotate-epoch; serve uses into_make_service_with_connect_info; stale doc comments updated; /i/ empty code gets the HTML 404. - tests: sync_api, push_fcm (local stub of Google's endpoints), scheduler_daily; skeleton routes test now asserts no route is 501. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+25
-11
@@ -48,18 +48,18 @@ database is then `$KVITT_DATA_DIR/kvitt.sqlite`. Every connection uses WAL, `for
|
||||
|
||||
| Command | State in M2a | Exit codes |
|
||||
|---|---|---|
|
||||
| `serve` | Works: migrations, `tasks::start`, HTTP server; SIGTERM drains in-flight requests for up to 10 s. | 0 on clean shutdown |
|
||||
| `serve` | Works: migrations, `tasks::start` (push sender, scheduler, pruning), HTTP server with the client address in `ConnectInfo` (rate limiter); SIGTERM drains in-flight requests for up to 10 s. | 0 on clean shutdown |
|
||||
| `migrate` | Works: migrations only. | 0 |
|
||||
| `healthcheck` | Works: `GET http://127.0.0.1:<port of KVITT_BIND>/health`, used by the compose healthcheck (no curl in the image). | 0 if 200, else 1 |
|
||||
| `bootstrap` | Works (M3): cancels a pending `admin_setup` invite, prints a new code, link and expiry; `already initialized` on stderr when users exist. | 0 ok, 2 already initialized |
|
||||
| `pair-device --user <user_id>` | Works (M3): prints a `device` invite for the user like `bootstrap`. | 0 ok, 3 unknown user |
|
||||
| `rotate-epoch` | Prints `not implemented in M2a` (M4). | 2 |
|
||||
| `rotate-epoch` | Works (M4): sets `meta.sync_epoch` to a new random value and prints it (SPEC §10.9 restore step 5). | 0 ok, 1 database error |
|
||||
|
||||
## HTTP API
|
||||
|
||||
Every route of SPEC §7 is registered in `src/router.rs`. `GET /health` (E-1) and `GET /setup/status` (E-2) are
|
||||
implemented by M2a, E-3 to E-30 and E-33 by M3; E-31 and E-32 return 501 `not_implemented` until M4 fills in
|
||||
`src/api/sync.rs`. The rules of SPEC §14.3.1 are layers in `router.rs`:
|
||||
implemented by M2a, E-3 to E-30 and E-33 by M3, E-31 and E-32 by M4 (`src/api/sync.rs`, protocol in `src/sync/`).
|
||||
`GET /i/` with an empty code answers the landing page's HTML 404. The rules of SPEC §14.3.1 are layers in `router.rs`:
|
||||
`X-Kvitt-Protocol: 1` on authenticated routes (426 otherwise, checked before the token), `X-Kvitt-Server-Version`
|
||||
on every response, body limits 1 MiB / 5 MiB for `PUT /me/picture`, JSON 404 for unknown paths.
|
||||
|
||||
@@ -85,8 +85,11 @@ the wire contract makes `tests/openapi_contract.rs` fail against `contracts/open
|
||||
* **Recurring generation (M3, for M4's scheduler):** `api::recurring::generate_due_periods(&AppState, template_id)`
|
||||
generates the due periods of one template (SPEC §9.5); `api::recurring::generate_all_due(&AppState)` runs it for
|
||||
every non-deleted, non-paused template.
|
||||
* **Background tasks (M4):** `src/tasks/mod.rs` has `pub async fn start(state: AppState) -> Result<(), AppError>`,
|
||||
called once by `serve` after migrations. In M2a it starts nothing.
|
||||
* **Background tasks (M4):** `src/tasks/mod.rs` has `pub async fn start(state: AppState) -> Result<AppState, AppError>`,
|
||||
called once by `serve` after migrations; it installs the push queue in the returned state (the router is built from
|
||||
it) and spawns the FCM sender, the recurring scheduler (at start and daily 06:00 Europe/Stockholm, through
|
||||
`generate_all_due`) and the `sync_mutations` pruning (daily 04:00). The scheduler loop (`tasks/scheduler.rs`) reads
|
||||
the injectable `AppState.clock` and re-checks it at least every 30 s, so clock jumps are handled.
|
||||
* **Audit (`src/audit/mod.rs`, M4 implements, M3 and M9 call):**
|
||||
|
||||
```rust
|
||||
@@ -99,10 +102,12 @@ the wire contract makes `tests/openapi_contract.rs` fail against `contracts/open
|
||||
`AuditEvent` carries entity type and id, action, source, actor user and device, `client_at`, `server_at`, and
|
||||
the wire object before (`None` for create) and after the change as JSON; the writer derives `changes` per SPEC
|
||||
§14.4.7 and inserts the row with a `server_version` from the caller's `BEGIN IMMEDIATE` transaction (`conn`).
|
||||
`AppState.audit` holds `NoopAuditWriter` until M4 replaces it.
|
||||
`AppState.audit` holds `SqlAuditWriter` (M4; the diff is `audit::diff::changes`).
|
||||
* **Push (`src/push/mod.rs`, M4 implements, M3 calls):** `PushQueue::enqueue(&self, message: PushMessage)` with
|
||||
`PushMessage { push_type: PushType, entity_id: Option<Uuid>, actor_user_id: Uuid, count: u32 }` and the eight
|
||||
types of SPEC §14.6. It never blocks or fails; call it after commit. `AppState.push` holds `NoopPushQueue`.
|
||||
types of SPEC §14.6. It never blocks or fails; call it after commit. `AppState::new` holds `NoopPushQueue` (push
|
||||
disabled); `tasks::start` replaces it with `PushHub` (bounded queue of 100) drained by `push::Sender` (FCM HTTP v1,
|
||||
OAuth2 service-account JWT, retries 1 s / 5 s / 25 s) when `KVITT_FCM_CREDENTIALS` exists.
|
||||
* **Recurring dates (`kvitt-domain`, for M3's generation function and M4's scheduler):**
|
||||
`Period` (`YYYY-MM`, `FromStr`/`Display`, `next()`, `days()`, `clamped_date(day_of_month)`),
|
||||
`next_candidate_period(start_period, end_period, last_generated_period) -> Result<Option<Period>, RecurrenceError>`
|
||||
@@ -117,12 +122,21 @@ From `server/`: `cargo fmt --check && cargo clippy --all-targets -- -D warnings
|
||||
Kotlin-only analytics files are parsed and skipped).
|
||||
* `crates/kvitt-server/tests/openapi_contract.rs`: the generated document equals `contracts/openapi.yaml` after
|
||||
the normalisations documented at the top of the file, plus self-tests that the comparison catches changes.
|
||||
* `crates/kvitt-server/tests/skeleton/`: migration identity and pragmas, every route registered (501), E-1, E-2,
|
||||
* `crates/kvitt-server/tests/skeleton/`: migration identity and pragmas, every route registered and implemented, E-1, E-2,
|
||||
protocol and token rules, error shapes against `contracts/examples/errors/`, wire types against every example in
|
||||
`contracts/examples/`.
|
||||
|
||||
M3 and M4 put their integration tests in `tests/core_*.rs`, `tests/sync_*.rs`, `tests/scheduler_*.rs`,
|
||||
`tests/push_*.rs`. No `unwrap`/`expect` in non-test code (a crate-level `deny` enforces it under clippy).
|
||||
* `crates/kvitt-server/tests/core_api/` (M3): E-2 to E-30, E-33, generation, rate limiting, pictures, CLI.
|
||||
* `crates/kvitt-server/tests/sync_api/` (M4): every rule of SPEC §14.4 (pull, push steps 1 to 12, rejection reasons,
|
||||
§9.10 conflicts, 14-day offline scenario, the push examples), audit writer, `rotate-epoch`, end to end push → pull,
|
||||
`ConnectInfo` rate-limit buckets over real TCP.
|
||||
* `crates/kvitt-server/tests/push_fcm/` (M4): payloads against `contracts/examples/push/`, recipients, OAuth2 flow
|
||||
and token cache, retries, token clearing, queue overflow, push disabled, against a local stub of Google's endpoints
|
||||
(an RSA key is generated per test; none is committed).
|
||||
* `crates/kvitt-server/tests/scheduler_daily/` (M4): 06:00 and 04:00 across both DST changes, run at start, clock
|
||||
jumps, catch-up through `generate_all_due`, paused and deleted templates skipped, 90-day pruning.
|
||||
|
||||
The M4 binaries reuse M3's harness (`tests/core_api/common.rs`) and `tests/sync_api/m4.rs` through `#[path]`. No `unwrap`/`expect` in non-test code (a crate-level `deny` enforces it under clippy).
|
||||
|
||||
## Docker image
|
||||
|
||||
|
||||
Reference in New Issue
Block a user