{% raw %} # Online presence DevPlace shows whether a user is **online** right now. You see it on every profile page and at the top of a direct-message conversation, as a small status dot with a short label: `online`, `last seen 5m ago`, or `offline`. ## What you see - Every user **avatar** carries a small dot in its bottom-right corner: **green** when the person is online, **muted grey** when they are not, so presence is visible everywhere an avatar appears (the feed, comments, the navbar, message lists, and more). Hover the dot for the exact status. - On a **profile** and at the top of a **conversation** you also get the word `online`, or **`last seen ...`** with a relative time, or `offline` if the person has not been seen since the feature started tracking them. - The indicator **updates on its own** while you have the page open: if someone comes online while you are looking at their profile, the dot turns green within a few seconds, and it fades back to grey shortly after they go idle. You never need to refresh. Live updating needs you to be signed in; a signed-out visitor sees the status as it was when the page loaded. - The **feed** shows an **Online now** panel at the bottom of the left sidebar: the avatars of everyone currently online, ordered alphabetically so they keep a stable spot, with a live count. People appear and disappear from it in real time as they come and go, again with no refresh. "Active" simply means loading any page on the site. There is nothing to switch on and no busy or away status to set; presence is automatic. ## How long you stay "online" You count as online for a short window after your last activity. The default window is **60 seconds**, so a moment after you stop browsing you quietly drop to `last seen ...`. Because the window is deliberately short, the status is honest: a green dot means the person really is here right now, not that they logged in hours ago. Operators can change the window, so on some deployments it may be longer or shorter. To keep the indicator steady rather than jittery, the status is **quick to turn on and slow to turn off**: you show as online the moment you are active, but a brief pause does not immediately drop you - there is a short grace margin before the dot goes grey. This stops the flicker you would otherwise see if your activity landed right on the edge of the window, and it applies to both the avatar dots and the Online now list, so they always agree. ## Privacy Online status is public, the same way your posts and profile are public. It reflects only activity on DevPlace and never your location, your device, or anything you do elsewhere. The only thing recorded is the time you were last active, and it is overwritten in place each time, never kept as a history. ## For contributors Presence is one timestamp plus a lightweight push, built to touch the database as little as possible. **The data.** Each user row carries a single `last_seen` column (UTC ISO). There is no separate table and no per-request insert, so the feature adds no data growth. A user is online when `now - last_seen` is under `config.PRESENCE_TIMEOUT_SECONDS` (env `DEVPLACE_PRESENCE_TIMEOUT_SECONDS`, default 60). **Writing it.** The `track_presence` middleware in `main.py` resolves the current user on each non-asset request and calls `services/presence.py` `touch(uid)`. `touch` keeps a per-worker in-memory throttle and writes `last_seen` at most once per `PRESENCE_WRITE_SECONDS` (half the window) per user, as an in-place `UPDATE`. Continuous browsing is a dictionary lookup, not a write. This is the only cross-worker-correct approach here: pub/sub is in-process, so SQLite is the shared medium. **Reading it.** `presence.is_online(user_row)` is exposed as the Jinja global `is_online(user)` and rendered from the user row a page already loaded, so no extra query runs. The value is also on `UserOut.last_seen` and `ProfileOut.profile_online` for JSON clients. **Live updates (one set, one topic, change-only, hysteresis).** `PresenceRelayService` (`services/presence_relay.py`) runs on the service-lock owner and recomputes ONE online set each tick from ONE indexed query over the online population (`presence.online_candidates()`, capped at `config.PRESENCE_TRACK_LIMIT`, env `DEVPLACE_PRESENCE_TRACK_LIMIT`, default 500). Membership uses hysteresis (`presence.stays_online`): a user goes online at `PRESENCE_TIMEOUT_SECONDS` but only drops after an extra `PRESENCE_ONLINE_MARGIN_SECONDS` grace (`DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS`, default 20) - quick on, slow off - which is what prevents boundary flicker. That single set is published on the single shared topic `public.presence.roster` as `{count, online: [uid, ...], users: [...]}`, **only when the set of online users changes** (a `frozenset` compare, so reordering never republishes) and never on a fixed interval, so an idle site costs zero messages. `online` is the authority for every avatar dot anywhere on the page; `users` is the same set trimmed to `PRESENCE_ONLINE_LIMIT` for the feed's avatar panel. There are no per-user presence topics: one set, one topic, one frame, so no two indicators can drift apart. `services/presence.py` `is_online(user)` is literally `stays_online(seconds_since(last_seen), was_online=False)`, so the server-rendered initial state and the live set apply the same rule. **The frontend.** `static/js/PresenceManager.js` (`app.presence`) makes ONE subscription to `public.presence.roster` and keeps the pushed `online` uid set. Every element carrying `data-presence-uid` - anywhere on any page, discovered on load and through a `MutationObserver` for markup inserted later - resolves its state as membership of that one set, toggling the `online` class and, for `data-presence-label` elements, the "online / last seen X / offline" text (the relative time is a `` formatted by the shared `LocalTime`, so presence never formats a date itself). There is **no client-side clock and no expiry timer**: the relay is the only thing that decides who is online, and before the first frame arrives the server-rendered state simply stands. `static/js/OnlineUsers.js` (`app.onlineUsers`) renders the feed's avatar panel from `users` in that same frame, so the panel and the dots are two views of one payload. `services/presence.py` `online_users()` renders the initial panel server-side with the same indexed `last_seen` query (`database.get_online_users`; `idx_users_last_seen`), ordered **alphabetically by username** so an avatar keeps a stable position instead of jumping around as activity ticks. **The avatar dot.** The corner dot is one reusable partial, `templates/_presence_dot.html`, which emits a `` (guarded on `uid`, so an author with no resolvable user renders no dot). It is included by the shared avatar partial `templates/_avatar_link.html` (which covers most avatars) and by the raw-avatar sites wrapped in a positioned `.avatar-badge` span, including the feed's Online now panel - a roster avatar is not a special case, it is the same subscribed dot as everywhere else. In JavaScript the matching builder is `Avatar.badgeElement(user)` (`static/js/Avatar.js`), used by `OnlineUsers` and by the chat's conversation list, so dot markup exists in exactly two places: the partial and that helper. Because the dot carries `data-presence-uid` but no `data-presence-label`, `PresenceManager` drives its colour with **no extra JavaScript**. The dot sizes itself as a percentage of the avatar (clamped 8-14px), so it stays proportional at every avatar size. Reuse `is_online`, `_presence_dot.html`/`Avatar.badgeElement`, the `public.presence.roster` topic, and `PresenceManager` for any new online indicator rather than re-implementing presence. {% endraw %}