diff --git a/dashboard/backend/src/modules/deployments/routes.ts b/dashboard/backend/src/modules/deployments/routes.ts index fcaae28..688967a 100644 --- a/dashboard/backend/src/modules/deployments/routes.ts +++ b/dashboard/backend/src/modules/deployments/routes.ts @@ -13,23 +13,29 @@ import { createAuditLog } from '../audit/repository.js'; import { productId } from '../../lib/config.js'; export async function deploymentRoutes(fastify: FastifyInstance) { - // Get recent deployments across all services - fastify.get('/deployments', async (req, reply) => { + // Get recent deployments across all services (admin only) + fastify.get('/deployments', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { const query = QueryParamsSchema.parse(req.query); const deployments = await getRecentDeployments(query.limit); return reply.send(deployments); }); - // Get deployments for a specific service - fastify.get('/deployments/service/:serviceId', async (req, reply) => { + // Get deployments for a specific service (admin only) + fastify.get('/deployments/service/:serviceId', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { const params = TriggerDeploymentParamsSchema.parse(req.params); const query = QueryParamsSchema.parse(req.query); const deployments = await getDeploymentsByService(params.serviceId, query.limit); return reply.send(deployments); }); - // Get single deployment - fastify.get('/deployments/:id', async (req, reply) => { + // Get single deployment (admin only) + fastify.get('/deployments/:id', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { const params = DeploymentParamsSchema.parse(req.params); const deployment = await getDeploymentById(params.id); if (!deployment) { @@ -38,9 +44,11 @@ export async function deploymentRoutes(fastify: FastifyInstance) { return reply.send(deployment); }); - // Get deployment logs (SSE disabled due to Fastify 5 compatibility) + // Get deployment logs (admin only; SSE disabled due to Fastify 5 compatibility) // TODO: Re-enable SSE when fastify-sse-v2 supports Fastify 5 - fastify.get('/deployments/:id/logs', async (req, reply) => { + fastify.get('/deployments/:id/logs', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { const params = DeploymentParamsSchema.parse(req.params); const deployment = await getDeploymentById(params.id); diff --git a/dashboard/backend/src/modules/env/routes.ts b/dashboard/backend/src/modules/env/routes.ts index 1327e61..8c9010f 100644 --- a/dashboard/backend/src/modules/env/routes.ts +++ b/dashboard/backend/src/modules/env/routes.ts @@ -1,21 +1,27 @@ import type { FastifyInstance } from 'fastify'; -import { BadRequestError } from '../../lib/auth.js'; +import { BadRequestError, requireAdmin } from '../../lib/auth.js'; import { deleteEnvVar, getEnvVar, getEnvVars, upsertEnvVar } from './repository.js'; import { EnvVarInputSchema, EnvVarParamsSchema } from './types.js'; export async function envRoutes(fastify: FastifyInstance) { - fastify.get('/env', async (req, reply) => { + fastify.get('/env', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { return reply.send(await getEnvVars()); }); - fastify.get('/env/:id', async (req, reply) => { + fastify.get('/env/:id', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { const params = EnvVarParamsSchema.parse(req.params); const envVar = await getEnvVar(params.id); if (!envVar) return reply.code(404).send({ error: 'Environment variable not found' }); return reply.send(envVar); }); - fastify.post('/env', async (req, reply) => { + fastify.post('/env', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { try { const input = EnvVarInputSchema.parse(req.body) as { name: string }; return reply.code(201).send(await upsertEnvVar(input)); @@ -25,7 +31,9 @@ export async function envRoutes(fastify: FastifyInstance) { } }); - fastify.put('/env/:id', async (req, reply) => { + fastify.put('/env/:id', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { try { const params = EnvVarParamsSchema.parse(req.params); const input = EnvVarInputSchema.parse({ ...(req.body as object), id: params.id }) as { name: string; id: string }; @@ -36,13 +44,18 @@ export async function envRoutes(fastify: FastifyInstance) { } }); - fastify.delete('/env/:id', async (req, reply) => { + fastify.delete('/env/:id', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { const params = EnvVarParamsSchema.parse(req.params); - await deleteEnvVar(params.id); + const deleted = await deleteEnvVar(params.id); + if (!deleted) return reply.code(404).send({ error: 'Environment variable not found' }); return reply.code(204).send(); }); - fastify.post('/env/sync-azure', async (req, reply) => { + fastify.post('/env/sync-azure', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { return reply.send({ synced: 0, errors: ['Azure Key Vault sync is not configured in this local dashboard build.'] }); }); } diff --git a/dashboard/backend/src/modules/health/routes.ts b/dashboard/backend/src/modules/health/routes.ts index 6e03121..6c4f31d 100644 --- a/dashboard/backend/src/modules/health/routes.ts +++ b/dashboard/backend/src/modules/health/routes.ts @@ -53,16 +53,8 @@ export async function healthRoutes(fastify: FastifyInstance) { // Clear health cache (admin only) fastify.delete('/health/cache', { preHandler: async (req) => requireAdmin(req), - }, async (req, reply) => { - try { - requireAdmin(req); - clearHealthCache(); - return reply.send({ message: 'Health cache cleared' }); - } catch (error) { - if (error instanceof Error) { - throw new BadRequestError(error.message); - } - throw error; - } + }, async (_req, reply) => { + clearHealthCache(); + return reply.send({ message: 'Health cache cleared' }); }); } diff --git a/dashboard/web/src/app/health/page.tsx b/dashboard/web/src/app/health/page.tsx index fcc03ba..34c05f6 100644 --- a/dashboard/web/src/app/health/page.tsx +++ b/dashboard/web/src/app/health/page.tsx @@ -5,6 +5,7 @@ import { SidebarNav } from '@/components/sidebar-nav'; import { api } from '@/lib/api'; import type { Service, ServiceHealth } from '@/lib/api'; import { Activity, Clock, RefreshCw, TrendingUp } from 'lucide-react'; +import { getStatusColor } from '@/lib/utils'; export default function HealthDashboardPage() { const [services, setServices] = useState([]); @@ -52,21 +53,6 @@ export default function HealthDashboardPage() { return () => clearInterval(interval); }, [loadData]); - function getStatusColor(status: string) { - switch (status) { - case 'up': - case 'success': - return 'text-green-600 bg-green-50 border-green-200'; - case 'down': - case 'failed': - return 'text-red-600 bg-red-50 border-red-200'; - case 'degraded': - case 'running': - return 'text-yellow-600 bg-yellow-50 border-yellow-200'; - default: - return 'text-gray-600 bg-gray-50 border-gray-200'; - } - } function getResponseTimeColor(responseTime?: number) { if (!responseTime) return 'text-gray-500'; diff --git a/dashboard/web/src/app/metrics/page.tsx b/dashboard/web/src/app/metrics/page.tsx index 822456d..131acb1 100644 --- a/dashboard/web/src/app/metrics/page.tsx +++ b/dashboard/web/src/app/metrics/page.tsx @@ -151,38 +151,58 @@ export default function MetricsPage() { {/* Deployment Trend Chart */}

Deployment Trend (Last 7 Days)

-
- {deploymentTrend.map((day) => ( -
-
-
0 ? '4px' : '0' }} - title={`Success: ${day.success}`} - /> -
0 ? '4px' : '0' }} - title={`Failed: ${day.failed}`} - /> -
-
- {new Date(day.date).toLocaleDateString('en', { weekday: 'short' })} -
-
{day.count}
+ {deployments.length === 0 ? ( +
+ No deployment data yet +
+ ) : ( + <> + {/* Bar chart — each column is a flex column-reverse so bars grow from bottom */} +
+ {deploymentTrend.map((day) => ( +
+ {/* Count label above bars */} + + {day.count > 0 ? day.count : ''} + + {/* Stacked bar (grows upward via flex-col-reverse) */} +
0 ? 4 : 0)}px` }} + > + {day.failed > 0 && ( +
+ )} + {day.success > 0 && ( +
+ )} +
+
+ {new Date(day.date + 'T12:00:00').toLocaleDateString('en', { weekday: 'short' })} +
+
+ ))}
- ))} -
-
-
-
- Success -
-
-
- Failed -
-
+
+
+
+ Success +
+
+
+ Failed +
+
+ + )}
{/* Deployments by Service */} diff --git a/dashboard/web/src/app/page.tsx b/dashboard/web/src/app/page.tsx index 808b9b4..75bcfae 100644 --- a/dashboard/web/src/app/page.tsx +++ b/dashboard/web/src/app/page.tsx @@ -5,7 +5,8 @@ import { SidebarNav } from '@/components/sidebar-nav'; import { api } from '@/lib/api'; import type { Service, Deployment } from '@/lib/api'; import { useAuth } from '@/lib/auth'; -import { Play, Activity, Clock, RefreshCw, Plus, Edit, Trash2, FileText } from 'lucide-react'; +import { Play, Activity, Clock, RefreshCw, Plus, Edit, Trash2, FileText, AlertCircle } from 'lucide-react'; +import { getStatusColor } from '@/lib/utils'; import { ServiceForm } from '@/components/service-form'; import { LogViewer } from '@/components/log-viewer'; @@ -126,22 +127,6 @@ export default function DashboardPage() { setViewingLogsDeployment(null); } - function getStatusColor(status: string) { - switch (status) { - case 'up': - case 'success': - return 'text-green-600 bg-green-50 border-green-200'; - case 'down': - case 'failed': - return 'text-red-600 bg-red-50 border-red-200'; - case 'degraded': - case 'running': - return 'text-yellow-600 bg-yellow-50 border-yellow-200'; - default: - return 'text-gray-600 bg-gray-50 border-gray-200'; - } - } - if (loading) { return (
@@ -188,12 +173,6 @@ export default function DashboardPage() { Create Service -