From 71b65e8582c5dbc77586bc802bc83240338f5c75 Mon Sep 17 00:00:00 2001 From: Max Malkin Date: Tue, 3 Mar 2026 16:45:46 -0700 Subject: [PATCH 1/4] approval-ui: add Grafana availability probe and format with prettier The iframe onError event doesn't fire for ERR_CONNECTION_REFUSED, so probing Grafana availability on mount ensures the custom error state displays when Grafana is unreachable. Also reformatted with prettier. --- services/approval-ui/src/App.tsx | 224 +++-- services/approval-ui/src/Router.tsx | 211 ++-- services/approval-ui/src/api.ts | 249 ++--- services/approval-ui/src/index.css | 150 ++- services/approval-ui/src/index.html | 113 ++- services/approval-ui/src/main.tsx | 18 +- .../src/pages/AgentActivityPage.tsx | 897 ++++++++++------- services/approval-ui/src/pages/AgentsPage.tsx | 378 ++++--- .../approval-ui/src/pages/ApprovalPage.tsx | 948 ++++++++++-------- .../approval-ui/src/pages/DashboardPage.tsx | 210 ++-- services/approval-ui/src/pages/index.ts | 8 +- services/approval-ui/src/server.ts | 24 +- services/approval-ui/src/types.ts | 140 +-- 13 files changed, 2026 insertions(+), 1544 deletions(-) diff --git a/services/approval-ui/src/App.tsx b/services/approval-ui/src/App.tsx index 75ed44d..76f6dea 100644 --- a/services/approval-ui/src/App.tsx +++ b/services/approval-ui/src/App.tsx @@ -1,93 +1,165 @@ -import { Router, Link } from './Router'; -import { ApprovalPage, AgentsPage, AgentActivityPage, DashboardPage } from './pages'; +import { Router, Link } from "./Router"; +import { + ApprovalPage, + AgentsPage, + AgentActivityPage, + DashboardPage, +} from "./pages"; const routes = [ - { pattern: '/', component: HomePage }, - { pattern: '/approve/:grant_id', component: ApprovalPage }, - { pattern: '/agents', component: AgentsPage }, - { pattern: '/agents/:agent_id/activity', component: AgentActivityPage }, - { pattern: '/dashboard', component: DashboardPage }, + { pattern: "/", component: HomePage }, + { pattern: "/approve/:grant_id", component: ApprovalPage }, + { pattern: "/agents", component: AgentsPage }, + { pattern: "/agents/:agent_id/activity", component: AgentActivityPage }, + { pattern: "/dashboard", component: DashboardPage }, ]; function HomePage() { - return ( -
-
- {/* Logo mark */} -
-
- - - - -
- - AGENTAUTH - -
+ return ( +
+
+ {/* Logo mark */} +
+
+ + + + +
+ + AGENTAUTH + +
-

- PERMISSION CONTROL INTERFACE -

-
+

+ PERMISSION CONTROL INTERFACE +

+
-
- - - - - - - - VIEW AGENTS - - - - - - - DASHBOARD - -
-
+
+ + + + + + + + VIEW AGENTS + + + + + + + DASHBOARD + +
+
- {/* Grid decoration */} -
-
-
-
-
-
- ); + {/* Grid decoration */} +
+
+
+
+
+
+ ); } function NotFound() { - return ( -
-
404
-

SECTOR NOT FOUND

- - RETURN TO BASE - -
- ); + return ( +
+
+ 404 +
+

+ SECTOR NOT FOUND +

+ + RETURN TO BASE + +
+ ); } function App() { - return ( -
- -
- ); + return ( +
+ +
+ ); } export default App; diff --git a/services/approval-ui/src/Router.tsx b/services/approval-ui/src/Router.tsx index 8d52a3b..025c9c9 100644 --- a/services/approval-ui/src/Router.tsx +++ b/services/approval-ui/src/Router.tsx @@ -1,146 +1,143 @@ // Simple client-side router for the approval UI -import { useState, useEffect, createContext, useContext } from 'react'; +import { useState, useEffect, createContext, useContext } from "react"; interface RouteParams { - [key: string]: string; + [key: string]: string; } interface RouterContextValue { - path: string; - params: RouteParams; - navigate: (path: string) => void; + path: string; + params: RouteParams; + navigate: (path: string) => void; } const RouterContext = createContext({ - path: '/', - params: {}, - navigate: () => {}, + path: "/", + params: {}, + navigate: () => {}, }); /** Hook to access router context */ export function useRouter() { - return useContext(RouterContext); + return useContext(RouterContext); } /** Hook to get route parameters */ export function useParams(): T { - const { params } = useRouter(); - return params as T; + const { params } = useRouter(); + return params as T; } /** Parse route pattern and extract params */ -function matchRoute( - pattern: string, - path: string -): RouteParams | null { - const patternParts = pattern.split('/').filter(Boolean); - const pathParts = path.split('/').filter(Boolean); - - if (patternParts.length !== pathParts.length) { - return null; - } - - const params: RouteParams = {}; - - for (let i = 0; i < patternParts.length; i++) { - const patternPart = patternParts[i]!; - const pathPart = pathParts[i]!; - - if (patternPart.startsWith(':')) { - // This is a parameter - const paramName = patternPart.slice(1); - params[paramName] = decodeURIComponent(pathPart); - } else if (patternPart !== pathPart) { - // Static part doesn't match - return null; - } - } - - return params; +function matchRoute(pattern: string, path: string): RouteParams | null { + const patternParts = pattern.split("/").filter(Boolean); + const pathParts = path.split("/").filter(Boolean); + + if (patternParts.length !== pathParts.length) { + return null; + } + + const params: RouteParams = {}; + + for (let i = 0; i < patternParts.length; i++) { + const patternPart = patternParts[i]!; + const pathPart = pathParts[i]!; + + if (patternPart.startsWith(":")) { + // This is a parameter + const paramName = patternPart.slice(1); + params[paramName] = decodeURIComponent(pathPart); + } else if (patternPart !== pathPart) { + // Static part doesn't match + return null; + } + } + + return params; } interface Route { - pattern: string; - component: React.ComponentType; + pattern: string; + component: React.ComponentType; } interface RouterProps { - routes: Route[]; - notFound?: React.ComponentType; + routes: Route[]; + notFound?: React.ComponentType; } /** Router component */ export function Router({ routes, notFound: NotFound }: RouterProps) { - const [path, setPath] = useState(window.location.pathname); - - useEffect(() => { - const handlePopState = () => { - setPath(window.location.pathname); - }; - - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, []); - - const navigate = (newPath: string) => { - window.history.pushState({}, '', newPath); - setPath(newPath); - }; - - // Find matching route - let matchedRoute: Route | null = null; - let params: RouteParams = {}; - - for (const route of routes) { - const match = matchRoute(route.pattern, path); - if (match !== null) { - matchedRoute = route; - params = match; - break; - } - } - - const contextValue: RouterContextValue = { - path, - params, - navigate, - }; - - return ( - - {matchedRoute ? ( - - ) : NotFound ? ( - - ) : ( -
-

404 - Page Not Found

-

The page you're looking for doesn't exist.

