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({ - - + - + + {customRange?.from && customRange?.to && ( - {compactCustomRangeLabel} + + {compactCustomRangeLabel} + )} {TIME_RANGE_PRESETS.map((option) => ( @@ -831,7 +849,10 @@ export default function DashboardPage() { className='h-8 w-full px-2.5 text-xs sm:ml-auto sm:w-auto' > {isManualRefreshing ? 'Refreshing...' : 'Refresh'} @@ -850,7 +871,9 @@ export default function DashboardPage() { metricType={activeChartConfig.metricType} tabs={chartConfigs.map((config) => ({ id: config.id, - label: isMobile ? config.mobileTitle ?? config.title : config.title, + label: isMobile + ? (config.mobileTitle ?? config.title) + : config.title, }))} activeTabId={activeChartId} onTabChange={setActiveChartId} @@ -865,7 +888,8 @@ export default function DashboardPage() { No data available - No metrics data exists for this range yet. Try a broader range. + No metrics data exists for this range yet. Try a broader + range. diff --git a/ui/app/providers/page.tsx b/ui/app/providers/page.tsx index 8a6536f9..fb62f054 100644 --- a/ui/app/providers/page.tsx +++ b/ui/app/providers/page.tsx @@ -104,17 +104,16 @@ export default function ProvidersPage() { refetchOnWindowFocus: false, }); - const { data: providerModels, isLoading: isLoadingModels } = useQuery< - ProviderModels | null - >({ - queryKey: ['provider-models', viewingModels], - queryFn: () => - viewingModels - ? AdminService.getProviderModels(viewingModels) - : Promise.resolve(null), - enabled: !!viewingModels, - refetchOnWindowFocus: false, - }); + const { data: providerModels, isLoading: isLoadingModels } = + useQuery({ + queryKey: ['provider-models', viewingModels], + queryFn: () => + viewingModels + ? AdminService.getProviderModels(viewingModels) + : Promise.resolve(null), + enabled: !!viewingModels, + refetchOnWindowFocus: false, + }); const createMutation = useMutation({ mutationFn: (data: CreateUpstreamProvider) => @@ -379,7 +378,9 @@ export default function ProvidersPage() { -

No providers configured

+

+ No providers configured +

Get started by adding your first upstream provider

@@ -398,9 +399,13 @@ export default function ProvidersPage() { isExpanded={expandedProviders.has(provider.id)} canShowBalance={canShowBalance(provider.provider_type)} platformUrl={getPlatformUrl(provider.provider_type)} - isModelsLoading={isLoadingModels && viewingModels === provider.id} + isModelsLoading={ + isLoadingModels && viewingModels === provider.id + } providerModels={ - viewingModels === provider.id ? providerModels ?? null : null + viewingModels === provider.id + ? (providerModels ?? null) + : null } isDeletingModel={deleteModelMutation.isPending} onToggleExpansion={() => toggleProviderExpansion(provider.id)} diff --git a/ui/app/unauthorized/page.tsx b/ui/app/unauthorized/page.tsx index 99718acb..48a0e159 100644 --- a/ui/app/unauthorized/page.tsx +++ b/ui/app/unauthorized/page.tsx @@ -22,7 +22,11 @@ export default function UnauthorizedPage() { - 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({
- +