fix(dashboard): scope sessions and analytics to selected profile (#45598)
This commit is contained in:
+32
-32
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user