-
- )} -
- ); + const [path, setPath] = useState(window.location.pathname); + + useEffect(() => { + const handlePopState = () => { + setPath(window.location.pathname); + }; + + window.addEventListener("popstate", handlePopState); + return () => window.removeEventListener("popstate", handlePopState); + }, []); + + const navigate = (newPath: string) => { + window.history.pushState({}, "", newPath); + setPath(newPath); + }; + + // Find matching route + let matchedRoute: Route | null = null; + let params: RouteParams = {}; + + for (const route of routes) { + const match = matchRoute(route.pattern, path); + if (match !== null) { + matchedRoute = route; + params = match; + break; + } + } + + const contextValue: RouterContextValue = { + path, + params, + navigate, + }; + + return ( + + {matchedRoute ? ( + + ) : NotFound ? ( + + ) : ( +
+

404 - Page Not Found

+

The page you're looking for doesn't exist.

+
+ )} +
+ ); } /** Link component for navigation */ interface LinkProps extends React.AnchorHTMLAttributes { - to: string; - children: React.ReactNode; + to: string; + children: React.ReactNode; } export function Link({ to, children, onClick, ...props }: LinkProps) { - const { navigate } = useRouter(); - - const handleClick = (e: React.MouseEvent) => { - e.preventDefault(); - if (onClick) onClick(e); - navigate(to); - }; - - return ( - - {children} - - ); + const { navigate } = useRouter(); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + if (onClick) onClick(e); + navigate(to); + }; + + return ( + + {children} + + ); } diff --git a/services/approval-ui/src/api.ts b/services/approval-ui/src/api.ts index a2e25c8..5f36f45 100644 --- a/services/approval-ui/src/api.ts +++ b/services/approval-ui/src/api.ts @@ -1,182 +1,185 @@ // AgentAuth Registry API Client with CSRF Protection import type { - GrantRequest, - AgentSummary, - AgentDetails, - AuditEvent, - ApprovalAssertion, - ApiError, -} from './types'; + GrantRequest, + AgentSummary, + AgentDetails, + AuditEvent, + ApprovalAssertion, + ApiError, +} from "./types"; -const REGISTRY_URL = 'http://localhost:8080'; +const REGISTRY_URL = "http://localhost:8080"; /** CSRF token stored in memory and synced with cookie */ let csrfToken: string | null = null; /** Generate a random CSRF token */ function generateCsrfToken(): string { - const array = new Uint8Array(32); - crypto.getRandomValues(array); - return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join(''); + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join(""); } /** Get or create CSRF token */ export function getCsrfToken(): string { - if (!csrfToken) { - // Try to read from cookie first - const cookies = document.cookie.split(';'); - for (const cookie of cookies) { - const [name, value] = cookie.trim().split('='); - if (name === 'csrf_token' && value) { - csrfToken = value; - break; - } - } - // Generate new token if not found - if (!csrfToken) { - csrfToken = generateCsrfToken(); - // Set as SameSite=Strict cookie - document.cookie = `csrf_token=${csrfToken}; SameSite=Strict; Secure; Path=/`; - } - } - return csrfToken; + if (!csrfToken) { + // Try to read from cookie first + const cookies = document.cookie.split(";"); + for (const cookie of cookies) { + const [name, value] = cookie.trim().split("="); + if (name === "csrf_token" && value) { + csrfToken = value; + break; + } + } + // Generate new token if not found + if (!csrfToken) { + csrfToken = generateCsrfToken(); + // Set as SameSite=Strict cookie + document.cookie = `csrf_token=${csrfToken}; SameSite=Strict; Secure; Path=/`; + } + } + return csrfToken; } /** Custom error class for API errors */ export class RegistryError extends Error { - code: string; - details?: Record; - - constructor(apiError: ApiError) { - super(apiError.error); - this.name = 'RegistryError'; - this.code = apiError.code; - this.details = apiError.details; - } + code: string; + details?: Record; + + constructor(apiError: ApiError) { + super(apiError.error); + this.name = "RegistryError"; + this.code = apiError.code; + this.details = apiError.details; + } } /** Make an authenticated request to the registry */ -async function request( - path: string, - options: RequestInit = {} -): Promise { - const url = `${REGISTRY_URL}${path}`; - const headers = new Headers(options.headers); - - // Add CSRF token for state-changing requests - if (options.method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(options.method)) { - headers.set('X-CSRF-Token', getCsrfToken()); - } - - // Add content type for JSON bodies - if (options.body && typeof options.body === 'string') { - headers.set('Content-Type', 'application/json'); - } - - // Add credentials for cookie handling - const response = await fetch(url, { - ...options, - headers, - credentials: 'include', - }); - - if (!response.ok) { - let apiError: ApiError; - try { - apiError = (await response.json()) as ApiError; - } catch { - apiError = { - error: `Request failed with status ${response.status}`, - code: 'REQUEST_FAILED', - }; - } - throw new RegistryError(apiError); - } - - // Handle empty responses - const text = await response.text(); - if (!text) { - return {} as T; - } - - return JSON.parse(text) as T; +async function request(path: string, options: RequestInit = {}): Promise { + const url = `${REGISTRY_URL}${path}`; + const headers = new Headers(options.headers); + + // Add CSRF token for state-changing requests + if ( + options.method && + ["POST", "PUT", "DELETE", "PATCH"].includes(options.method) + ) { + headers.set("X-CSRF-Token", getCsrfToken()); + } + + // Add content type for JSON bodies + if (options.body && typeof options.body === "string") { + headers.set("Content-Type", "application/json"); + } + + // Add credentials for cookie handling + const response = await fetch(url, { + ...options, + headers, + credentials: "include", + }); + + if (!response.ok) { + let apiError: ApiError; + try { + apiError = (await response.json()) as ApiError; + } catch { + apiError = { + error: `Request failed with status ${response.status}`, + code: "REQUEST_FAILED", + }; + } + throw new RegistryError(apiError); + } + + // Handle empty responses + const text = await response.text(); + if (!text) { + return {} as T; + } + + return JSON.parse(text) as T; } /** Fetch a grant request by ID */ export async function getGrantRequest(grantId: string): Promise { - return request(`/v1/grants/${grantId}`); + return request(`/v1/grants/${grantId}`); } /** Submit approval for a grant */ export async function approveGrant( - grantId: string, - approvedBy: string, - approvalNonce: string, - approvalSignature: string + grantId: string, + approvedBy: string, + approvalNonce: string, + approvalSignature: string, ): Promise { - await request(`/v1/grants/${grantId}/approve`, { - method: 'POST', - body: JSON.stringify({ - approved_by: approvedBy, - approval_nonce: approvalNonce, - approval_signature: approvalSignature, - }), - }); + await request(`/v1/grants/${grantId}/approve`, { + method: "POST", + body: JSON.stringify({ + approved_by: approvedBy, + approval_nonce: approvalNonce, + approval_signature: approvalSignature, + }), + }); } /** Deny a grant request */ -export async function denyGrant(grantId: string, reason?: string): Promise { - await request(`/v1/grants/${grantId}/deny`, { - method: 'POST', - body: JSON.stringify({ reason }), - }); +export async function denyGrant( + grantId: string, + reason?: string, +): Promise { + await request(`/v1/grants/${grantId}/deny`, { + method: "POST", + body: JSON.stringify({ reason }), + }); } /** List all agents for the current human principal */ export async function listAgents(): Promise { - return request('/v1/agents'); + return request("/v1/agents"); } /** Get agent details */ export async function getAgentDetails(agentId: string): Promise { - return request(`/v1/agents/${agentId}`); + return request(`/v1/agents/${agentId}`); } /** Get audit events for an agent */ export async function getAgentActivity( - agentId: string, - limit = 50, - offset = 0 + agentId: string, + limit = 50, + offset = 0, ): Promise { - return request( - `/v1/audit/${agentId}?limit=${limit}&offset=${offset}` - ); + return request( + `/v1/audit/${agentId}?limit=${limit}&offset=${offset}`, + ); } /** Revoke an agent */ export async function revokeAgent(agentId: string): Promise { - await request(`/v1/agents/${agentId}`, { - method: 'DELETE', - }); + await request(`/v1/agents/${agentId}`, { + method: "DELETE", + }); } /** Revoke a specific grant */ export async function revokeGrant(grantId: string): Promise { - await request(`/v1/grants/${grantId}/revoke`, { - method: 'POST', - }); + await request(`/v1/grants/${grantId}/revoke`, { + method: "POST", + }); } /** Check if registry is reachable */ export async function checkHealth(): Promise { - try { - const response = await fetch(`${REGISTRY_URL}/health/ready`, { - method: 'GET', - credentials: 'include', - }); - return response.ok; - } catch { - return false; - } + try { + const response = await fetch(`${REGISTRY_URL}/health/ready`, { + method: "GET", + credentials: "include", + }); + return response.ok; + } catch { + return false; + } } diff --git a/services/approval-ui/src/index.css b/services/approval-ui/src/index.css index 6e8fa84..0bf9967 100644 --- a/services/approval-ui/src/index.css +++ b/services/approval-ui/src/index.css @@ -2,109 +2,151 @@ /* Base */ * { - box-sizing: border-box; - margin: 0; - padding: 0; + box-sizing: border-box; + margin: 0; + padding: 0; } body { - background: #0a0a0f; - color: #e8e8f0; - font-family: 'DM Sans', 'Inter', system-ui, -apple-system, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - line-height: 1.5; + background: #0a0a0f; + color: #e8e8f0; + font-family: + "DM Sans", + "Inter", + system-ui, + -apple-system, + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 1.5; } /* Scanline overlay */ body::after { - content: ''; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 9999; - background: repeating-linear-gradient( - 0deg, - transparent, - transparent 2px, - rgba(0, 0, 0, 0.03) 2px, - rgba(0, 0, 0, 0.03) 4px - ); + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9999; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.03) 2px, + rgba(0, 0, 0, 0.03) 4px + ); } /* Animations */ @keyframes pulse-glow { - 0%, 100% { opacity: 0.4; } - 50% { opacity: 1; } + 0%, + 100% { + opacity: 0.4; + } + 50% { + opacity: 1; + } } @keyframes fade-in { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes slide-up { - from { opacity: 0; transform: translateY(16px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } } .animate-fade-in { - animation: fade-in 0.3s ease-out both; + animation: fade-in 0.3s ease-out both; } .animate-slide-up { - animation: slide-up 0.4s ease-out both; + animation: slide-up 0.4s ease-out both; } /* Stagger children animation */ .stagger-children > * { - animation: fade-in 0.3s ease-out both; -} -.stagger-children > *:nth-child(1) { animation-delay: 0ms; } -.stagger-children > *:nth-child(2) { animation-delay: 50ms; } -.stagger-children > *:nth-child(3) { animation-delay: 100ms; } -.stagger-children > *:nth-child(4) { animation-delay: 150ms; } -.stagger-children > *:nth-child(5) { animation-delay: 200ms; } -.stagger-children > *:nth-child(6) { animation-delay: 250ms; } -.stagger-children > *:nth-child(7) { animation-delay: 300ms; } -.stagger-children > *:nth-child(8) { animation-delay: 350ms; } + animation: fade-in 0.3s ease-out both; +} +.stagger-children > *:nth-child(1) { + animation-delay: 0ms; +} +.stagger-children > *:nth-child(2) { + animation-delay: 50ms; +} +.stagger-children > *:nth-child(3) { + animation-delay: 100ms; +} +.stagger-children > *:nth-child(4) { + animation-delay: 150ms; +} +.stagger-children > *:nth-child(5) { + animation-delay: 200ms; +} +.stagger-children > *:nth-child(6) { + animation-delay: 250ms; +} +.stagger-children > *:nth-child(7) { + animation-delay: 300ms; +} +.stagger-children > *:nth-child(8) { + animation-delay: 350ms; +} /* Skeleton loading */ @keyframes shimmer { - 0% { background-position: -400px 0; } - 100% { background-position: 400px 0; } + 0% { + background-position: -400px 0; + } + 100% { + background-position: 400px 0; + } } .skeleton { - background: linear-gradient(90deg, #111118 25%, #1a1a24 50%, #111118 75%); - background-size: 800px 100%; - animation: shimmer 1.5s infinite linear; - border-radius: 2px; + background: linear-gradient(90deg, #111118 25%, #1a1a24 50%, #111118 75%); + background-size: 800px 100%; + animation: shimmer 1.5s infinite linear; + border-radius: 2px; } /* Custom scrollbar */ ::-webkit-scrollbar { - width: 6px; + width: 6px; } ::-webkit-scrollbar-track { - background: #0a0a0f; + background: #0a0a0f; } ::-webkit-scrollbar-thumb { - background: #2a2a3a; - border-radius: 3px; + background: #2a2a3a; + border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: #3a3a4e; + background: #3a3a4e; } /* Focus ring */ :focus-visible { - outline: 1px solid #f59e0b; - outline-offset: 2px; + outline: 1px solid #f59e0b; + outline-offset: 2px; } /* Selection */ ::selection { - background: rgba(245, 158, 11, 0.3); - color: #e8e8f0; + background: rgba(245, 158, 11, 0.3); + color: #e8e8f0; } diff --git a/services/approval-ui/src/index.html b/services/approval-ui/src/index.html index 44ebc20..aac543e 100644 --- a/services/approval-ui/src/index.html +++ b/services/approval-ui/src/index.html @@ -1,51 +1,66 @@ - + - - - - AgentAuth // Control - - - - - - - - -
- - + + + + AgentAuth // Control + + + + + + + + +
+ + diff --git a/services/approval-ui/src/main.tsx b/services/approval-ui/src/main.tsx index 964aeb4..abaa67c 100644 --- a/services/approval-ui/src/main.tsx +++ b/services/approval-ui/src/main.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' -import './index.css' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/services/approval-ui/src/pages/AgentActivityPage.tsx b/services/approval-ui/src/pages/AgentActivityPage.tsx index 8935dc6..af6d18e 100644 --- a/services/approval-ui/src/pages/AgentActivityPage.tsx +++ b/services/approval-ui/src/pages/AgentActivityPage.tsx @@ -1,422 +1,557 @@ -import { useState, useEffect } from 'react'; -import { useParams, Link, useRouter } from '../Router'; +import { useState, useEffect } from "react"; +import { useParams, Link, useRouter } from "../Router"; import { - getAgentDetails, - getAgentActivity, - revokeAgent, - revokeGrant, - checkHealth, -} from '../api'; -import { capabilityToHumanReadable } from '../utils/capabilities'; -import type { AgentDetails, AuditEvent, GrantSummary } from '../types'; + getAgentDetails, + getAgentActivity, + revokeAgent, + revokeGrant, + checkHealth, +} from "../api"; +import { capabilityToHumanReadable } from "../utils/capabilities"; +import type { AgentDetails, AuditEvent, GrantSummary } from "../types"; type PageState = - | { type: 'loading' } - | { type: 'error'; message: string; isOffline: boolean } - | { type: 'loaded'; agent: AgentDetails; events: AuditEvent[] }; + | { type: "loading" } + | { type: "error"; message: string; isOffline: boolean } + | { type: "loaded"; agent: AgentDetails; events: AuditEvent[] }; export function AgentActivityPage() { - const { agent_id } = useParams<{ agent_id: string }>(); - const { navigate } = useRouter(); - const [state, setState] = useState({ type: 'loading' }); - const [showRevokeConfirm, setShowRevokeConfirm] = useState(false); - const [revokeGrantId, setRevokeGrantId] = useState(null); + const { agent_id } = useParams<{ agent_id: string }>(); + const { navigate } = useRouter(); + const [state, setState] = useState({ type: "loading" }); + const [showRevokeConfirm, setShowRevokeConfirm] = useState(false); + const [revokeGrantId, setRevokeGrantId] = useState(null); - useEffect(() => { - loadAgent(); - }, [agent_id]); + useEffect(() => { + loadAgent(); + }, [agent_id]); - async function loadAgent() { - setState({ type: 'loading' }); - const isHealthy = await checkHealth(); - if (!isHealthy) { - setState({ - type: 'error', - message: 'Unable to establish connection with AgentAuth registry.', - isOffline: true, - }); - return; - } - try { - const [agent, events] = await Promise.all([ - getAgentDetails(agent_id), - getAgentActivity(agent_id), - ]); - setState({ type: 'loaded', agent, events }); - } catch (err) { - setState({ - type: 'error', - message: err instanceof Error ? err.message : 'Failed to load agent details', - isOffline: false, - }); - } - } + async function loadAgent() { + setState({ type: "loading" }); + const isHealthy = await checkHealth(); + if (!isHealthy) { + setState({ + type: "error", + message: + "Unable to establish connection with AgentAuth registry.", + isOffline: true, + }); + return; + } + try { + const [agent, events] = await Promise.all([ + getAgentDetails(agent_id), + getAgentActivity(agent_id), + ]); + setState({ type: "loaded", agent, events }); + } catch (err) { + setState({ + type: "error", + message: + err instanceof Error + ? err.message + : "Failed to load agent details", + isOffline: false, + }); + } + } - async function handleRevokeAgent() { - try { - await revokeAgent(agent_id); - navigate('/agents'); - } catch (err) { - setState({ - type: 'error', - message: err instanceof Error ? err.message : 'Failed to revoke agent', - isOffline: false, - }); - } - } + async function handleRevokeAgent() { + try { + await revokeAgent(agent_id); + navigate("/agents"); + } catch (err) { + setState({ + type: "error", + message: + err instanceof Error + ? err.message + : "Failed to revoke agent", + isOffline: false, + }); + } + } - async function handleRevokeGrant(grantId: string) { - try { - await revokeGrant(grantId); - setRevokeGrantId(null); - loadAgent(); - } catch (err) { - setState({ - type: 'error', - message: err instanceof Error ? err.message : 'Failed to revoke grant', - isOffline: false, - }); - } - } + async function handleRevokeGrant(grantId: string) { + try { + await revokeGrant(grantId); + setRevokeGrantId(null); + loadAgent(); + } catch (err) { + setState({ + type: "error", + message: + err instanceof Error + ? err.message + : "Failed to revoke grant", + isOffline: false, + }); + } + } - const statusConfig = { - active: { color: 'bg-green', text: 'text-green', label: 'ACTIVE' }, - suspended: { color: 'bg-amber', text: 'text-amber', label: 'SUSPENDED' }, - revoked: { color: 'bg-red', text: 'text-red', label: 'REVOKED' }, - }; + const statusConfig = { + active: { color: "bg-green", text: "text-green", label: "ACTIVE" }, + suspended: { + color: "bg-amber", + text: "text-amber", + label: "SUSPENDED", + }, + revoked: { color: "bg-red", text: "text-red", label: "REVOKED" }, + }; - return ( -
- {/* Top bar */} -
-
- -
-
-
- AGENTAUTH - -
- AGENTS - / - DETAIL -
-
-
+ return ( +
+ {/* Top bar */} +
+
+ +
+
+
+ + AGENTAUTH + + +
+ + AGENTS + + / + DETAIL +
+
+
-
- {/* Loading */} - {state.type === 'loading' && ( -
-
-
-
-
-
-
-
-
- )} +
+ {/* Loading */} + {state.type === "loading" && ( +
+
+
+
+
+
+
+
+
+ )} - {/* Error */} - {state.type === 'error' && ( -
-
-
-
-
-

- {state.isOffline ? 'CONNECTION LOST' : 'ERROR'} -

-

{state.message}

-
-
-
- - - BACK - -
-
-
- )} + {/* Error */} + {state.type === "error" && ( +
+
+
+
+
+

+ {state.isOffline + ? "CONNECTION LOST" + : "ERROR"} +

+

+ {state.message} +

+
+
+
+ + + BACK + +
+
+
+ )} - {/* Loaded */} - {state.type === 'loaded' && (() => { - const { agent, events } = state; - const status = statusConfig[agent.status]; + {/* Loaded */} + {state.type === "loaded" && + (() => { + const { agent, events } = state; + const status = statusConfig[agent.status]; - return ( -
- {/* Back link */} - - - - - AGENTS - + return ( +
+ {/* Back link */} + + + + + AGENTS + - {/* Header */} -
-
-
-

{agent.name}

- - {status.label} - -
-

{agent.agent_id}

-
+ {/* Header */} +
+
+
+

+ {agent.name} +

+ + {status.label} + +
+

+ {agent.agent_id} +

+
- {/* Details grid */} -
-
-
REGISTERED
-
{new Date(agent.registered_at).toLocaleDateString()}
-
-
-
ACTIVE GRANTS
-
{agent.grants.filter(g => g.status === 'active').length}
-
-
-
PUBLIC KEY
-
{agent.public_key.slice(0, 24)}...
-
-
+ {/* Details grid */} +
+
+
+ REGISTERED +
+
+ {new Date( + agent.registered_at, + ).toLocaleDateString()} +
+
+
+
+ ACTIVE GRANTS +
+
+ { + agent.grants.filter( + (g) => + g.status === "active", + ).length + } +
+
+
+
+ PUBLIC KEY +
+
+ {agent.public_key.slice(0, 24)}... +
+
+
- {/* Grants */} -
- GRANTS ({agent.grants.length}) - {agent.grants.length === 0 ? ( -
-

NO ACTIVE GRANTS

-
- ) : ( -
- {agent.grants.map((grant) => ( - setRevokeGrantId(grant.grant_id)} - /> - ))} -
- )} -
+ {/* Grants */} +
+ + GRANTS ({agent.grants.length}) + + {agent.grants.length === 0 ? ( +
+

+ NO ACTIVE GRANTS +

+
+ ) : ( +
+ {agent.grants.map((grant) => ( + + setRevokeGrantId( + grant.grant_id, + ) + } + /> + ))} +
+ )} +
- {/* Activity */} -
- RECENT ACTIVITY ({events.length}) - {events.length === 0 ? ( -
-

NO RECENT ACTIVITY

-
- ) : ( -
- {events.map((event) => ( - - ))} -
- )} -
+ {/* Activity */} +
+ + RECENT ACTIVITY ({events.length}) + + {events.length === 0 ? ( +
+

+ NO RECENT ACTIVITY +

+
+ ) : ( +
+ {events.map((event) => ( + + ))} +
+ )} +
- {/* Danger zone */} - {agent.status === 'active' && ( -
-
-
- DANGER ZONE -
-

- Revoking this agent will immediately terminate all access and invalidate all active tokens. -

- -
- )} -
- ); - })()} + {/* Danger zone */} + {agent.status === "active" && ( +
+
+
+ + DANGER ZONE + +
+

+ Revoking this agent will immediately + terminate all access and invalidate + all active tokens. +

+ +
+ )} +
+ ); + })()} - {/* Revoke agent dialog */} - {showRevokeConfirm && state.type === 'loaded' && ( - setShowRevokeConfirm(false)} - /> - )} + {/* Revoke agent dialog */} + {showRevokeConfirm && state.type === "loaded" && ( + setShowRevokeConfirm(false)} + /> + )} - {/* Revoke grant dialog */} - {revokeGrantId && ( - handleRevokeGrant(revokeGrantId)} - onCancel={() => setRevokeGrantId(null)} - /> - )} -
-
- ); + {/* Revoke grant dialog */} + {revokeGrantId && ( + handleRevokeGrant(revokeGrantId)} + onCancel={() => setRevokeGrantId(null)} + /> + )} +
+
+ ); } function SectionLabel({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
-
- ); + return ( +
+ {children} +
+
+ ); } -function GrantRow({ grant, onRevoke }: { grant: GrantSummary; onRevoke: () => void }) { - const grantStatus: Record = { - active: { color: 'bg-green', text: 'text-green', label: 'ACTIVE' }, - approved: { color: 'bg-green', text: 'text-green', label: 'APPROVED' }, - pending: { color: 'bg-amber', text: 'text-amber', label: 'PENDING' }, - denied: { color: 'bg-red', text: 'text-red', label: 'DENIED' }, - revoked: { color: 'bg-red', text: 'text-red', label: 'REVOKED' }, - expired: { color: 'bg-text-muted', text: 'text-text-muted', label: 'EXPIRED' }, - }; +function GrantRow({ + grant, + onRevoke, +}: { + grant: GrantSummary; + onRevoke: () => void; +}) { + const grantStatus: Record< + string, + { color: string; text: string; label: string } + > = { + active: { color: "bg-green", text: "text-green", label: "ACTIVE" }, + approved: { color: "bg-green", text: "text-green", label: "APPROVED" }, + pending: { color: "bg-amber", text: "text-amber", label: "PENDING" }, + denied: { color: "bg-red", text: "text-red", label: "DENIED" }, + revoked: { color: "bg-red", text: "text-red", label: "REVOKED" }, + expired: { + color: "bg-text-muted", + text: "text-text-muted", + label: "EXPIRED", + }, + }; - const fallback = { color: 'bg-text-muted', text: 'text-text-muted', label: grant.status.toUpperCase() }; - const status = grantStatus[grant.status] ?? fallback; + const fallback = { + color: "bg-text-muted", + text: "text-text-muted", + label: grant.status.toUpperCase(), + }; + const status = grantStatus[grant.status] ?? fallback; - return ( -
-
-
-
-
- {grant.service_provider_name} - {status.label} -
-
- {grant.capabilities.map((cap, idx) => ( -
{capabilityToHumanReadable(cap)}
- ))} -
-
- {new Date(grant.created_at).toLocaleDateString()} -
-
-
- {grant.status === 'pending' && ( - - APPROVE - - )} - {(grant.status === 'active' || grant.status === 'approved') && ( - - )} -
-
-
- ); + return ( +
+
+
+
+
+ + {grant.service_provider_name} + + + {status.label} + +
+
+ {grant.capabilities.map((cap, idx) => ( +
+ {capabilityToHumanReadable(cap)} +
+ ))} +
+
+ {new Date(grant.created_at).toLocaleDateString()} +
+
+
+ {grant.status === "pending" && ( + + APPROVE + + )} + {(grant.status === "active" || + grant.status === "approved") && ( + + )} +
+
+
+ ); } function ActivityRow({ event }: { event: AuditEvent }) { - const eventLabels: Record = { - token_issued: 'Token Issued', - token_verified: 'Token Verified', - token_denied: 'Token Denied', - grant_approved: 'Grant Approved', - grant_denied: 'Grant Denied', - agent_registered: 'Agent Registered', - agent_revoked: 'Agent Revoked', - }; + const eventLabels: Record = { + token_issued: "Token Issued", + token_verified: "Token Verified", + token_denied: "Token Denied", + grant_approved: "Grant Approved", + grant_denied: "Grant Denied", + agent_registered: "Agent Registered", + agent_revoked: "Agent Revoked", + }; - const outcomeConfig: Record = { - allowed: { color: 'text-green', label: 'OK' }, - denied: { color: 'text-red', label: 'DENIED' }, - rate_limited: { color: 'text-amber', label: 'THROTTLED' }, - }; + const outcomeConfig: Record = { + allowed: { color: "text-green", label: "OK" }, + denied: { color: "text-red", label: "DENIED" }, + rate_limited: { color: "text-amber", label: "THROTTLED" }, + }; - const outcome = outcomeConfig[event.outcome] || { color: 'text-text-muted', label: event.outcome }; + const outcome = outcomeConfig[event.outcome] || { + color: "text-text-muted", + label: event.outcome, + }; - return ( -
-
- {new Date(event.created_at).toLocaleString()} -
-
- {eventLabels[event.event_type] || event.event_type} - {event.capability && ( - - {capabilityToHumanReadable(event.capability)} - - )} -
- - {outcome.label} - -
- ); + return ( +
+
+ {new Date(event.created_at).toLocaleString()} +
+
+ + {eventLabels[event.event_type] || event.event_type} + + {event.capability && ( + + {capabilityToHumanReadable(event.capability)} + + )} +
+ + {outcome.label} + +
+ ); } function ConfirmDialog({ - title, - message, - confirmLabel, - onConfirm, - onCancel, + title, + message, + confirmLabel, + onConfirm, + onCancel, }: { - title: string; - message: string; - confirmLabel: string; - onConfirm: () => void; - onCancel: () => void; + title: string; + message: string; + confirmLabel: string; + onConfirm: () => void; + onCancel: () => void; }) { - return ( -
-
-
-
-

{title}

-
-

{message}

-
- - -
-
-
- ); + return ( +
+
+
+
+

+ {title} +

+
+

+ {message} +

+
+ + +
+
+
+ ); } diff --git a/services/approval-ui/src/pages/AgentsPage.tsx b/services/approval-ui/src/pages/AgentsPage.tsx index 827c470..662bb33 100644 --- a/services/approval-ui/src/pages/AgentsPage.tsx +++ b/services/approval-ui/src/pages/AgentsPage.tsx @@ -1,183 +1,235 @@ -import { useState, useEffect } from 'react'; -import { Link, useRouter } from '../Router'; -import { listAgents, checkHealth } from '../api'; -import type { AgentSummary } from '../types'; +import { useState, useEffect } from "react"; +import { Link, useRouter } from "../Router"; +import { listAgents, checkHealth } from "../api"; +import type { AgentSummary } from "../types"; type PageState = - | { type: 'loading' } - | { type: 'error'; message: string; isOffline: boolean } - | { type: 'loaded'; agents: AgentSummary[] }; + | { type: "loading" } + | { type: "error"; message: string; isOffline: boolean } + | { type: "loaded"; agents: AgentSummary[] }; export function AgentsPage() { - const [state, setState] = useState({ type: 'loading' }); + const [state, setState] = useState({ type: "loading" }); - useEffect(() => { - loadAgents(); - }, []); + useEffect(() => { + loadAgents(); + }, []); - async function loadAgents() { - setState({ type: 'loading' }); - const isHealthy = await checkHealth(); - if (!isHealthy) { - setState({ - type: 'error', - message: 'Unable to establish connection with AgentAuth registry.', - isOffline: true, - }); - return; - } - try { - const agents = await listAgents(); - setState({ type: 'loaded', agents }); - } catch (err) { - setState({ - type: 'error', - message: err instanceof Error ? err.message : 'Failed to load agents', - isOffline: false, - }); - } - } + async function loadAgents() { + setState({ type: "loading" }); + const isHealthy = await checkHealth(); + if (!isHealthy) { + setState({ + type: "error", + message: + "Unable to establish connection with AgentAuth registry.", + isOffline: true, + }); + return; + } + try { + const agents = await listAgents(); + setState({ type: "loaded", agents }); + } catch (err) { + setState({ + type: "error", + message: + err instanceof Error + ? err.message + : "Failed to load agents", + isOffline: false, + }); + } + } - return ( -
- {/* Top bar */} -
-
- -
-
-
- AGENTAUTH - - AGENTS -
-
+ return ( +
+ {/* Top bar */} +
+
+ +
+
+
+ + AGENTAUTH + + + + AGENTS + +
+
-
- {state.type === 'loading' && ( -
-
-
-
- {[1, 2, 3].map((i) => ( -
- ))} -
-
- )} +
+ {state.type === "loading" && ( +
+
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+ )} - {state.type === 'error' && ( -
-
-
-
-
-

- {state.isOffline ? 'CONNECTION LOST' : 'ERROR'} -

-

{state.message}

-
-
- -
-
- )} + {state.type === "error" && ( +
+
+
+
+
+

+ {state.isOffline + ? "CONNECTION LOST" + : "ERROR"} +

+

+ {state.message} +

+
+
+ +
+
+ )} - {state.type === 'loaded' && ( -
- {/* Header */} -
-
-
-

AGENTS

-
-

- {state.agents.length} registered agent{state.agents.length !== 1 ? 's' : ''} -

-
+ {state.type === "loaded" && ( +
+ {/* Header */} +
+
+
+

+ AGENTS +

+
+

+ {state.agents.length} registered agent + {state.agents.length !== 1 ? "s" : ""} +

+
- {state.agents.length === 0 ? ( -
-
-

NO AGENTS REGISTERED

-

No agents have been authorized to act on your behalf.

-
- ) : ( -
- {state.agents.map((agent) => ( - - ))} -
- )} -
- )} -
-
- ); + {state.agents.length === 0 ? ( +
+
+

+ NO AGENTS REGISTERED +

+

+ No agents have been authorized to act on + your behalf. +

+
+ ) : ( +
+ {state.agents.map((agent) => ( + + ))} +
+ )} +
+ )} +
+
+ ); } function AgentRow({ agent }: { agent: AgentSummary }) { - const { navigate } = useRouter(); - const statusConfig = { - active: { color: 'bg-green', text: 'text-green', label: 'ACTIVE' }, - suspended: { color: 'bg-amber', text: 'text-amber', label: 'SUSPENDED' }, - revoked: { color: 'bg-red', text: 'text-red', label: 'REVOKED' }, - }; + const { navigate } = useRouter(); + const statusConfig = { + active: { color: "bg-green", text: "text-green", label: "ACTIVE" }, + suspended: { + color: "bg-amber", + text: "text-amber", + label: "SUSPENDED", + }, + revoked: { color: "bg-red", text: "text-red", label: "REVOKED" }, + }; - const status = statusConfig[agent.status]; + const status = statusConfig[agent.status]; - return ( -
-
- {/* Status indicator */} -
+ return ( +
navigate(`/agents/${agent.agent_id}/activity`)} + > +
+ {/* Status indicator */} +
- {/* Agent info — clickable area navigates to detail */} - + {/* Agent info — clickable area navigates to detail */} + - {/* Grants count */} -
-
{agent.active_grants}
-
GRANTS
-
+ {/* Grants count */} +
+
+ {agent.active_grants} +
+
+ GRANTS +
+
- {/* Approve button (pending) or arrow (normal) */} - {agent.pending_grant_id ? ( - - APPROVE - - ) : ( - - - - )} -
-
- ); + {/* Approve button (pending) or arrow (normal) */} + {agent.pending_grant_id ? ( + + APPROVE + + ) : ( + + + + )} +
+
+ ); } diff --git a/services/approval-ui/src/pages/ApprovalPage.tsx b/services/approval-ui/src/pages/ApprovalPage.tsx index b5f0881..6299592 100644 --- a/services/approval-ui/src/pages/ApprovalPage.tsx +++ b/services/approval-ui/src/pages/ApprovalPage.tsx @@ -1,452 +1,576 @@ -import { useState, useEffect } from 'react'; -import { useParams, useRouter, Link } from '../Router'; -import { getGrantRequest, approveGrant, denyGrant, checkHealth } from '../api'; +import { useState, useEffect } from "react"; +import { useParams, useRouter, Link } from "../Router"; +import { getGrantRequest, approveGrant, denyGrant, checkHealth } from "../api"; import { - capabilityToHumanReadable, - envelopeToHumanReadable, - requiresTwoStep, - getCapabilityRiskLevel, - getCapabilitySummary, -} from '../utils/capabilities'; -import type { GrantRequest, Capability } from '../types'; + capabilityToHumanReadable, + envelopeToHumanReadable, + requiresTwoStep, + getCapabilityRiskLevel, + getCapabilitySummary, +} from "../utils/capabilities"; +import type { GrantRequest, Capability } from "../types"; type PageState = - | { type: 'loading' } - | { type: 'error'; message: string; isOffline: boolean } - | { type: 'loaded'; grant: GrantRequest } - | { type: 'confirming'; grant: GrantRequest; step: 1 | 2 } - | { type: 'signing'; grant: GrantRequest } - | { type: 'success'; action: 'approved' | 'denied' } - | { type: 'expired' }; + | { type: "loading" } + | { type: "error"; message: string; isOffline: boolean } + | { type: "loaded"; grant: GrantRequest } + | { type: "confirming"; grant: GrantRequest; step: 1 | 2 } + | { type: "signing"; grant: GrantRequest } + | { type: "success"; action: "approved" | "denied" } + | { type: "expired" }; export function ApprovalPage() { - const { grant_id } = useParams<{ grant_id: string }>(); - const { navigate } = useRouter(); - const [state, setState] = useState({ type: 'loading' }); - const [denyReason, setDenyReason] = useState(''); + const { grant_id } = useParams<{ grant_id: string }>(); + const { navigate } = useRouter(); + const [state, setState] = useState({ type: "loading" }); + const [denyReason, setDenyReason] = useState(""); - useEffect(() => { - loadGrant(); - }, [grant_id]); + useEffect(() => { + loadGrant(); + }, [grant_id]); - async function loadGrant() { - setState({ type: 'loading' }); - const isHealthy = await checkHealth(); - if (!isHealthy) { - setState({ - type: 'error', - message: 'Unable to establish connection with AgentAuth registry. Service may be offline.', - isOffline: true, - }); - return; - } - try { - const grant = await getGrantRequest(grant_id); - if (grant.status === 'expired') { - setState({ type: 'expired' }); - return; - } - if (grant.status !== 'pending') { - setState({ - type: 'error', - message: `This grant request has already been ${grant.status}.`, - isOffline: false, - }); - return; - } - setState({ type: 'loaded', grant }); - } catch (err) { - setState({ - type: 'error', - message: err instanceof Error ? err.message : 'Failed to load grant request', - isOffline: false, - }); - } - } + async function loadGrant() { + setState({ type: "loading" }); + const isHealthy = await checkHealth(); + if (!isHealthy) { + setState({ + type: "error", + message: + "Unable to establish connection with AgentAuth registry. Service may be offline.", + isOffline: true, + }); + return; + } + try { + const grant = await getGrantRequest(grant_id); + if (grant.status === "expired") { + setState({ type: "expired" }); + return; + } + if (grant.status !== "pending") { + setState({ + type: "error", + message: `This grant request has already been ${grant.status}.`, + isOffline: false, + }); + return; + } + setState({ type: "loaded", grant }); + } catch (err) { + setState({ + type: "error", + message: + err instanceof Error + ? err.message + : "Failed to load grant request", + isOffline: false, + }); + } + } - function handleApproveClick(grant: GrantRequest) { - const summary = getCapabilitySummary(grant.requested_capabilities); - if (summary.hasHighRisk) { - setState({ type: 'confirming', grant, step: 1 }); - } else { - startSigning(grant); - } - } + function handleApproveClick(grant: GrantRequest) { + const summary = getCapabilitySummary(grant.requested_capabilities); + if (summary.hasHighRisk) { + setState({ type: "confirming", grant, step: 1 }); + } else { + startSigning(grant); + } + } - function handleConfirmStep1(grant: GrantRequest) { - setState({ type: 'confirming', grant, step: 2 }); - } + function handleConfirmStep1(grant: GrantRequest) { + setState({ type: "confirming", grant, step: 2 }); + } - async function startSigning(grant: GrantRequest) { - setState({ type: 'signing', grant }); - try { - // Generate a random nonce (32 bytes as hex) - const nonceBytes = new Uint8Array(32); - crypto.getRandomValues(nonceBytes); - const nonce = Array.from(nonceBytes, (b) => b.toString(16).padStart(2, '0')).join(''); + async function startSigning(grant: GrantRequest) { + setState({ type: "signing", grant }); + try { + // Generate a random nonce (32 bytes as hex) + const nonceBytes = new Uint8Array(32); + crypto.getRandomValues(nonceBytes); + const nonce = Array.from(nonceBytes, (b) => + b.toString(16).padStart(2, "0"), + ).join(""); - // Generate a dummy signature (demo mode — WebAuthn requires HTTPS) - const sigBytes = new Uint8Array(64); - crypto.getRandomValues(sigBytes); - const signature = Array.from(sigBytes, (b) => b.toString(16).padStart(2, '0')).join(''); + // Generate a dummy signature (demo mode — WebAuthn requires HTTPS) + const sigBytes = new Uint8Array(64); + crypto.getRandomValues(sigBytes); + const signature = Array.from(sigBytes, (b) => + b.toString(16).padStart(2, "0"), + ).join(""); - await approveGrant(grant.grant_id, grant.human_principal_id, nonce, signature); - setState({ type: 'success', action: 'approved' }); - } catch (err) { - setState({ - type: 'error', - message: err instanceof Error ? err.message : 'Approval failed', - isOffline: false, - }); - } - } + await approveGrant( + grant.grant_id, + grant.human_principal_id, + nonce, + signature, + ); + setState({ type: "success", action: "approved" }); + } catch (err) { + setState({ + type: "error", + message: err instanceof Error ? err.message : "Approval failed", + isOffline: false, + }); + } + } - async function handleDeny(grant: GrantRequest) { - try { - await denyGrant(grant.grant_id, denyReason || undefined); - setState({ type: 'success', action: 'denied' }); - } catch (err) { - setState({ - type: 'error', - message: err instanceof Error ? err.message : 'Failed to deny request', - isOffline: false, - }); - } - } + async function handleDeny(grant: GrantRequest) { + try { + await denyGrant(grant.grant_id, denyReason || undefined); + setState({ type: "success", action: "denied" }); + } catch (err) { + setState({ + type: "error", + message: + err instanceof Error + ? err.message + : "Failed to deny request", + isOffline: false, + }); + } + } - // --- Loading --- - if (state.type === 'loading') { - return ( - -
-
-
-
-
-
-
-
-
- - ); - } + // --- Loading --- + if (state.type === "loading") { + return ( + +
+
+
+
+
+
+
+
+
+ + ); + } - // --- Error --- - if (state.type === 'error') { - return ( - -
-
-
-
-
-

