From d70bf820e15597991245796e8135ae9aeb1b73ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:21:39 +0000 Subject: [PATCH 1/3] feat: add admin stats endpoint and observability dashboard updates Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com> Agent-Logs-Url: https://github.com/BTreeMap/OneShot/sessions/d47e0923-07d8-4f34-b78c-abef458ea389 --- api/app/uploads/router.py | 47 +++ api/openapi.json | 67 +++- api/tests/test_oneshot_integration.py | 13 + web/src/api/openapi.ts | 55 ++++ web/src/api/types.ts | 5 + web/src/pages/Admin.tsx | 54 +++- web/src/pages/Dashboard.tsx | 348 ++++++++------------- web/src/pages/__tests__/Admin.test.tsx | 3 + web/src/pages/__tests__/Dashboard.test.tsx | 46 +++ 9 files changed, 408 insertions(+), 230 deletions(-) create mode 100644 web/src/pages/__tests__/Dashboard.test.tsx diff --git a/api/app/uploads/router.py b/api/app/uploads/router.py index 3f18f89..ae714fe 100644 --- a/api/app/uploads/router.py +++ b/api/app/uploads/router.py @@ -56,6 +56,15 @@ class OneShotTokenAuditItem(BaseModel): target_email: str | None is_used: bool created_at: datetime + expires_at: datetime + + +class OneShotStatsResponse(BaseModel): + total_files: int + total_storage_bytes: int + tokens_issued: int + tokens_used: int + active_tokens: int class FileAuditItem(BaseModel): @@ -239,11 +248,49 @@ async def list_oneshot_tokens( target_email=row.target_email, is_used=row.is_used, created_at=row.created_at, + expires_at=row.expires_at, ) for row in rows ] +@router.get("/admin/stats", response_model=OneShotStatsResponse) +async def get_oneshot_stats( + db: AsyncSession = Depends(_db_dep), + _admin_user: User = require_admin(), +) -> OneShotStatsResponse: + total_files = ( + await db.execute(select(func.count(FileMetadata.id))) + ).scalar_one() + total_storage_bytes = ( + await db.execute(select(func.coalesce(func.sum(FileMetadata.size_bytes), 0))) + ).scalar_one() + tokens_issued = ( + await db.execute(select(func.count(OneShotToken.id))) + ).scalar_one() + tokens_used = ( + await db.execute( + select(func.count(OneShotToken.id)).where(OneShotToken.is_used.is_(True)) + ) + ).scalar_one() + active_tokens = ( + await db.execute( + select(func.count(OneShotToken.id)).where( + OneShotToken.is_used.is_(False), + OneShotToken.expires_at > func.now(), + ) + ) + ).scalar_one() + + return OneShotStatsResponse( + total_files=total_files, + total_storage_bytes=total_storage_bytes, + tokens_issued=tokens_issued, + tokens_used=tokens_used, + active_tokens=active_tokens, + ) + + @router.get("/admin/files", response_model=list[FileAuditItem]) async def list_uploaded_files( db: AsyncSession = Depends(_db_dep), diff --git a/api/openapi.json b/api/openapi.json index f977427..3cea4f4 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -214,6 +214,11 @@ "title": "Created At", "type": "string" }, + "expires_at": { + "format": "date-time", + "title": "Expires At", + "type": "string" + }, "id": { "title": "Id", "type": "string" @@ -238,11 +243,45 @@ "id", "target_email", "is_used", - "created_at" + "created_at", + "expires_at" ], "title": "OneShotTokenAuditItem", "type": "object" }, + "OneShotStatsResponse": { + "properties": { + "active_tokens": { + "title": "Active Tokens", + "type": "integer" + }, + "tokens_issued": { + "title": "Tokens Issued", + "type": "integer" + }, + "tokens_used": { + "title": "Tokens Used", + "type": "integer" + }, + "total_files": { + "title": "Total Files", + "type": "integer" + }, + "total_storage_bytes": { + "title": "Total Storage Bytes", + "type": "integer" + } + }, + "required": [ + "total_files", + "total_storage_bytes", + "tokens_issued", + "tokens_used", + "active_tokens" + ], + "title": "OneShotStatsResponse", + "type": "object" + }, "OneShotUploadResponse": { "properties": { "file_id": { @@ -763,6 +802,32 @@ ] } }, + "/api/admin/stats": { + "get": { + "operationId": "get_oneshot_stats_api_admin_stats_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OneShotStatsResponse" + } + } + }, + "description": "Successful Response" + } + }, + "security": [ + { + "DeviceJWT": [] + } + ], + "summary": "Get Oneshot Stats", + "tags": [ + "oneshot" + ] + } + }, "/api/admin/files/{file_id}/download": { "get": { "operationId": "download_file_api_admin_files__file_id__download_get", diff --git a/api/tests/test_oneshot_integration.py b/api/tests/test_oneshot_integration.py index 0f617c1..b3fcf91 100644 --- a/api/tests/test_oneshot_integration.py +++ b/api/tests/test_oneshot_integration.py @@ -110,6 +110,7 @@ async def _assert_expiry_is_set() -> None: assert token_row["target_email"] == "recipient@example.com" assert token_row["is_used"] is True assert token_row["created_at"] + assert token_row["expires_at"] files_response = client.get( "/api/admin/files", @@ -122,6 +123,18 @@ async def _assert_expiry_is_set() -> None: assert file_row["size_bytes"] == len(b"classified-bytes") assert file_row["created_at"] + stats_response = client.get( + "/api/admin/stats", + headers={"Authorization": f"Bearer {admin_jwt}"}, + ) + assert stats_response.status_code == 200 + stats = stats_response.json() + assert stats["total_files"] >= 1 + assert stats["total_storage_bytes"] >= len(b"classified-bytes") + assert stats["tokens_issued"] >= 1 + assert stats["tokens_used"] >= 1 + assert stats["active_tokens"] >= 0 + download_response = client.get( f"/api/admin/files/{file_id}/download", headers={"Authorization": f"Bearer {admin_jwt}"}, diff --git a/web/src/api/openapi.ts b/web/src/api/openapi.ts index 1c30e20..868f435 100644 --- a/web/src/api/openapi.ts +++ b/web/src/api/openapi.ts @@ -41,6 +41,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/admin/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Oneshot Stats */ + get: operations["get_oneshot_stats_api_admin_stats_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/admin/files/{file_id}/download": { parameters: { query?: never; @@ -479,6 +496,11 @@ export interface components { * Format: date-time */ created_at: string; + /** + * Expires At + * Format: date-time + */ + expires_at: string; /** Id */ id: string; /** Is Used */ @@ -486,6 +508,19 @@ export interface components { /** Target Email */ target_email: string | null; }; + /** OneShotStatsResponse */ + OneShotStatsResponse: { + /** Active Tokens */ + active_tokens: number; + /** Tokens Issued */ + tokens_issued: number; + /** Tokens Used */ + tokens_used: number; + /** Total Files */ + total_files: number; + /** Total Storage Bytes */ + total_storage_bytes: number; + }; /** OneShotUploadResponse */ OneShotUploadResponse: { /** File Id */ @@ -784,6 +819,26 @@ export interface operations { }; }; }; + get_oneshot_stats_api_admin_stats_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OneShotStatsResponse"]; + }; + }; + }; + }; download_file_api_admin_files__file_id__download_get: { parameters: { query?: never; diff --git a/web/src/api/types.ts b/web/src/api/types.ts index de8da5c..70101e6 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -40,6 +40,9 @@ export type EchoResponse = components["schemas"]["EchoResponse"]; /** Item body for GET /api/admin/oneshot-tokens */ export type OneShotTokenAuditItem = components["schemas"]["OneShotTokenAuditItem"]; +/** Response body for GET /api/admin/stats */ +export type OneShotStatsResponse = components["schemas"]["OneShotStatsResponse"]; + /** Item body for GET /api/admin/files */ export type FileAuditItem = components["schemas"]["FileAuditItem"]; @@ -51,6 +54,7 @@ type _AssertDemoEchoPost = paths["/demo/echo"]["post"]; type _AssertDemoPingGet = paths["/demo/ping"]["get"]; type _AssertDemoSseGet = paths["/demo/sse"]["get"]; type _AssertAdminOneShotTokensGet = paths["/api/admin/oneshot-tokens"]["get"]; +type _AssertAdminStatsGet = paths["/api/admin/stats"]["get"]; type _AssertAdminFilesGet = paths["/api/admin/files"]["get"]; type _AssertAdminFileDownloadGet = paths["/api/admin/files/{file_id}/download"]["get"]; @@ -62,6 +66,7 @@ export type { _AssertDemoPingGet, _AssertDemoSseGet, _AssertAdminOneShotTokensGet, + _AssertAdminStatsGet, _AssertAdminFilesGet, _AssertAdminFileDownloadGet, }; diff --git a/web/src/pages/Admin.tsx b/web/src/pages/Admin.tsx index a98470b..f061fb5 100644 --- a/web/src/pages/Admin.tsx +++ b/web/src/pages/Admin.tsx @@ -26,6 +26,23 @@ function formatBytes(sizeBytes: number): string { return `${sizeBytes} B`; } +function tokenStatus(tokenRow: OneShotTokenAuditItem): "Used" | "Expired" | "Active" { + if (tokenRow.is_used) { + return "Used"; + } + return new Date(tokenRow.expires_at) < new Date() ? "Expired" : "Active"; +} + +function statusClassName(status: "Used" | "Expired" | "Active"): string { + if (status === "Used") { + return "bg-success/15 text-success"; + } + if (status === "Expired") { + return "bg-danger/15 text-danger"; + } + return "bg-primary/15 text-primary"; +} + function parseDispositionFilename(contentDisposition: string | null): string | null { if (!contentDisposition) { return null; @@ -249,21 +266,36 @@ export function Admin() {
- Overview for {user?.id}. -
-+ OneShot observability for {userId}. +
{stat.change}
+- {sale.name} -
-{sale.email}
-vs last 30 days
-vs last 30 days
-avg. per campaign
-+ No standard external accounts are provisioned. Access is granted through + ephemeral single-use upload links only. +
++ Uploaded files are neutralized at rest as opaque extensionless blobs, with + original metadata preserved only in controlled metadata records. +
++ Token redemption is atomic and irreversible, preventing replay and ensuring + links expire exactly once. +
+