Users
+ {actionError &&
{actionError}
}
{usersLoading &&
Loading users...
}
{usersError &&
Failed to load users: {usersError}
}
{users && (
diff --git a/frontend/src/pages/Analytics.test.tsx b/frontend/src/pages/Analytics.test.tsx
index 5ba6564..5aaf9a2 100644
--- a/frontend/src/pages/Analytics.test.tsx
+++ b/frontend/src/pages/Analytics.test.tsx
@@ -3,23 +3,16 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
-vi.mock('../hooks/useAuth', () => ({
- useAuth: vi.fn(),
-}))
-
vi.mock('../api/analytics', () => ({
getCostBreakdown: vi.fn(),
getTaskOutcomes: vi.fn(),
getEfficiency: vi.fn(),
}))
-import { useAuth } from '../hooks/useAuth'
import { getCostBreakdown, getTaskOutcomes, getEfficiency } from '../api/analytics'
import type { CostBreakdown, TaskOutcomes, Efficiency } from '../api/analytics'
import Analytics from './Analytics'
-const mockUseAuth = vi.mocked(useAuth)
-
const mockGetCostBreakdown = vi.mocked(getCostBreakdown)
const mockGetTaskOutcomes = vi.mocked(getTaskOutcomes)
const mockGetEfficiency = vi.mocked(getEfficiency)
@@ -71,14 +64,6 @@ function setupMocks() {
beforeEach(() => {
vi.resetAllMocks()
- mockUseAuth.mockReturnValue({
- user: { id: 'u1', email: 'admin@test.com', display_name: 'Admin', role: 'admin' },
- loading: false,
- login: vi.fn(),
- loginWithOIDC: vi.fn(),
- setUserFromOIDC: vi.fn(),
- logout: vi.fn(),
- })
})
describe('Analytics', () => {
@@ -201,18 +186,6 @@ describe('Analytics', () => {
expect(screen.getByText('Project Alpha')).toBeInTheDocument()
})
- it('shows access denied for non-admin users', () => {
- mockUseAuth.mockReturnValue({
- user: { id: 'u2', email: 'user@test.com', display_name: 'User', role: 'user' },
- loading: false,
- login: vi.fn(),
- loginWithOIDC: vi.fn(),
- setUserFromOIDC: vi.fn(),
- logout: vi.fn(),
- })
-
- render(
)
- expect(screen.getByText(/Access denied/)).toBeInTheDocument()
- expect(mockGetCostBreakdown).not.toHaveBeenCalled()
- })
+ // Note: admin role enforcement moved to RequireRole route guard (App.tsx).
+ // Analytics component itself always fetches data — access control is at the router level.
})
diff --git a/frontend/src/pages/Analytics.tsx b/frontend/src/pages/Analytics.tsx
index 5430b29..a52977e 100644
--- a/frontend/src/pages/Analytics.tsx
+++ b/frontend/src/pages/Analytics.tsx
@@ -6,14 +6,15 @@
// Used by: App.tsx
import { useEffect, useState } from 'react'
-import { useAuth } from '../hooks/useAuth'
import {
getCostBreakdown, getTaskOutcomes, getEfficiency,
type CostBreakdown, type TaskOutcomes, type Efficiency,
} from '../api/analytics'
const pctBar = (value: number, className = 'ok') => (
-
+
)
@@ -21,7 +22,6 @@ const pctBar = (value: number, className = 'ok') => (
const pctClass = (rate: number) => rate >= 0.8 ? 'ok' : rate >= 0.5 ? 'warn' : 'danger'
export default function Analytics() {
- const { user } = useAuth()
const [cost, setCost] = useState
(null)
const [outcomes, setOutcomes] = useState(null)
const [efficiency, setEfficiency] = useState(null)
@@ -29,7 +29,6 @@ export default function Analytics() {
const [loading, setLoading] = useState(true)
useEffect(() => {
- if (user?.role !== 'admin') { setLoading(false); return }
const errors: string[] = []
Promise.allSettled([
getCostBreakdown().then(setCost),
@@ -42,9 +41,8 @@ export default function Analytics() {
if (errors.length) setError(errors.join('; '))
setLoading(false)
})
- }, [user])
+ }, [])
- if (user?.role !== 'admin') return Access denied. Admin role required.
if (loading) return Loading analytics...
return (
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index 9ae691d..567851e 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -42,9 +42,10 @@ export default function Login() {
Sign In
{error && {error}
}
-
+
@@ -47,7 +49,9 @@ export default function Usage() {
${budget.monthly_spent_usd.toFixed(2)}
/ ${budget.monthly_limit_usd.toFixed(2)} ({budget.monthly_pct.toFixed(0)}%)
-
+
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 9013ef6..e9c958b 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -194,7 +194,7 @@ pre { background: var(--bg); padding: 0.75rem; border-radius: var(--radius); ove
background: var(--surface); color: var(--text); cursor: pointer;
font-size: 0.875rem; transition: background 0.15s;
}
-.oauth-btn:hover { background: var(--hover); }
+.oauth-btn:hover { background: var(--border); }
.oauth-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Sidebar user section */
@@ -322,3 +322,30 @@ pre { background: var(--bg); padding: 0.75rem; border-radius: var(--radius); ove
[data-theme="light"] pre { background: #f0f1f5; }
[data-theme="light"] .verification-card { background: #fafafa; }
[data-theme="light"] tr:hover { background: rgba(0,0,0,0.02); }
+
+/* Responsive — tablet */
+@media (max-width: 1023px) {
+ .grid-4 { grid-template-columns: 1fr 1fr; }
+ .grid-3 { grid-template-columns: 1fr 1fr; }
+}
+
+/* Responsive — mobile */
+@media (max-width: 767px) {
+ .layout { flex-direction: column; }
+ .sidebar {
+ width: 100%; flex-direction: row; flex-wrap: wrap;
+ border-right: none; border-bottom: 1px solid var(--border);
+ padding: 0.75rem; gap: 0.25rem; align-items: center;
+ }
+ .sidebar h1 { margin-bottom: 0; margin-right: auto; }
+ .sidebar a { padding: 0.375rem 0.5rem; font-size: 0.8rem; }
+ .sidebar-user {
+ flex-direction: row; align-items: center; margin-top: 0;
+ padding-top: 0; border-top: none; width: 100%;
+ }
+ .main { padding: 1rem; }
+ .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
+ .modal-content { width: 95%; max-width: none; }
+ table { font-size: 0.75rem; }
+ th, td { padding: 0.375rem; }
+}