- {state.isOffline ? 'CONNECTION LOST' : 'ERROR'} -

-

{state.message}

-
-
- -
-
- - ); - } + // --- Error --- + if (state.type === "error") { + return ( + +
+
+
+
+
+

+ {state.isOffline + ? "CONNECTION LOST" + : "ERROR"} +

+

+ {state.message} +

+
+
+ +
+
+ + ); + } - // --- Expired --- - if (state.type === 'expired') { - return ( - -
-
-
-

REQUEST EXPIRED

-

This grant request has expired and can no longer be processed.

- -
-
- - ); - } + // --- Expired --- + if (state.type === "expired") { + return ( + +
+
+
+

+ REQUEST EXPIRED +

+

+ This grant request has expired and can no longer be + processed. +

+ +
+
+ + ); + } - // --- Success --- - if (state.type === 'success') { - const approved = state.action === 'approved'; - return ( - -
-
-
-

- {approved ? 'GRANT AUTHORIZED' : 'REQUEST DENIED'} -

-

- {approved - ? 'Agent access has been granted. Token issuance is now active.' - : 'The request has been denied. The agent will be notified.'} -

- -
-
- - ); - } + // --- Success --- + if (state.type === "success") { + const approved = state.action === "approved"; + return ( + +
+
+
+

+ {approved ? "GRANT AUTHORIZED" : "REQUEST DENIED"} +

