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/gateway/meshnet_gateway/server.py b/packages/gateway/meshnet_gateway/server.py
index 7a31ef8..ebce5df 100644
--- a/packages/gateway/meshnet_gateway/server.py
+++ b/packages/gateway/meshnet_gateway/server.py
@@ -286,7 +286,7 @@ class _GatewayHandler(http.server.BaseHTTPRequestHandler):
self._send_json(200, completion)
def _proxy_to_head_worker(self, url: str, body_bytes: bytes) -> None:
- """Forward a raw request body to a head worker and stream the response back."""
+ """Forward a raw request body to a head worker and relay SSE without buffering."""
target_url = f"{url}/v1/chat/completions"
req = urllib.request.Request(
target_url,
@@ -297,6 +297,19 @@ class _GatewayHandler(http.server.BaseHTTPRequestHandler):
try:
with urllib.request.urlopen(req, timeout=30.0) as r:
content_type = r.headers.get("Content-Type", "application/json")
+ if "text/event-stream" in content_type:
+ self.send_response(r.status)
+ self.send_header("Content-Type", content_type)
+ self.send_header("Cache-Control", "no-cache")
+ self.send_header("X-Accel-Buffering", "no")
+ self.end_headers()
+ while True:
+ line = r.readline()
+ if not line:
+ break
+ self.wfile.write(line)
+ self.wfile.flush()
+ return
resp_body = r.read()
status = r.status
except urllib.error.HTTPError as exc:
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/cli.py b/packages/tracker/meshnet_tracker/cli.py
index 585e4e4..cd8fb26 100644
--- a/packages/tracker/meshnet_tracker/cli.py
+++ b/packages/tracker/meshnet_tracker/cli.py
@@ -9,6 +9,12 @@ from pathlib import Path
from .accounts import DEFAULT_ACCOUNTS_DB_PATH
from .billing import DEFAULT_BILLING_DB_PATH
from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH
+from .logging_setup import (
+ DEFAULT_LOG_BACKUP_COUNT,
+ DEFAULT_LOG_DIR,
+ DEFAULT_LOG_MAX_BYTES,
+ configure_tracker_file_logging,
+)
from .routing_stats import RoutingConfig
from .server import (
DEFAULT_CALLER_CREDIT_USDT,
@@ -302,6 +308,34 @@ def main() -> None:
metavar="SECONDS",
help="Half-life for decaying route throughput observations (default 600)",
)
+ common.add_argument(
+ "--log-dir",
+ default=DEFAULT_LOG_DIR,
+ metavar="PATH",
+ help=(
+ "Directory for rotating tracker logs "
+ f"(default: {DEFAULT_LOG_DIR}; files: info.log, warning.log, error.log)"
+ ),
+ )
+ common.add_argument(
+ "--log-max-bytes",
+ type=int,
+ default=DEFAULT_LOG_MAX_BYTES,
+ metavar="BYTES",
+ help=f"Rotate each tracker log file after this many bytes (default: {DEFAULT_LOG_MAX_BYTES})",
+ )
+ common.add_argument(
+ "--log-backup-count",
+ type=int,
+ default=DEFAULT_LOG_BACKUP_COUNT,
+ metavar="N",
+ help=f"Number of rotated tracker log files to keep per level (default: {DEFAULT_LOG_BACKUP_COUNT})",
+ )
+ common.add_argument(
+ "--no-file-logs",
+ action="store_true",
+ help="Disable rotating tracker log files and only write to the terminal",
+ )
parser = argparse.ArgumentParser(
prog="meshnet-tracker",
@@ -315,6 +349,13 @@ def main() -> None:
args = parser.parse_args()
if args.command in {None, "start"}:
+ if not args.no_file_logs:
+ log_dir = configure_tracker_file_logging(
+ args.log_dir,
+ max_bytes=args.log_max_bytes,
+ backup_count=args.log_backup_count,
+ )
+ print(f"meshnet-tracker logs: {log_dir}", flush=True)
cluster_peers = [u.strip() for u in args.cluster_peers.split(",") if u.strip()]
relay_url = args.relay_url or derive_relay_url_from_public_tracker_url(args.self_url)
treasury = None
diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html
index 9b42241..e3ba6a7 100644
--- a/packages/tracker/meshnet_tracker/dashboard.html
+++ b/packages/tracker/meshnet_tracker/dashboard.html
@@ -234,6 +234,7 @@
Routing (learned)
loading…