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…