Yash030 Claude Opus 4.7 commited on
Commit
6339a53
·
1 Parent(s): 574e4e7

Extend session visibility in admin dashboard with configurable retention

Browse files

- Add retention_seconds param to SessionTracker (default 30min)
- Sessions now stay visible 30min after last request instead of 2min
- Add SESSION_RETENTION_MINUTES env var for configuration
- Show Active/Idle status in admin UI (<5min vs >5min idle)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

api/admin.py CHANGED
@@ -66,6 +66,10 @@ def _get_admin_data() -> dict[str, Any]:
66
  "last_activity_seconds": last_activity_seconds,
67
  "last_activity_display": _format_time(last_activity_seconds),
68
  "requests_per_minute": int(req_per_min),
 
 
 
 
69
  }
70
  )
71
 
 
66
  "last_activity_seconds": last_activity_seconds,
67
  "last_activity_display": _format_time(last_activity_seconds),
68
  "requests_per_minute": int(req_per_min),
69
+ "status": "Active" if last_activity_seconds < 300 else "Idle",
70
+ "status_class": "status-active"
71
+ if last_activity_seconds < 300
72
+ else "status-idle",
73
  }
74
  )
75
 
api/services.py CHANGED
@@ -11,7 +11,7 @@ from fastapi import HTTPException, Request
11
  from fastapi.responses import StreamingResponse
12
  from loguru import logger
13
 
14
- from config.settings import Settings
15
  from core.anthropic import get_token_count, get_user_facing_error_message
16
  from core.anthropic.sse import ANTHROPIC_SSE_RESPONSE_HEADERS, format_sse_event
17
  from core.session_tracker import SessionTracker
@@ -132,7 +132,10 @@ class ClaudeProxyService:
132
  self._provider_getter = provider_getter
133
  self._model_router = model_router or ModelRouter(settings)
134
  self._token_counter = token_counter
135
- self._session_tracker = SessionTracker.get_instance()
 
 
 
136
 
137
  def create_message(self, request: Request, request_data: MessagesRequest) -> object:
138
  """Create a message response or streaming response with optional failover."""
 
11
  from fastapi.responses import StreamingResponse
12
  from loguru import logger
13
 
14
+ from config.settings import Settings, get_settings
15
  from core.anthropic import get_token_count, get_user_facing_error_message
16
  from core.anthropic.sse import ANTHROPIC_SSE_RESPONSE_HEADERS, format_sse_event
17
  from core.session_tracker import SessionTracker
 
132
  self._provider_getter = provider_getter
133
  self._model_router = model_router or ModelRouter(settings)
134
  self._token_counter = token_counter
135
+ settings_local = get_settings()
136
+ self._session_tracker = SessionTracker.get_instance(
137
+ retention_seconds=settings_local.session_retention_minutes * 60
138
+ )
139
 
140
  def create_message(self, request: Request, request_data: MessagesRequest) -> object:
141
  """Create a message response or streaming response with optional failover."""
config/settings.py CHANGED
@@ -142,6 +142,9 @@ class Settings(BaseSettings):
142
  )
143
  # ==================== Zen/OpenCode Config ====================
144
  zen_api_key: str = Field(default="", validation_alias="ZEN_API_KEY")
 
 
 
145
  zen_base_url: str = Field(
146
  default="https://opencode.ai/zen", validation_alias="ZEN_BASE_URL"
147
  )
 
142
  )
143
  # ==================== Zen/OpenCode Config ====================
144
  zen_api_key: str = Field(default="", validation_alias="ZEN_API_KEY")
145
+ session_retention_minutes: int = Field(
146
+ default=30, validation_alias="SESSION_RETENTION_MINUTES"
147
+ )
148
  zen_base_url: str = Field(
149
  default="https://opencode.ai/zen", validation_alias="ZEN_BASE_URL"
150
  )
core/session_tracker.py CHANGED
@@ -6,7 +6,7 @@ import asyncio
6
  import time
7
  from collections import defaultdict
8
  from dataclasses import dataclass
9
- from typing import ClassVar
10
 
11
  from loguru import logger
12
 
@@ -56,6 +56,7 @@ class SessionTracker:
56
  max_sessions: int = 50,
57
  window_seconds: float = 60.0,
58
  per_session_rate_limit: int = 30,
 
59
  ):
60
  if hasattr(self, "_initialized"):
61
  return
@@ -68,14 +69,18 @@ class SessionTracker:
68
  self._max_sessions = max_sessions
69
  self._window_seconds = window_seconds
70
  self._per_session_rate_limit = per_session_rate_limit
 
 
 
71
  self._lock = asyncio.Lock()
72
  self._initialized = True
73
 
74
  logger.info(
75
- "SessionTracker initialized (max_sessions={}, window={}s, per_session_limit={}/min)",
76
  max_sessions,
77
  window_seconds,
78
  per_session_rate_limit,
 
79
  )
80
 
81
  @classmethod
@@ -93,7 +98,7 @@ class SessionTracker:
93
  def _cleanup_old_sessions(self) -> None:
94
  """Remove sessions with no recent activity (must be called with lock held)."""
95
  now = time.monotonic()
