Locks: admin-configurable expiry cap for manual user locks (deferred from #315 L4) #394

Closed
opened 2026-09-02 20:00:28 +00:00 by Cordy · 0 comments
Owner

Deferred from #315 (design point L4, shipped v0.6.106 / PR #393 without it). Manual (user-type) locks are currently indefinite — a lock set by a user who then goes on holiday stays until someone with rights breaks it. L4 ratified an optional instance-wide cap, off by default.

What to build

Setting. lockMaxHours (int, 0 = off/indefinite, the default) in internal/settings — runtime setting via the existing GET/PUT /api/v1/settings, validated >= 0. Naming follows the existing settings style.

Enforcement. In internal/storage/filelock, treat a user-type lock as expired when lockMaxHours > 0 and now > rec.At + cap. Two options, decide at implementation:

  1. Inject a MaxAge func() time.Duration hook on Store, consulted in Record.live() for user locks (dav locks keep their own Expires). Wired in main.go from the settings store. Changing the setting immediately re-evaluates existing locks — no migration, no rewrite of locks.json.
  2. Stamp Expires at lock creation. Rejected in L4 discussion: a later cap change would not affect existing locks, and cap-off would strand old expiries.
    Option 1 is the recommendation — the cap is a policy lens over stored facts, not stored state.

Admin UI. One quiet-field row (design-system .srow treatment, 104px input + unit suffix "hours", 0 = "no limit" helper text) in the admin dashboard. Placement: Basic settings, near the other instance-behaviour toggles (final placement at implementation; Retention & holds is wrong — locks are not retention).

API/UX detail. When a manual lock lapses via the cap, it simply stops being live (lazy, like dav expiry): chip disappears on next listing, writes pass. No notification in v1.

Tests. live() respects the hook for user locks only; hook nil = indefinite (existing tests stay valid); cap change takes effect without restart; settings validation.

Refs: #315 comment L1–L10 (id 3386), internal/storage/filelock/filelock.go (Record.live), internal/settings, PR #393.

Deferred from #315 (design point **L4**, shipped v0.6.106 / PR #393 without it). Manual (`user`-type) locks are currently **indefinite** — a lock set by a user who then goes on holiday stays until someone with rights breaks it. L4 ratified an optional instance-wide cap, off by default. ## What to build **Setting.** `lockMaxHours` (int, 0 = off/indefinite, the default) in `internal/settings` — runtime setting via the existing `GET/PUT /api/v1/settings`, validated `>= 0`. Naming follows the existing settings style. **Enforcement.** In `internal/storage/filelock`, treat a `user`-type lock as expired when `lockMaxHours > 0` and `now > rec.At + cap`. Two options, decide at implementation: 1. Inject a `MaxAge func() time.Duration` hook on `Store`, consulted in `Record.live()` for `user` locks (dav locks keep their own `Expires`). Wired in `main.go` from the settings store. Changing the setting immediately re-evaluates existing locks — no migration, no rewrite of `locks.json`. 2. Stamp `Expires` at lock creation. Rejected in L4 discussion: a later cap change would not affect existing locks, and cap-off would strand old expiries. Option 1 is the recommendation — the cap is a policy lens over stored facts, not stored state. **Admin UI.** One quiet-field row (design-system `.srow` treatment, 104px input + unit suffix "hours", 0 = "no limit" helper text) in the admin dashboard. Placement: Basic settings, near the other instance-behaviour toggles (final placement at implementation; Retention & holds is wrong — locks are not retention). **API/UX detail.** When a manual lock lapses via the cap, it simply stops being live (lazy, like dav expiry): chip disappears on next listing, writes pass. No notification in v1. **Tests.** `live()` respects the hook for user locks only; hook nil = indefinite (existing tests stay valid); cap change takes effect without restart; settings validation. Refs: #315 comment L1–L10 (id 3386), `internal/storage/filelock/filelock.go` (`Record.live`), `internal/settings`, PR #393.
Cordy closed this issue 2026-09-02 20:08:49 +00:00
Sign in to join this conversation.
No labels
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: Cordy/Cairn#394
No description provided.