diff --git a/devplacepy/services/CLAUDE.md b/devplacepy/services/CLAUDE.md index 21979a74..3fc18d24 100644 --- a/devplacepy/services/CLAUDE.md +++ b/devplacepy/services/CLAUDE.md @@ -185,7 +185,7 @@ Online status is a single **`users.last_seen`** UTC-ISO column (ensured in `data **Write path (all workers):** `main.py`'s `track_presence` HTTP middleware resolves the cached current user on every non-`/static`, non-`/avatar` request and calls `presence.touch(uid)`. `touch` keeps a per-worker in-memory `_last_write: dict[uid -> monotonic]` and writes `users.last_seen` (via `database.set_last_seen`) only when the last write for that uid is older than `config.PRESENCE_WRITE_SECONDS` (= `PRESENCE_TIMEOUT_SECONDS // 2`). So continuous browsing is a dict lookup; a write happens at most ~once per half-window per active user per worker, and the row is updated in place (zero growth). It deliberately does **not** call `clear_user_cache` (that would defeat the 300s auth cache; the stale cached self-row is irrelevant since presence of *other* users is always read from a fresh row). -**Consent gate (write path).** `touch` checks `presence.recording_allowed(uid)` (the `activity_recording` consent) **after** the per-worker throttle, so the consent read costs at most one query per half-window per active user rather than one per request. A user who withdraws the consent simply stops being written and appears offline; `base.html` shows a `.recording-indicator` while it is on. Never move the check above the throttle. +**Consent gate (write path).** `touch` checks `presence.recording_allowed(uid)` (the `activity_recording` consent) **after** the per-worker throttle, so the consent read costs at most one query per half-window per active user rather than one per request. A user who withdraws the consent simply stops being written and appears offline. Never move the check above the throttle. **Read path (any worker):** `presence.is_online(user_row)` = `now - last_seen < PRESENCE_TIMEOUT_SECONDS` (env `DEVPLACE_PRESENCE_TIMEOUT_SECONDS`, default 60). Profile (`routers/profile/index.py` -> `profile_online`) and messages (`routers/messages.py` seed) read `last_seen` off the user row they already loaded - no extra query. Exposed as the Jinja global `is_online(user)` (`templating.py`), on `UserOut.last_seen` and `ProfileOut.profile_online`. This is the **only** cross-worker-correct approach here because pub/sub is in-process. diff --git a/devplacepy/services/moderation/CLAUDE.md b/devplacepy/services/moderation/CLAUDE.md index 434a4b22..4d88bbb5 100644 --- a/devplacepy/services/moderation/CLAUDE.md +++ b/devplacepy/services/moderation/CLAUDE.md @@ -106,7 +106,7 @@ The gate is at exactly one place: `GatewayService.consent_denied` in `services/o `container_credentials` gates `containers/api.validate_run_as`: a container configured to run as **someone else** would inject that person's real `DEVPLACE_API_KEY` into software they do not operate, so it is refused unless they granted the consent. Running a container as yourself never asks - you are the one handing over your own credential. -`activity_recording` gates `presence.touch`, checked **after** the per-worker throttle so the consent read costs at most one query per half-window per user. Withdraw it and you simply appear offline. The indicator is the `.recording-indicator` in `base.html`, rendered from the `activity_recording_on(user)` Jinja global. +`activity_recording` gates `presence.touch`, checked **after** the per-worker throttle so the consent read costs at most one query per half-window per user. Withdraw it and you simply appear offline. The consent is managed on the profile privacy tab; no page chrome advertises its state. ## Terms re-acceptance diff --git a/devplacepy/static/css/base.css b/devplacepy/static/css/base.css index 585494ba..ed39d462 100644 --- a/devplacepy/static/css/base.css +++ b/devplacepy/static/css/base.css @@ -1375,35 +1375,6 @@ body:has(.page-messages) { } -.recording-indicator { - position: fixed; - left: var(--space-lg); - bottom: calc(var(--space-2xl) + var(--space-2xl)); - display: inline-flex; - align-items: center; - gap: var(--space-xs); - padding: var(--space-xs) var(--space-sm); - border-radius: var(--radius); - background: var(--bg-card); - border: 1px solid var(--border-light); - color: var(--text-muted); - font-size: 0.75rem; - z-index: var(--z-fab); - will-change: transform; -} - -.recording-indicator a { - color: var(--text-muted); -} - -.recording-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--danger); - flex: none; -} - .maturity-gate { padding: var(--space-2xl); text-align: center; diff --git a/devplacepy/templates/base.html b/devplacepy/templates/base.html index 8eae148b..a7c6cea9 100644 --- a/devplacepy/templates/base.html +++ b/devplacepy/templates/base.html @@ -212,13 +212,6 @@ {% endblock %} - {% if user and activity_recording_on(user) %} -