96
- cutoff = now - (self._window_seconds * 2)
97
  to_remove = [
98
  sid
99
  for sid, state in self._sessions.items()
@@ -129,7 +134,7 @@ class SessionTracker:
129
 
130
  async def track_request(self, session_id: str, provider_id: str) -> None:
131
  """Record a request for a session to a provider (async-safe)."""
132
- self._track_request_sync(session_id, provider_id)
133
 
134
  def track_request_sync(self, session_id: str, provider_id: str) -> None:
135
  """Record a request for a session to a provider (sync version for hot path)."""
@@ -151,7 +156,7 @@ class SessionTracker:
151
  async def track_request_async(self, session_id: str, provider_id: str) -> None:
152
  """Async version with lock for when called from async contexts that need guarantees."""
153
  async with self._lock:
154
- self._track_request_sync(session_id, provider_id)
155
 
156
  async def release_request(self, session_id: str, provider_id: str) -> None:
157
  """Release a request slot when streaming completes."""
@@ -214,7 +219,7 @@ class SessionTracker:
214
  def get_all_session_loads(self) -> dict[str, SessionLoad]:
215
  """Get load information for all active sessions."""
216
  return {
217
- sid: self.get_session_load(sid)
218
  for sid in self._sessions
219
  if self.get_session_load(sid) is not None
220
  }
 
6
  import time
7
  from collections import defaultdict
8
  from dataclasses import dataclass
9
+ from typing import ClassVar, cast
10
 
11
  from loguru import logger
12
 
 
56
  max_sessions: int = 50,
57
  window_seconds: float = 60.0,
58
  per_session_rate_limit: int = 30,
59
+ retention_seconds: float | None = None,
60
  ):
61
  if hasattr(self, "_initialized"):
62
  return
 
69
  self._max_sessions = max_sessions
70
  self._window_seconds = window_seconds
71
  self._per_session_rate_limit = per_session_rate_limit
72
+ self._retention_seconds = (
73
+ retention_seconds if retention_seconds is not None else window_seconds * 2
74
+ )
75
  self._lock = asyncio.Lock()
76
  self._initialized = True
77
 
78
  logger.info(
79
+ "SessionTracker initialized (max_sessions={}, window={}s, per_session_limit={}/min, retention={}s)",
80
  max_sessions,
81
  window_seconds,
82
  per_session_rate_limit,
83
+ self._retention_seconds,
84
  )
85
 
86
  @classmethod
 
98
  def _cleanup_old_sessions(self) -> None:
99
  """Remove sessions with no recent activity (must be called with lock held)."""
100
  now = time.monotonic()
101
+ cutoff = now - self._retention_seconds
102
  to_remove = [
103
  sid
104
  for sid, state in self._sessions.items()
 
134
 
135
  async def track_request(self, session_id: str, provider_id: str) -> None:
136
  """Record a request for a session to a provider (async-safe)."""
137
+ self.track_request_sync(session_id, provider_id)
138
 
139
  def track_request_sync(self, session_id: str, provider_id: str) -> None:
140
  """Record a request for a session to a provider (sync version for hot path)."""
 
156
  async def track_request_async(self, session_id: str, provider_id: str) -> None:
157
  """Async version with lock for when called from async contexts that need guarantees."""
158
  async with self._lock:
159
+ self.track_request_sync(session_id, provider_id)
160
 
161
  async def release_request(self, session_id: str, provider_id: str) -> None:
162
  """Release a request slot when streaming completes."""
 
219
  def get_all_session_loads(self) -> dict[str, SessionLoad]:
220
  """Get load information for all active sessions."""
221
  return {
222
+ sid: cast(SessionLoad, self.get_session_load(sid))
223
  for sid in self._sessions
224
  if self.get_session_load(sid) is not None
225
  }
templates/admin.html CHANGED
@@ -93,6 +93,12 @@
93
  .status-limited {
94
  color: #f85149;
95
  }
 
 
 
 
 
 
96
  .empty {
97
  color: #8b949e;
98
  font-style: italic;
@@ -140,6 +146,7 @@
140
  <th>Providers</th>
141
  <th>Last Active</th>
142
  <th>Req/Min</th>
 
143
  </tr>
144
  </thead>
145
  <tbody>
@@ -154,6 +161,7 @@
154
  </td>
155
  <td>{{ session.last_activity_display }}</td>
156
  <td class="number">{{ session.requests_per_minute }}</td>
 
157
  </tr>
158
  {% endfor %}
159
  </tbody>
 
93
  .status-limited {
94
  color: #f85149;
95
  }
96
+ .status-active {
97
+ color: #3fb950;
98
+ }
99
+ .status-idle {
100
+ color: #8b949e;
101
+ }
102
  .empty {
103
  color: #8b949e;
104
  font-style: italic;
 
146
  <th>Providers</th>
147
  <th>Last Active</th>
148
  <th>Req/Min</th>
149
+ <th>Status</th>
150
  </tr>
151
  </thead>
152
  <tbody>
 
161
  </td>
162
  <td>{{ session.last_activity_display }}</td>
163
  <td class="number">{{ session.requests_per_minute }}</td>
164
+ <td class="{{ session.status_class }}">{{ session.status }}</td>
165
  </tr>
166
  {% endfor %}
167
  </tbody>