fix: restore admin recent activity feed

parent b81ef423
...@@ -356,7 +356,28 @@ async def api_status(username: str = Depends(require_auth)): ...@@ -356,7 +356,28 @@ async def api_status(username: str = Depends(require_auth)):
recent_activity = [] recent_activity = []
try: try:
from codai.api.log import get_recent_activity from codai.api.log import get_recent_activity
recent_activity = get_recent_activity() for row in get_recent_activity():
if not isinstance(row, dict):
continue
try:
ts = int(row.get("time", 0) or 0)
except (TypeError, ValueError):
ts = 0
try:
status = int(row.get("status", 0) or 0)
except (TypeError, ValueError):
status = 0
try:
duration = round(float(row.get("duration", 0) or 0), 2)
except (TypeError, ValueError):
duration = 0.0
recent_activity.append({
"time": ts,
"model": str(row.get("model") or "—"),
"type": str(row.get("type") or "unknown"),
"status": status,
"duration": duration,
})
except Exception: except Exception:
pass pass
......
...@@ -175,6 +175,7 @@ td code{font-family:var(--mono);font-size:11.5px;background:var(--raised);paddin ...@@ -175,6 +175,7 @@ td code{font-family:var(--mono);font-size:11.5px;background:var(--raised);paddin
.badge-admin{background:var(--accent-s);color:#A5B4FC;border:1px solid rgba(99,102,241,.2)} .badge-admin{background:var(--accent-s);color:#A5B4FC;border:1px solid rgba(99,102,241,.2)}
.badge-user{background:var(--raised);color:var(--text-3);border:1px solid var(--border)} .badge-user{background:var(--raised);color:var(--text-3);border:1px solid var(--border)}
.badge-ok{background:rgba(52,211,153,.08);color:var(--green);border:1px solid rgba(52,211,153,.2)} .badge-ok{background:rgba(52,211,153,.08);color:var(--green);border:1px solid rgba(52,211,153,.2)}
.badge-danger{background:rgba(248,113,113,.08);color:var(--red);border:1px solid rgba(248,113,113,.2)}
/* ── Modals ──────────────────────────────────────────────────────── */ /* ── Modals ──────────────────────────────────────────────────────── */
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(2px);z-index:500;align-items:center;justify-content:center} .modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(2px);z-index:500;align-items:center;justify-content:center}
......
...@@ -35,9 +35,36 @@ _TRACKED_PATHS = { ...@@ -35,9 +35,36 @@ _TRACKED_PATHS = {
"/v1/chat/completions": "chat", "/v1/chat/completions": "chat",
"/v1/completions": "completion", "/v1/completions": "completion",
"/v1/images/generations": "image", "/v1/images/generations": "image",
"/v1/images/edits": "image-edit",
"/v1/images/inpaint": "image-inpaint",
"/v1/images/upscale": "image-upscale",
"/v1/images/deblur": "image-deblur",
"/v1/images/unpixelate": "image-unpixelate",
"/v1/images/outfit": "image-outfit",
"/v1/images/faceswap": "image-faceswap",
"/v1/images/depth": "image-depth",
"/v1/images/segment": "image-segment",
"/v1/video/generations": "video",
"/v1/video/upscale": "video-upscale",
"/v1/video/subtitle": "video-subtitle",
"/v1/video/interpolate": "video-interpolate",
"/v1/video/dub": "video-dub",
"/v1/audio/speech": "tts", "/v1/audio/speech": "tts",
"/v1/audio/transcriptions": "transcription", "/v1/audio/transcriptions": "transcription",
"/v1/audio/generate": "audio-generate",
"/v1/audio/clone": "voice-clone",
"/v1/audio/convert": "voice-convert",
"/v1/audio/stems": "audio-stems",
"/v1/audio/cleanup": "audio-cleanup",
"/v1/embeddings": "embedding", "/v1/embeddings": "embedding",
"/v1/pipelines/image-to-video": "pipeline-image-to-video",
"/v1/pipelines/video-dub": "pipeline-video-dub",
"/v1/pipelines/story": "pipeline-story",
"/v1/pipelines/audio-dub": "pipeline-audio-dub",
"/v1/pipelines/audio-understand": "pipeline-audio-understand",
"/v1/pipelines/audio-music-dub": "pipeline-audio-music-dub",
"/v1/pipelines/custom": "pipeline-custom",
"/v1/pipelines/run": "pipeline-run",
} }
......
import base64 import base64
import os import os
import sys import sys
import types
import wave import wave
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
...@@ -262,8 +263,49 @@ def test_audio_cleanup_returns_artifact_and_applied_operations(monkeypatch, stud ...@@ -262,8 +263,49 @@ def test_audio_cleanup_returns_artifact_and_applied_operations(monkeypatch, stud
assert body["backend"]["engine"] == "ffmpeg-afftdn" assert body["backend"]["engine"] == "ffmpeg-afftdn"
assert body["backend"]["quality"] == "best-effort" assert body["backend"]["quality"] == "best-effort"
assert body["applied"] == ["noise_reduction", "normalize"] assert body["applied"] == ["noise_reduction", "normalize"]
assert "/v1/files/" in body["data"][0]["url"] assert body["limitations"] == ["not-ml-restoration"]
assert "not-ml-restoration" in body["limitations"]
def test_admin_status_includes_recent_activity(studio_client, monkeypatch):
from codai.admin import routes as admin_routes
from codai.api import log as api_log
api_log._activity.clear()
api_log._activity.appendleft({
"time": 1715000000,
"model": "demo-model",
"type": "chat",
"status": 200,
"duration": 1.23,
})
api_log._activity.appendleft({
"time": "bad-time",
"model": None,
"type": None,
"status": "500",
"duration": "2.5",
})
monkeypatch.setattr(admin_routes, "config_manager", types.SimpleNamespace(models_data={}, pipelines_data=[]), raising=False)
response = studio_client.get("/admin/api/status")
assert response.status_code == 200
payload = response.json()
assert payload["recent_activity"][0] == {
"time": 0,
"model": "—",
"type": "unknown",
"status": 500,
"duration": 2.5,
}
assert payload["recent_activity"][1] == {
"time": 1715000000,
"model": "demo-model",
"type": "chat",
"status": 200,
"duration": 1.23,
}
def test_chat_template_wires_preview_shells_for_new_runnable_panels(): def test_chat_template_wires_preview_shells_for_new_runnable_panels():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment