feat(phase-2): workstream 6 — Web Dashboard UI
- dashboard/: Vite 5 + React 18 + TypeScript strict SPA
- Auth: sessionStorage credentials, TokenManager validation, AuthProvider context
- Pages: Login, Agents (search + filter), AgentDetail (suspend/reactivate),
Credentials (generate/rotate/revoke, new secret shown once),
AuditLog (filters + pagination), Health (PG + Redis status, 30s refresh)
- Components: Button, Badge, ConfirmDialog, AppShell, RequireAuth
- All destructive actions gated by ConfirmDialog
- Zero dangerouslySetInnerHTML; sessionStorage only (OWASP compliant)
- src/routes/health.ts: unauthenticated GET /health — PG + Redis connectivity
- src/app.ts: health route + dashboard/dist/ served at /dashboard with SPA fallback
- 6 new health route tests; 308/308 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
19
src/app.ts
19
src/app.ts
@@ -31,11 +31,13 @@ import { createAgentsRouter } from './routes/agents.js';
|
||||
import { createTokenRouter } from './routes/token.js';
|
||||
import { createCredentialsRouter } from './routes/credentials.js';
|
||||
import { createAuditRouter } from './routes/audit.js';
|
||||
import { createHealthRouter } from './routes/health.js';
|
||||
|
||||
import { errorHandler } from './middleware/errorHandler.js';
|
||||
import { createOpaMiddleware } from './middleware/opa.js';
|
||||
import { createVaultClientFromEnv } from './vault/VaultClient.js';
|
||||
import { RedisClientType } from 'redis';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Creates and returns a configured Express application.
|
||||
@@ -139,6 +141,9 @@ export async function createApp(): Promise<Application> {
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// Health check — unauthenticated, no OPA
|
||||
app.use('/health', createHealthRouter(pool, redis as RedisClientType));
|
||||
|
||||
app.use(`${API_BASE}/agents`, createAgentsRouter(agentController, opaMiddleware));
|
||||
app.use(
|
||||
`${API_BASE}/agents/:agentId/credentials`,
|
||||
@@ -147,6 +152,20 @@ export async function createApp(): Promise<Application> {
|
||||
app.use(`${API_BASE}/token`, createTokenRouter(tokenController, opaMiddleware));
|
||||
app.use(`${API_BASE}/audit`, createAuditRouter(auditController, opaMiddleware));
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Dashboard static assets (served from dashboard/dist/)
|
||||
// Placed after API routes so API routes take precedence.
|
||||
// __dirname is available because the project compiles to CommonJS.
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
const dashboardDist = path.resolve(__dirname, '../../dashboard/dist');
|
||||
|
||||
app.use('/dashboard', express.static(dashboardDist));
|
||||
|
||||
// SPA fallback — serve index.html for all /dashboard/* routes not matching a static file
|
||||
app.get('/dashboard/*', (_req, res) => {
|
||||
res.sendFile(path.join(dashboardDist, 'index.html'));
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Global error handler (must be last)
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
79
src/routes/health.ts
Normal file
79
src/routes/health.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Health check route for SentryAgent.ai AgentIdP.
|
||||
* Returns connectivity status for PostgreSQL and Redis.
|
||||
* Unauthenticated — safe to call from monitoring systems and the dashboard.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { Pool } from 'pg';
|
||||
import { RedisClientType } from 'redis';
|
||||
|
||||
/** Response shape for GET /health */
|
||||
interface HealthResponse {
|
||||
status: 'ok' | 'degraded';
|
||||
version: string;
|
||||
uptime: number;
|
||||
services: {
|
||||
postgres: 'connected' | 'disconnected';
|
||||
redis: 'connected' | 'disconnected';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns the Express router for the health endpoint.
|
||||
*
|
||||
* @param pool - PostgreSQL connection pool.
|
||||
* @param redis - Redis client instance.
|
||||
* @returns Configured Express router.
|
||||
*/
|
||||
export function createHealthRouter(pool: Pool, redis: RedisClientType): Router {
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /health
|
||||
* Returns 200 when all services are healthy, 503 when any are degraded.
|
||||
*/
|
||||
router.get('/', (_req: Request, res: Response): void => {
|
||||
const check = async (): Promise<void> => {
|
||||
let postgresStatus: 'connected' | 'disconnected' = 'disconnected';
|
||||
let redisStatus: 'connected' | 'disconnected' = 'disconnected';
|
||||
|
||||
// Check PostgreSQL
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
await client.query('SELECT 1');
|
||||
client.release();
|
||||
postgresStatus = 'connected';
|
||||
} catch {
|
||||
postgresStatus = 'disconnected';
|
||||
}
|
||||
|
||||
// Check Redis
|
||||
try {
|
||||
await redis.ping();
|
||||
redisStatus = 'connected';
|
||||
} catch {
|
||||
redisStatus = 'disconnected';
|
||||
}
|
||||
|
||||
const allHealthy = postgresStatus === 'connected' && redisStatus === 'connected';
|
||||
const httpStatus = allHealthy ? 200 : 503;
|
||||
|
||||
const body: HealthResponse = {
|
||||
status: allHealthy ? 'ok' : 'degraded',
|
||||
version: process.env['npm_package_version'] ?? '1.0.0',
|
||||
uptime: Math.floor(process.uptime()),
|
||||
services: {
|
||||
postgres: postgresStatus,
|
||||
redis: redisStatus,
|
||||
},
|
||||
};
|
||||
|
||||
res.status(httpStatus).json(body);
|
||||
};
|
||||
|
||||
void check();
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user