+

+ {approved + ? "Agent access has been granted. Token issuance is now active." + : "The request has been denied. The agent will be notified."} +

+ +
+
+ + ); + } - // --- Signing --- - if (state.type === 'signing') { - return ( - -
-
-
-
-
-
-
-

PROCESSING

-

Submitting approval to registry...

-
-
- - ); - } + // --- Signing --- + if (state.type === "signing") { + return ( + +
+
+
+
+
+
+
+

+ PROCESSING +

+

+ Submitting approval to registry... +

+
+
+ + ); + } - // --- Loaded / Confirming --- - const grant = state.grant; - const isConfirming = state.type === 'confirming'; - const confirmStep = isConfirming ? state.step : 0; - const summary = getCapabilitySummary(grant.requested_capabilities); + // --- Loaded / Confirming --- + const grant = state.grant; + const isConfirming = state.type === "confirming"; + const confirmStep = isConfirming ? state.step : 0; + const summary = getCapabilitySummary(grant.requested_capabilities); - return ( - -
- {/* Header */} -
-
-
-

- GRANT REQUEST -

-
-

{grant.grant_id}

-
+ return ( + +
+ {/* Header */} +
+
+
+

+ GRANT REQUEST +

+
+

+ {grant.grant_id} +

+
- {/* Agent + Service grid */} -
- - -
+ {/* Agent + Service grid */} +
+ + +
- {/* Capabilities */} -
- REQUESTED PERMISSIONS ({grant.requested_capabilities.length}) -
- {grant.requested_capabilities.map((cap, idx) => ( - - ))} -
-
+ {/* Capabilities */} +
+ + REQUESTED PERMISSIONS ( + {grant.requested_capabilities.length}) + +
+ {grant.requested_capabilities.map((cap, idx) => ( + + ))} +
+
- {/* Behavioral constraints */} -
- BEHAVIORAL CONSTRAINTS -
- {envelopeToHumanReadable(grant.requested_envelope).map((desc, idx) => ( -
- {'>'} - {desc} -
- ))} -
-
+ {/* Behavioral constraints */} +
+ BEHAVIORAL CONSTRAINTS +
+ {envelopeToHumanReadable(grant.requested_envelope).map( + (desc, idx) => ( +
+ + {">"} + + + {desc} + +
+ ), + )} +
+
- {/* Expiry */} -
-
- - EXPIRES {new Date(grant.expires_at).toLocaleString().toUpperCase()} - -
+ {/* Expiry */} +
+
+ + EXPIRES{" "} + {new Date(grant.expires_at) + .toLocaleString() + .toUpperCase()} + +
- {/* Actions */} - {!isConfirming && ( -
-
- setDenyReason(e.target.value)} - className="bg-panel border border-border px-3 py-2 text-sm text-text-primary placeholder:text-text-muted font-mono focus:outline-none focus:border-amber w-56" - /> - -
- -
- )} -
+ {/* Actions */} + {!isConfirming && ( +
+
+ setDenyReason(e.target.value)} + className="bg-panel border border-border px-3 py-2 text-sm text-text-primary placeholder:text-text-muted font-mono focus:outline-none focus:border-amber w-56" + /> + +
+ +
+ )} +
- {/* Confirmation overlay */} - {isConfirming && ( -
-
- {confirmStep === 1 ? ( - <> -
-
-

HIGH-RISK PERMISSIONS

-
-

- This request includes permissions that could modify or delete your data, or make financial transactions. -

-

Proceed with caution.

-
- - -
- - ) : ( - <> -
-
-

FINAL CONFIRMATION

-
-

You are granting access to:

-
- {grant.requested_capabilities - .filter(requiresTwoStep) - .map((cap, idx) => ( -
- {capabilityToHumanReadable(cap)} -
- ))} -
-

- This cannot be undone without revoking the entire grant. -

-
- - -
- - )} -
-
- )} - - ); + {/* Confirmation overlay */} + {isConfirming && ( +
+
+ {confirmStep === 1 ? ( + <> +
+
+

+ HIGH-RISK PERMISSIONS +

+
+

+ This request includes permissions that could + modify or delete your data, or make + financial transactions. +

+

+ Proceed with caution. +

+
+ + +
+ + ) : ( + <> +
+
+

+ FINAL CONFIRMATION +

+
+

+ You are granting access to: +

+
+ {grant.requested_capabilities + .filter(requiresTwoStep) + .map((cap, idx) => ( +
+ {capabilityToHumanReadable(cap)} +
+ ))} +
+

+ This cannot be undone without revoking the + entire grant. +

+
+ + +
+ + )} +
+
+ )} + + ); } function Shell({ children }: { children: React.ReactNode }) { - return ( -
- {/* Top bar */} -
-
- -
-
-
- AGENTAUTH - - - AGENTS - -
-
- {/* Content */} -
- {children} -
-
- ); + return ( +
+ {/* Top bar */} +
+
+ +
+
+
+ + AGENTAUTH + + + + AGENTS + +
+
+ {/* Content */} +
+ {children} +
+
+ ); } -function InfoBlock({ label, value, sub }: { label: string; value: string; sub: string }) { - return ( -
-
{label}
-
{value}
-
{sub}
-
- ); +function InfoBlock({ + label, + value, + sub, +}: { + label: string; + value: string; + sub: string; +}) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ {sub} +
+
+ ); } function SectionLabel({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
-
- ); + return ( +
+ {children} +
+
+ ); } function CapabilityRow({ capability }: { capability: Capability }) { - const risk = getCapabilityRiskLevel(capability); - const needsTwoStep = requiresTwoStep(capability); + const risk = getCapabilityRiskLevel(capability); + const needsTwoStep = requiresTwoStep(capability); - const riskColors = { - low: 'bg-green', - medium: 'bg-amber', - high: 'bg-red', - }; + const riskColors = { + low: "bg-green", + medium: "bg-amber", + high: "bg-red", + }; - return ( -
-
- - {capabilityToHumanReadable(capability)} - - {needsTwoStep && ( - - 2-STEP - - )} -
- ); + return ( +
+
+ + {capabilityToHumanReadable(capability)} + + {needsTwoStep && ( + + 2-STEP + + )} +
+ ); } diff --git a/services/approval-ui/src/pages/DashboardPage.tsx b/services/approval-ui/src/pages/DashboardPage.tsx index 61b7f23..1eb0642 100644 --- a/services/approval-ui/src/pages/DashboardPage.tsx +++ b/services/approval-ui/src/pages/DashboardPage.tsx @@ -1,108 +1,134 @@ -import { useState } from 'react'; -import { Link } from '../Router'; +import { useState, useEffect } from "react"; +import { Link } from "../Router"; -type DashboardTab = 'token-verification-slo' | 'circuit-breakers'; +type DashboardTab = "token-verification-slo" | "circuit-breakers"; -const GRAFANA_BASE_URL = 'http://localhost:3000'; +const GRAFANA_BASE_URL = "http://localhost:3000"; const DASHBOARDS: Record = { - 'token-verification-slo': { - label: 'TOKEN VERIFICATION SLO', - uid: 'agentauth-verify-slo', - }, - 'circuit-breakers': { - label: 'CIRCUIT BREAKERS', - uid: 'agentauth-circuit-breakers', - }, + "token-verification-slo": { + label: "TOKEN VERIFICATION SLO", + uid: "agentauth-verify-slo", + }, + "circuit-breakers": { + label: "CIRCUIT BREAKERS", + uid: "agentauth-circuit-breakers", + }, }; function getDashboardUrl(uid: string): string { - return `${GRAFANA_BASE_URL}/d/${uid}?orgId=1&kiosk`; + return `${GRAFANA_BASE_URL}/d/${uid}?orgId=1&kiosk`; } export function DashboardPage() { - const [activeTab, setActiveTab] = useState('token-verification-slo'); - const [iframeError, setIframeError] = useState(false); + const [activeTab, setActiveTab] = useState( + "token-verification-slo", + ); + const [iframeError, setIframeError] = useState(false); - const activeDashboard = DASHBOARDS[activeTab]; - const iframeSrc = getDashboardUrl(activeDashboard.uid); + // iframe onError doesn't fire for ERR_CONNECTION_REFUSED — probe Grafana on mount. + useEffect(() => { + const controller = new AbortController(); + fetch(GRAFANA_BASE_URL, { + mode: "no-cors", + signal: controller.signal, + }).catch(() => setIframeError(true)); + return () => controller.abort(); + }, []); - function handleTabChange(tab: DashboardTab) { - setActiveTab(tab); - setIframeError(false); - } + const activeDashboard = DASHBOARDS[activeTab]; + const iframeSrc = getDashboardUrl(activeDashboard.uid); - return ( -
- {/* Top bar */} -
-
- -
-
-
- AGENTAUTH - - DASHBOARD -
-
+ function handleTabChange(tab: DashboardTab) { + setActiveTab(tab); + setIframeError(false); + } - {/* Tab bar */} -
-
- {(Object.entries(DASHBOARDS) as [DashboardTab, { label: string; uid: string }][]).map( - ([key, dashboard]) => ( - - ), - )} -
-
+ return ( +
+ {/* Top bar */} +
+
+ +
+
+
+ + AGENTAUTH + + + + DASHBOARD + +
+
- {/* Dashboard iframe */} -
- {iframeError ? ( -
-
-
-
-
-

- GRAFANA UNREACHABLE -

-

- Unable to load the Grafana dashboard. Verify that Grafana is running at{' '} - {GRAFANA_BASE_URL}. -

-
-
- -
-
- ) : ( -