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…

Call wall

loading...
+

Chat / inference