fix(dashboard): scope sessions and analytics to selected profile (#45598)

This commit is contained in:
Teknium
2026-06-13 05:42:38 -07:00
committed by GitHub
parent 2abcae9678
commit 62b4618e9a
3 changed files with 175 additions and 65 deletions
+32 -32
View File
@@ -2535,6 +2535,7 @@ async def get_sessions(
order: str = "created",
source: str = None,
exclude_sources: str = None,
profile: Optional[str] = None,
):
"""List sessions.
@@ -2558,9 +2559,11 @@ async def get_sessions(
status_code=400,
detail="order must be one of: created, recent",
)
profile_name: Optional[str] = None
if profile:
profile_name, _ = _cron_profile_home(profile)
try:
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
min_message_count = max(0, min_messages)
archived_only = archived == "only"
@@ -2594,11 +2597,16 @@ async def get_sessions(
s.get("ended_at") is None
and (now - s.get("last_active", s.get("started_at", 0))) < 300
)
if profile_name:
s["profile"] = profile_name
s["is_default_profile"] = profile_name == "default"
# SQLite stores the flag as 0/1; expose a real JSON boolean.
s["archived"] = bool(s.get("archived"))
return {"sessions": sessions, "total": total, "limit": limit, "offset": offset}
finally:
db.close()
except HTTPException:
raise
except Exception:
_log.exception("GET /api/sessions failed")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -2724,7 +2732,7 @@ async def get_profiles_sessions(
@app.get("/api/sessions/search")
async def search_sessions(q: str = "", limit: int = 20):
async def search_sessions(q: str = "", limit: int = 20, profile: Optional[str] = None):
"""Search sessions by ID plus full-text message content using FTS5.
Direct session-id matches are surfaced first, then FTS message-content
@@ -2738,8 +2746,7 @@ async def search_sessions(q: str = "", limit: int = 20):
if not q or not q.strip():
return {"results": []}
try:
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
safe_limit = max(1, min(int(limit or 20), 100))
@@ -2881,6 +2888,8 @@ async def search_sessions(q: str = "", limit: int = 20):
return {"results": list(seen.values())}
finally:
db.close()
except HTTPException:
raise
except Exception:
_log.exception("GET /api/sessions/search failed")
raise HTTPException(status_code=500, detail="Search failed")
@@ -6290,6 +6299,7 @@ def _session_latest_descendant(session_id: str):
# reorder this block, move every route in it together.
class BulkDeleteSessions(BaseModel):
ids: List[str]
profile: Optional[str] = None
@app.post("/api/sessions/bulk-delete")
@@ -6334,8 +6344,7 @@ async def bulk_delete_sessions_endpoint(body: BulkDeleteSessions):
status_code=400,
detail="ids must contain at most 500 entries",
)
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(body.profile)
try:
deleted = db.delete_sessions(body.ids)
return {"ok": True, "deleted": deleted}
@@ -6344,15 +6353,14 @@ async def bulk_delete_sessions_endpoint(body: BulkDeleteSessions):
@app.get("/api/sessions/empty/count")
async def count_empty_sessions_endpoint():
async def count_empty_sessions_endpoint(profile: Optional[str] = None):
"""Return the number of empty, ended, non-archived sessions.
Drives the dashboard's "Delete empty (N)" button — when N is 0 the
UI hides the affordance so users aren't presented with a button
that does nothing. Cheap, single-COUNT query.
"""
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
return {"count": db.count_empty_sessions()}
finally:
@@ -6360,7 +6368,7 @@ async def count_empty_sessions_endpoint():
@app.delete("/api/sessions/empty")
async def delete_empty_sessions_endpoint():
async def delete_empty_sessions_endpoint(profile: Optional[str] = None):
"""Delete every empty (``message_count == 0``), ended,
non-archived session in a single transaction.
@@ -6379,8 +6387,7 @@ async def delete_empty_sessions_endpoint():
prune-on-startup pass. Matching that pre-existing trade-off keeps
the two delete endpoints' DB-vs-disk behaviour consistent.
"""
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
deleted = db.delete_empty_sessions()
return {"ok": True, "deleted": deleted}
@@ -6389,15 +6396,13 @@ async def delete_empty_sessions_endpoint():
@app.get("/api/sessions/stats")
async def get_session_stats():
async def get_session_stats(profile: Optional[str] = None):
"""Session-store statistics for the Sessions page (mirrors `hermes sessions stats`).
Registered before ``/api/sessions/{session_id}`` so the literal ``stats``
path isn't captured as a session id by the parameterized route.
"""
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
total = db.session_count(include_archived=True)
active_store = db.session_count(include_archived=False)
@@ -6535,11 +6540,9 @@ async def rename_session_endpoint(session_id: str, body: SessionRename):
@app.get("/api/sessions/{session_id}/export")
async def export_session_endpoint(session_id: str):
async def export_session_endpoint(session_id: str, profile: Optional[str] = None):
"""Export a single session (metadata + messages) as JSON."""
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
sid = db.resolve_session_id(session_id)
if not sid:
@@ -6555,6 +6558,7 @@ async def export_session_endpoint(session_id: str):
class SessionPrune(BaseModel):
older_than_days: int = 90
source: Optional[str] = None
profile: Optional[str] = None
@app.post("/api/sessions/prune")
@@ -6562,11 +6566,10 @@ async def prune_sessions_endpoint(body: SessionPrune):
"""Delete ended sessions older than N days (mirrors `hermes sessions prune`)."""
if body.older_than_days < 1:
raise HTTPException(status_code=400, detail="older_than_days must be >= 1")
from hermes_state import SessionDB
db = SessionDB()
profile_home = _cron_profile_home(body.profile)[1] if body.profile else get_hermes_home()
db = _open_session_db_for_profile(body.profile)
try:
sessions_dir = get_hermes_home() / "sessions"
sessions_dir = profile_home / "sessions"
removed = db.prune_sessions(
older_than_days=body.older_than_days,
source=(body.source or None),
@@ -9612,11 +9615,10 @@ async def update_config_raw(body: RawConfigUpdate, profile: Optional[str] = None
@app.get("/api/analytics/usage")
async def get_usage_analytics(days: int = 30):
from hermes_state import SessionDB
async def get_usage_analytics(days: int = 30, profile: Optional[str] = None):
from agent.insights import InsightsEngine
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
cutoff = time.time() - (days * 86400)
cur = db._conn.execute("""
@@ -9681,15 +9683,13 @@ async def get_usage_analytics(days: int = 30):
@app.get("/api/analytics/models")
async def get_models_analytics(days: int = 30):
async def get_models_analytics(days: int = 30, profile: Optional[str] = None):
"""Rich per-model analytics for the Models dashboard page.
Returns token/cost/session breakdown per model plus capability metadata
from models.dev (context window, vision, tools, reasoning, etc.).
"""
from hermes_state import SessionDB
db = SessionDB()
db = _open_session_db_for_profile(profile)
try:
cutoff = time.time() - (days * 86400)