Files
JakeBreath 99f617d296 Footer storage/backend display and a staff role that actually grants staff
Footer:
- Left is now 'Backend Storage:' with a capacity bar (blue, peach at 80%,
  red at 95% per DESIGN.md) and a used/total/free tooltip; the watched
  folder path is no longer printed. /api/status/ returns a compact storage
  summary instead of the path (the full storage page still shows paths to
  authenticated users).
- Centre shows the backend API origin (empty = same origin). Staff get a
  link to /setup to point the browser elsewhere; everyone else sees it as
  plain text. The Account 'Backend connection' card is gone — this is
  installation plumbing, not a per-user setting.
- Design spec updated to match.

Staff role:
- The custom role did nothing on several endpoints that only accepted
  Django's is_staff/is_superuser. One canonical check now exists:
  User.is_app_staff (superuser, Django staff, or the staff role), used by
  the stats/users APIs, item object permissions, can_delete, upload/
  similarity/download/match querysets, and the management commands
  (which also pick staff-role accounts for e621 sync/match and file
  ownership).

Verified with a role-only staff account (is_staff/is_superuser false):
stats/users 200, all 32 downloads + 2 scans visible, others' items
editable; the same account as role=user gets 403 for all of those.
2026-09-17 23:24:24 -05:00

94 lines
3.0 KiB
Python

import time
from django.conf import settings
from django.db import connection
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from . import stats
from .system_info import get_os_info
def _worker_counts():
"""Active/queued background jobs (downloads and e621 match scans)."""
from apps.library.models import DownloadTask, MatchTask
active = DownloadTask.objects.filter(
status=DownloadTask.STATUS_DOWNLOADING
).count() + MatchTask.objects.filter(status=MatchTask.STATUS_RUNNING).count()
queued = DownloadTask.objects.filter(
status=DownloadTask.STATUS_PENDING
).count() + MatchTask.objects.filter(status=MatchTask.STATUS_PENDING).count()
return {"active": active, "queued": queued}
class StatusView(APIView):
"""Runtime status used by the SPA shell (storage line, status pill, footer)."""
permission_classes = [AllowAny]
def get(self, request):
started = getattr(request, "start_time", time.perf_counter())
from apps.library.tools import storage_info
watched = storage_info()["watched_folder"]
payload = {
"app": "J621",
"env": settings.APP_ENV,
"git_hash": settings.GIT_COMMIT_HASH,
"version": settings.APP_VERSION,
"os": get_os_info(),
"storage": {
"used": watched["used"],
"total": watched["total"],
"free": watched["free"],
"percent_used": watched["percent_used"],
},
"e621_time_ms": None, # e621 latency is measured client-side
"workers": _worker_counts(),
}
payload["server_time_ms"] = round((time.perf_counter() - started) * 1000, 1)
return Response(payload)
class StatsView(APIView):
"""Staff-only runtime statistics for the dashboard."""
permission_classes = [IsAuthenticated]
def get(self, request):
if not request.user.is_app_staff:
raise PermissionDenied("Staff only.")
from apps.library.tools import storage_info
return Response(
{
"system": stats.system_stats(),
"gpus": stats.gpu_stats(),
"disk": storage_info(),
"jobs": stats.jobs_stats(),
"log": stats.log_tail(),
}
)
class HealthView(APIView):
"""Unauthenticated liveness probe for uptime monitors (hits /health)."""
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request):
database = True
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
except Exception: # noqa: BLE001 - report the failure, do not crash
database = False
return Response(
{"status": "ok" if database else "degraded", "database": database},
status=200 if database else 503,
)