feat(status): auto-expiring custom status (LC-319) #336

Merged
longjacksonle merged 1 commit from feat/lc-319-auto-expiring-status into main 2026-06-17 01:02:48 +02:00

What

Auto-expiring custom status: a user can schedule their custom status to clear itself after a chosen interval ("clear my status in 1 hour"). Implements LC-319 / LC-320.

The status picker gains a Clear after select below the custom-status input: Don't clear (default), 30 minutes, 1 hour, 4 hours, 1 day, 1 week. After the interval the custom text auto-clears; the presence value (active/idle/away/dnd) is left untouched, and the change fans out live to other viewers exactly like a manual clear.

How

  • Schema. New immutable migration auth/0028_custom_status_expiry.sql adds nullable users.custom_status_expires_at (NULL = never). Deliberately kept out of every existing SELECT / row_to_user_record, so the shared User/UserRecord model and its ~20 read sites are untouched.
  • Write path. db::auth::set_user_status gains an expires_modifier: Option<&str> param (a SQLite relative modifier like +1 hours) applied via a single CASE WHEN ? IS NULL THEN NULL ELSE datetime('now', ?) END. The modifier comes from a fixed allowlist in routes::status::expiry_modifier, never raw input. Clearing the custom text forces the expiry to NULL.
  • Sweep. db::auth::clear_expired_custom_statuses is a single RETURNING UPDATE that nulls text + expiry for past-due rows and returns (id, status). New spawn_status_expiry_scanner (60s tick, modeled on spawn_idle_scanner) broadcasts ChatEvent::UserStatusChanged { custom_status: None } per cleared row so avatars/hovercards drop the stale text live. 60s granularity matches the idle-flip tolerance; no per-request lazy clearing, so the read path stays write-free (consistent with the touch_user_activity contention note).
  • Picker. clear_after select (default "Don't clear", disabled while the custom input is empty); when an expiry is active, a muted localized "auto-clears" hint via <time data-lc-ts> reusing the LC-314 Intl.RelativeTimeFormat scanner. The select is authoritative (re-saving with "Don't clear" removes any existing expiry, Slack-style).
  • i18n. New status.ftl keys with en/es parity.

Testing

  • just check (clippy standalone + saas, fmt) clean.
  • just test and just test-saas green. New db_status.rs cases cover expiry scheduling, empty-custom forcing NULL expiry, the sweep clearing past-due rows only while preserving presence, and the no-op-when-nothing-expired path.

Operator impact

None. No new env var, config, or contract change; no [operator-action] marker.

🤖 Generated with Claude Code

## What Auto-expiring custom status: a user can schedule their custom status to clear itself after a chosen interval ("clear my status in 1 hour"). Implements LC-319 / LC-320. The status picker gains a **Clear after** select below the custom-status input: Don't clear (default), 30 minutes, 1 hour, 4 hours, 1 day, 1 week. After the interval the custom text auto-clears; the presence value (active/idle/away/dnd) is left untouched, and the change fans out live to other viewers exactly like a manual clear. ## How - **Schema.** New immutable migration `auth/0028_custom_status_expiry.sql` adds nullable `users.custom_status_expires_at` (NULL = never). Deliberately kept out of every existing `SELECT` / `row_to_user_record`, so the shared `User`/`UserRecord` model and its ~20 read sites are untouched. - **Write path.** `db::auth::set_user_status` gains an `expires_modifier: Option<&str>` param (a SQLite relative modifier like `+1 hours`) applied via a single `CASE WHEN ? IS NULL THEN NULL ELSE datetime('now', ?) END`. The modifier comes from a fixed allowlist in `routes::status::expiry_modifier`, never raw input. Clearing the custom text forces the expiry to NULL. - **Sweep.** `db::auth::clear_expired_custom_statuses` is a single `RETURNING` UPDATE that nulls text + expiry for past-due rows and returns `(id, status)`. New `spawn_status_expiry_scanner` (60s tick, modeled on `spawn_idle_scanner`) broadcasts `ChatEvent::UserStatusChanged { custom_status: None }` per cleared row so avatars/hovercards drop the stale text live. 60s granularity matches the idle-flip tolerance; no per-request lazy clearing, so the read path stays write-free (consistent with the `touch_user_activity` contention note). - **Picker.** `clear_after` select (default "Don't clear", disabled while the custom input is empty); when an expiry is active, a muted localized "auto-clears" hint via `<time data-lc-ts>` reusing the LC-314 `Intl.RelativeTimeFormat` scanner. The select is authoritative (re-saving with "Don't clear" removes any existing expiry, Slack-style). - **i18n.** New `status.ftl` keys with en/es parity. ## Testing - `just check` (clippy standalone + saas, fmt) clean. - `just test` and `just test-saas` green. New `db_status.rs` cases cover expiry scheduling, empty-custom forcing NULL expiry, the sweep clearing past-due rows only while preserving presence, and the no-op-when-nothing-expired path. ## Operator impact None. No new env var, config, or contract change; no `[operator-action]` marker. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(status): auto-expiring custom status (LC-319)
All checks were successful
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (push) Successful in 10s
check-secrets / Nosey parker (pull_request) Successful in 9s
check-secrets / Kingfisher (pull_request) Successful in 9s
check-secrets / TruffleHog (pull_request) Successful in 10s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 10m44s
86bc5cf713
Let a user schedule their custom status to auto-clear after a chosen interval. The status picker gains a "Clear after" select (30 minutes / 1 hour / 4 hours / 1 day / 1 week, or "Don't clear"); after the interval the custom text auto-clears while the presence value is left untouched, and the change fans out live to other viewers exactly like a manual clear.

New nullable `users.custom_status_expires_at` (migration auth/0028) is set via `set_user_status`'s new `expires_modifier` param, swept by a 60s `spawn_status_expiry_scanner` modeled on the idle scanner. The sweep nulls custom text + expiry for past-due rows and broadcasts `UserStatusChanged` so avatars/hovercards drop the stale text. Preset-to-modifier mapping is a fixed allowlist (never raw input). The picker shows a localized "auto-clears" hint reusing the LC-314 relative-time scanner. No new read-path writes; the new column stays out of the shared User model. en/es parity.

#LC-319
#LC-320

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-319-auto-expiring-status 2026-06-17 01:02:48 +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/lets-chat!336
No description provided.