diff --git a/routstr/core/admin.py b/routstr/core/admin.py
index 21f54f74..ef363d2c 100644
--- a/routstr/core/admin.py
+++ b/routstr/core/admin.py
@@ -2,6 +2,7 @@ import json
import secrets
from datetime import datetime, timezone
from pathlib import Path
+from typing import NoReturn
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
@@ -46,7 +47,7 @@ def _cleanup_expired_admin_sessions(now_timestamp: int | None = None) -> None:
admin_sessions.pop(token, None)
-def _raise_unauthorized(detail: str) -> None:
+def _raise_unauthorized(detail: str) -> NoReturn:
raise HTTPException(
status_code=401,
detail=detail,
diff --git a/tests/integration/test_general_info_endpoints.py b/tests/integration/test_general_info_endpoints.py
index d9399f09..48528793 100644
--- a/tests/integration/test_general_info_endpoints.py
+++ b/tests/integration/test_general_info_endpoints.py
@@ -264,12 +264,13 @@ async def test_models_endpoint_accept_headers(integration_client: AsyncClient) -
async def test_admin_endpoint_unauthenticated(
integration_client: AsyncClient, db_snapshot: Any
) -> None:
- """Test GET /admin/ endpoint redirects to /"""
+ """Test unauthenticated access to admin settings endpoint is rejected."""
await db_snapshot.capture()
response = await integration_client.get("/admin/api/settings")
- assert response.status_code == 403
+ assert response.status_code == 401
+ assert response.headers.get("www-authenticate") == "Bearer"
diff = await db_snapshot.diff()
assert len(diff["api_keys"]["added"]) == 0
diff --git a/ui/.prettierignore b/ui/.prettierignore
new file mode 100644
index 00000000..50268ec1
--- /dev/null
+++ b/ui/.prettierignore
@@ -0,0 +1,5 @@
+pnpm-lock.yaml
+.next
+node_modules
+out
+next-env.d.ts
diff --git a/ui/app/globals.css b/ui/app/globals.css
index d56b97f0..30eb4979 100644
--- a/ui/app/globals.css
+++ b/ui/app/globals.css
@@ -1,16 +1,16 @@
-@import "tailwindcss";
-@import "tw-animate-css";
+@import 'tailwindcss';
+@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *, .red *));
:root {
--radius: 0.625rem;
--font-geist-sans:
- ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
- "Apple Color Emoji", "Segoe UI Emoji";
+ ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica,
+ Arial, 'Apple Color Emoji', 'Segoe UI Emoji';
--font-geist-mono:
- ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
- "Courier New", monospace;
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
+ 'Courier New', monospace;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
@@ -171,10 +171,10 @@ body {
}
button,
- [type="button"],
- [type="submit"],
- [type="reset"],
- [role="button"] {
+ [type='button'],
+ [type='submit'],
+ [type='reset'],
+ [role='button'] {
cursor: pointer;
}
}
diff --git a/ui/app/layout.tsx b/ui/app/layout.tsx
index 08eecccb..92c51b56 100644
--- a/ui/app/layout.tsx
+++ b/ui/app/layout.tsx
@@ -1,19 +1,10 @@
import type { Metadata } from 'next';
-import { Geist, Geist_Mono } from 'next/font/google';
+import { GeistMono } from 'geist/font/mono';
+import { GeistSans } from 'geist/font/sans';
import './globals.css';
import { Providers } from './providers';
import { SuppressHydrationWarning } from '@/components/suppress-hydration-warning';
-const geistSans = Geist({
- variable: '--font-geist-sans',
- subsets: ['latin'],
-});
-
-const geistMono = Geist_Mono({
- variable: '--font-geist-mono',
- subsets: ['latin'],
-});
-
export const metadata: Metadata = {
title: 'Routstr',
description: 'Routstr model management',
@@ -29,7 +20,9 @@ export default function RootLayout({
}>) {
return (
-
+
{children}
diff --git a/ui/app/logs/log-filters.tsx b/ui/app/logs/log-filters.tsx
index 88a2ea54..38adc300 100644
--- a/ui/app/logs/log-filters.tsx
+++ b/ui/app/logs/log-filters.tsx
@@ -1,4 +1,9 @@
-import { useEffect, useState, type ChangeEvent, type KeyboardEvent } from 'react';
+import {
+ useEffect,
+ useState,
+ type ChangeEvent,
+ type KeyboardEvent,
+} from 'react';
import { format } from 'date-fns';
import { CalendarIcon, Filter, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -88,8 +93,12 @@ const ENDPOINT_OPTIONS = [
'/embeddings/models',
];
-const STATUS_4XX_CODES = STATUS_CODE_OPTIONS.filter((code) => code.startsWith('4'));
-const STATUS_5XX_CODES = STATUS_CODE_OPTIONS.filter((code) => code.startsWith('5'));
+const STATUS_4XX_CODES = STATUS_CODE_OPTIONS.filter((code) =>
+ code.startsWith('4')
+);
+const STATUS_5XX_CODES = STATUS_CODE_OPTIONS.filter((code) =>
+ code.startsWith('5')
+);
export function LogFilters({
selectedDate,
@@ -194,7 +203,9 @@ export function LogFilters({
const handleQuickStatusCode = (range: '4xx' | '5xx') => {
const rangeCodes = range === '4xx' ? STATUS_4XX_CODES : STATUS_5XX_CODES;
const nextSelection = new Set(selectedStatusCodes);
- const allSelected = rangeCodes.every((code) => selectedStatusCodes.includes(code));
+ const allSelected = rangeCodes.every((code) =>
+ selectedStatusCodes.includes(code)
+ );
if (allSelected) {
rangeCodes.forEach((code) => nextSelection.delete(code));
@@ -414,7 +425,11 @@ export function LogFilters({
-
diff --git a/ui/app/logs/multi-select-command-filter.tsx b/ui/app/logs/multi-select-command-filter.tsx
index 1f141b84..00b49ede 100644
--- a/ui/app/logs/multi-select-command-filter.tsx
+++ b/ui/app/logs/multi-select-command-filter.tsx
@@ -44,7 +44,10 @@ interface MultiSelectCommandFilterProps {
function FilterBadge({ value }: { value: string }) {
return (
-
+
{value}
@@ -93,7 +96,10 @@ export function MultiSelectCommandFilter({
-
+
{selectedValues.length > 0 ? (
selectedValues.map((value) => (
@@ -158,7 +164,10 @@ export function MultiSelectCommandFilter({
{options
.filter((option) => !selectedValues.includes(option))
.map((option) => (
-
toggleSelection(option)}>
+ toggleSelection(option)}
+ >
{option}
diff --git a/ui/app/logs/page.tsx b/ui/app/logs/page.tsx
index e375acdf..b531116a 100644
--- a/ui/app/logs/page.tsx
+++ b/ui/app/logs/page.tsx
@@ -226,7 +226,10 @@ export default function LogsPage() {
{isLoading ? (
{Array.from({ length: 8 }).map((_, index) => (
-
+
))}
) : logsData?.logs && logsData.logs.length > 0 ? (
diff --git a/ui/app/page.tsx b/ui/app/page.tsx
index 12573d62..cdc254e8 100644
--- a/ui/app/page.tsx
+++ b/ui/app/page.tsx
@@ -16,12 +16,7 @@ import {
type UsageSummary,
} from '@/lib/api/services/admin';
import { Button } from '@/components/ui/button';
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
-} from '@/components/ui/card';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Calendar } from '@/components/ui/calendar';
import { Badge } from '@/components/ui/badge';
import {
@@ -179,8 +174,9 @@ function getAutoIntervalMinutes(hours: number): number {
const allowedIntervals = [5, 15, 30, 60, 120, 180, 240, 360, 480, 720, 1440];
return (
- allowedIntervals.find((intervalMinutes) => intervalMinutes >= idealInterval) ??
- allowedIntervals[allowedIntervals.length - 1]
+ allowedIntervals.find(
+ (intervalMinutes) => intervalMinutes >= idealInterval
+ ) ?? allowedIntervals[allowedIntervals.length - 1]
);
}
@@ -480,7 +476,9 @@ export default function DashboardPage() {
DEFAULT_TIME_RANGE_PRESET;
const customRangeHours = getRangeHours(customRange);
const queryHours =
- isCustomRangeActive && customRangeHours ? customRangeHours : activePreset.hours;
+ isCustomRangeActive && customRangeHours
+ ? customRangeHours
+ : activePreset.hours;
const autoInterval = getAutoIntervalMinutes(queryHours);
const {
@@ -537,12 +535,14 @@ export default function DashboardPage() {
}
const metricPoints = metricsData.metrics as ChartDatum[];
- const revenuePoints = metricsData.metrics.map((metric: UsageMetricData) => ({
- ...metric,
- revenue_sats: metric.revenue_msats / 1000,
- refunds_sats: metric.refunds_msats / 1000,
- net_revenue_sats: (metric.revenue_msats - metric.refunds_msats) / 1000,
- })) as ChartDatum[];
+ const revenuePoints = metricsData.metrics.map(
+ (metric: UsageMetricData) => ({
+ ...metric,
+ revenue_sats: metric.revenue_msats / 1000,
+ refunds_sats: metric.refunds_msats / 1000,
+ net_revenue_sats: (metric.revenue_msats - metric.refunds_msats) / 1000,
+ })
+ ) as ChartDatum[];
return [
{
@@ -731,7 +731,8 @@ export default function DashboardPage() {
};
const activeChartConfig =
- chartConfigs.find((config) => config.id === activeChartId) ?? chartConfigs[0];
+ chartConfigs.find((config) => config.id === activeChartId) ??
+ chartConfigs[0];
const selectedRangeValue =
isCustomRangeActive && customRange?.from && customRange?.to
? 'custom'
@@ -746,22 +747,28 @@ export default function DashboardPage() {
- Dashboard
+
+ Dashboard
+
Node balances, request health, and revenue trends.
-
+
-
+
Usage Analytics
- Select a preset or custom date range to analyze traffic and revenue.
+ Select a preset or custom date range to analyze traffic and
+ revenue.
Showing {activeRangeLabel}.
@@ -792,7 +799,10 @@ export default function DashboardPage() {
-
+
-
diff --git a/ui/components/api-endpoint-form.tsx b/ui/components/api-endpoint-form.tsx
index 146c6f1d..071eb153 100644
--- a/ui/components/api-endpoint-form.tsx
+++ b/ui/components/api-endpoint-form.tsx
@@ -51,7 +51,9 @@ interface ApiEndpointFormProps {
setImageCount: Dispatch
>;
imageSize: '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792';
setImageSize: Dispatch<
- SetStateAction<'256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792'>
+ SetStateAction<
+ '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792'
+ >
>;
imageQuality: 'standard' | 'hd';
setImageQuality: Dispatch>;
@@ -167,7 +169,9 @@ export function ApiEndpointForm({
min={1}
max={4000}
value={maxTokens}
- onChange={(event) => setMaxTokens(parseInt(event.target.value) || 150)}
+ onChange={(event) =>
+ setMaxTokens(parseInt(event.target.value) || 150)
+ }
/>
@@ -244,7 +248,9 @@ export function ApiEndpointForm({
-
+
@@ -510,17 +544,23 @@ export function ApiEndpointForm({
-
+
-
+
-
+
-
+
-
-
+
+
{recordedAudio && (
diff --git a/ui/components/api-endpoint-response.tsx b/ui/components/api-endpoint-response.tsx
index 30e7ff30..7f04d5bc 100644
--- a/ui/components/api-endpoint-response.tsx
+++ b/ui/components/api-endpoint-response.tsx
@@ -119,7 +119,11 @@ const isAudioTranscriptionResponse = (
return 'text' in response;
};
-export function ApiEndpointResponse({ response }: { response: ApiResponse | null }) {
+export function ApiEndpointResponse({
+ response,
+}: {
+ response: ApiResponse | null;
+}) {
if (!response) {
return null;
}
@@ -164,7 +168,8 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
- {response.choices?.[0]?.message?.content || 'No content in response'}
+ {response.choices?.[0]?.message?.content ||
+ 'No content in response'}
@@ -174,15 +179,21 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
-
{response.usage.prompt_tokens}
+
+ {response.usage.prompt_tokens}
+
Prompt Tokens
-
{response.usage.completion_tokens}
+
+ {response.usage.completion_tokens}
+
Completion Tokens
-
{response.usage.total_tokens}
+
+ {response.usage.total_tokens}
+
Total Tokens
@@ -207,7 +218,11 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
Show first 10 values
- {JSON.stringify(response.data?.[0]?.embedding?.slice(0, 10), null, 2)}
+ {JSON.stringify(
+ response.data?.[0]?.embedding?.slice(0, 10),
+ null,
+ 2
+ )}
...
@@ -274,7 +289,8 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
)}
{model.created && (
- Created: {new Date(model.created * 1000).toLocaleDateString()}
+ Created:{' '}
+ {new Date(model.created * 1000).toLocaleDateString()}
)}
diff --git a/ui/components/app-page-shell.tsx b/ui/components/app-page-shell.tsx
index e57ec744..c4e02c48 100644
--- a/ui/components/app-page-shell.tsx
+++ b/ui/components/app-page-shell.tsx
@@ -79,7 +79,7 @@ export function AppPageShell({
return (
-
+
-
Routstr Node
+
+ Routstr Node
+
)}
setIsSidebarCollapsed((current) => !current)}
>
@@ -196,16 +198,16 @@ export function AppPageShell({
>
) : (
-
+
-
+
-