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 <time data-dt data-dt-mode="ago"> 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 <span class="presence-dot" data-presence-uid=... data-presence-last-seen=...> (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.