feat: add online presence tracking with last_seen column and configurable timeout

Add `last_seen` column to users table with index, implement `set_last_seen` and `get_online_users` database functions, expose presence config env vars (`PRESENCE_TIMEOUT_SECONDS`, `PRESENCE_ONLINE_LIMIT`, `PRESENCE_ONLINE_MARGIN_SECONDS`), include `last_seen` in follow list responses, and update profile docs to mention online indicator.
This commit is contained in:
2026-07-04 22:08:20 +00:00
parent 7002b23eeb
commit 0f872336b1
41 changed files with 935 additions and 94 deletions
+100
View File
@@ -267,6 +267,106 @@ def test_activity_comment_card_renders_overlay_link(app_server):
assert f'class="card-link" href="/posts/{post_slug}#comment-{comment_uid}"' in r.text
def test_profile_json_exposes_online_presence(app_server):
import time
name = f"pres{int(time.time() * 1000)}"
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
r = session.get(
f"{BASE_URL}/profile/{name}", headers={"Accept": "application/json"}
)
assert r.status_code == 200
body = r.json()
assert body["profile_online"] is True
assert body["profile_user"]["last_seen"]
def test_profile_json_offline_when_last_seen_stale(app_server):
from datetime import datetime, timezone, timedelta
from devplacepy.config import PRESENCE_TIMEOUT_SECONDS
from devplacepy.database import get_table, refresh_snapshot
owner = _seed_owner()
username = get_table("users").find_one(uid=owner)["username"]
stale = (
datetime.now(timezone.utc)
- timedelta(seconds=PRESENCE_TIMEOUT_SECONDS + 120)
).isoformat()
get_table("users").update({"uid": owner, "last_seen": stale}, ["uid"])
refresh_snapshot()
r = requests.get(
f"{BASE_URL}/profile/{username}", headers={"Accept": "application/json"}
)
assert r.status_code == 200
body = r.json()
assert body["profile_online"] is False
assert body["profile_user"]["last_seen"] == stale
def test_profile_hero_avatar_has_presence_dot(app_server):
import time
from devplacepy.database import get_table, refresh_snapshot
name = f"phero{int(time.time() * 1000)}"
s = requests.Session()
s.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
refresh_snapshot()
uid = get_table("users").find_one(username=name)["uid"]
html = s.get(f"{BASE_URL}/profile/{name}").text
assert "profile-avatar-wrap" in html
assert "presence-dot" in html
assert f'data-presence-uid="{uid}"' in html
def test_followers_list_avatar_has_presence_dot(app_server):
import time
from devplacepy.database import get_table, refresh_snapshot
stamp = int(time.time() * 1000)
target = f"folt{stamp}"
follower = f"folf{stamp}"
st = requests.Session()
sf = requests.Session()
for sess, name in ((st, target), (sf, follower)):
sess.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
sf.post(f"{BASE_URL}/follow/{target}", allow_redirects=True)
refresh_snapshot()
follower_uid = get_table("users").find_one(username=follower)["uid"]
html = st.get(f"{BASE_URL}/profile/{target}?tab=followers").text
assert "follow-user" in html
assert f'data-presence-uid="{follower_uid}"' in html
assert "data-presence-last-seen" in html
def test_viewing_profile_marks_notification_read(app_server):
import time
from datetime import datetime, timezone