From a0b37ad1b9f850bf00bc0ddd8c42f85f943d04a4 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 7 Jul 2026 22:13:12 +0300 Subject: [PATCH] store sessions in the DB --- .gitignore | 3 + packages/tracker/meshnet_tracker/accounts.py | 32 +++++- .../tracker/meshnet_tracker/dashboard.html | 6 +- packages/tracker/meshnet_tracker/server.py | 104 +++++++++++++----- tests/test_accounts.py | 65 +++++++++++ 5 files changed, 176 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 26860cd..1820726 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ dist/ # Local tracker/node sqlite databases (never commit runtime state) *.sqlite *.sqlite3 +logs/tracker/error.log +logs/tracker/info.log +logs/tracker/warning.log diff --git a/packages/tracker/meshnet_tracker/accounts.py b/packages/tracker/meshnet_tracker/accounts.py index 626ce6f..68099c5 100644 --- a/packages/tracker/meshnet_tracker/accounts.py +++ b/packages/tracker/meshnet_tracker/accounts.py @@ -8,7 +8,8 @@ regular user. Mutations are append-only events with unique ids — the same replication model as ``BillingLedger`` — so accounts and API keys converge across the tracker hive via gossip, and every dashboard can serve registration/login. -Sessions are deliberately local to each tracker (bearer tokens in memory). +Sessions are local to each tracker and persisted so dashboard cookies survive +tracker restarts. """ from __future__ import annotations @@ -115,6 +116,8 @@ class AccountStore: "account_id": account_id, "expires": time.time() + SESSION_TTL, } + self._dirty = True + self.save_to_db() return token def session_account(self, token: str | None) -> dict | None: @@ -134,7 +137,9 @@ class AccountStore: if not token: return with self._lock: - self._sessions.pop(token, None) + if self._sessions.pop(token, None) is not None: + self._dirty = True + self.save_to_db() # ---- API keys ---- @@ -271,6 +276,10 @@ class AccountStore: "CREATE TABLE IF NOT EXISTS account_events " "(event_id TEXT PRIMARY KEY, payload TEXT NOT NULL, ts REAL NOT NULL)" ) + con.execute( + "CREATE TABLE IF NOT EXISTS account_sessions " + "(token TEXT PRIMARY KEY, account_id TEXT NOT NULL, expires REAL NOT NULL)" + ) con.commit() con.close() @@ -279,6 +288,10 @@ class AccountStore: rows = con.execute( "SELECT payload FROM account_events ORDER BY ts, event_id" ).fetchall() + session_rows = con.execute( + "SELECT token, account_id, expires FROM account_sessions WHERE expires >= ?", + (time.time(),), + ).fetchall() con.close() with self._lock: for (payload,) in rows: @@ -288,6 +301,11 @@ class AccountStore: continue if event.get("id") not in self._seen_event_ids: self._apply_locked(event) + self._sessions = { + token: {"account_id": account_id, "expires": float(expires)} + for token, account_id, expires in session_rows + if account_id in self._accounts + } self._dirty = False def save_to_db(self) -> None: @@ -297,11 +315,21 @@ class AccountStore: if not self._dirty: return events = list(self._event_log) + sessions = [ + (token, session["account_id"], float(session["expires"])) + for token, session in self._sessions.items() + if session["expires"] >= time.time() + ] self._dirty = False con = sqlite3.connect(self._db_path) # type: ignore[arg-type] con.executemany( "INSERT OR IGNORE INTO account_events (event_id, payload, ts) VALUES (?, ?, ?)", [(e["id"], json.dumps(e), float(e.get("ts", 0.0))) for e in events], ) + con.execute("DELETE FROM account_sessions") + con.executemany( + "INSERT INTO account_sessions (token, account_id, expires) VALUES (?, ?, ?)", + sessions, + ) con.commit() con.close() diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index faa07fb..b22cfec 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -224,6 +224,7 @@

Model usage (RPM)

loading…

Call wall

loading...
+

Chat / inference