feat: dashboard theme overhaul — light content area with branded teal sidebar

Complete frontend visual redesign using certctl logo color palette:
- Deep teal sidebar (#0c2e25) with prominent centered logo (64px in white pill)
- Light content area (#f0f4f8) with white cards and visible borders
- Brand colors from logo: teal (#2ea88f), blue (#3b7dd8), orange (#e8873a), green (#4ebe6e)
- Inter + JetBrains Mono typography, colored stat card top borders
- All 17 pages + 7 components updated (25 files, ~700 lines changed)
- 15 new dashboard screenshots replacing old dark theme screenshots
- Prometheus metrics e2e test added, integration test mock fixes
- Docs updated: architecture.md theme description, testing-guide.md DNS-PERSIST-01 coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-26 23:27:42 -04:00
parent 8380cb7946
commit 50c520e1ff
48 changed files with 699 additions and 519 deletions
+26 -26
View File
@@ -9,16 +9,16 @@ import { formatDateTime } from '../api/utils';
import type { AuditEvent } from '../api/types';
const actionColors: Record<string, string> = {
certificate_created: 'text-emerald-400',
renewal_triggered: 'text-blue-400',
renewal_job_created: 'text-blue-400',
renewal_completed: 'text-emerald-400',
deployment_completed: 'text-emerald-400',
deployment_failed: 'text-red-400',
expiration_alert_sent: 'text-amber-400',
agent_registered: 'text-blue-400',
policy_violated: 'text-red-400',
certificate_revoked: 'text-red-400',
certificate_created: 'text-emerald-600',
renewal_triggered: 'text-brand-500',
renewal_job_created: 'text-brand-500',
renewal_completed: 'text-emerald-600',
deployment_completed: 'text-emerald-600',
deployment_failed: 'text-red-600',
expiration_alert_sent: 'text-amber-600',
agent_registered: 'text-brand-500',
policy_violated: 'text-red-600',
certificate_revoked: 'text-red-600',
};
const RESOURCE_TYPES = ['', 'certificate', 'agent', 'job', 'notification', 'policy', 'target', 'issuer'];
@@ -94,7 +94,7 @@ export default function AuditPage() {
key: 'action',
label: 'Action',
render: (e) => (
<span className={`text-sm font-medium ${actionColors[e.action] || 'text-slate-300'}`}>
<span className={`text-sm font-medium ${actionColors[e.action] || 'text-ink'}`}>
{e.action.replace(/_/g, ' ')}
</span>
),
@@ -104,8 +104,8 @@ export default function AuditPage() {
label: 'Actor',
render: (e) => (
<div>
<div className="text-sm text-slate-200">{e.actor}</div>
<div className="text-xs text-slate-500">{e.actor_type}</div>
<div className="text-sm text-ink">{e.actor}</div>
<div className="text-xs text-ink-faint">{e.actor_type}</div>
</div>
),
},
@@ -114,8 +114,8 @@ export default function AuditPage() {
label: 'Resource',
render: (e) => (
<div>
<div className="text-sm text-slate-300">{e.resource_type}</div>
<div className="text-xs text-slate-500 font-mono">{e.resource_id}</div>
<div className="text-sm text-ink">{e.resource_type}</div>
<div className="text-xs text-ink-faint font-mono">{e.resource_id}</div>
</div>
),
},
@@ -123,15 +123,15 @@ export default function AuditPage() {
key: 'details',
label: 'Details',
render: (e) => {
if (!e.details || Object.keys(e.details).length === 0) return <span className="text-slate-500">&mdash;</span>;
if (!e.details || Object.keys(e.details).length === 0) return <span className="text-ink-faint">&mdash;</span>;
return (
<span className="text-xs text-slate-400 font-mono truncate max-w-xs block">
<span className="text-xs text-ink-muted font-mono truncate max-w-xs block">
{JSON.stringify(e.details).slice(0, 60)}
</span>
);
},
},
{ key: 'time', label: 'Time', render: (e) => <span className="text-xs text-slate-400">{formatDateTime(e.timestamp)}</span> },
{ key: 'time', label: 'Time', render: (e) => <span className="text-xs text-ink-muted">{formatDateTime(e.timestamp)}</span> },
];
const hasFilters = resourceType || actorFilter || timeRange || actionFilter;
@@ -144,21 +144,21 @@ export default function AuditPage() {
action={
filtered.length > 0 ? (
<div className="flex gap-2">
<button onClick={() => exportCSV(filtered)} className="btn btn-ghost text-xs border border-slate-600">
<button onClick={() => exportCSV(filtered)} className="btn btn-ghost text-xs border border-surface-border">
Export CSV
</button>
<button onClick={() => exportJSON(filtered)} className="btn btn-ghost text-xs border border-slate-600">
<button onClick={() => exportJSON(filtered)} className="btn btn-ghost text-xs border border-surface-border">
Export JSON
</button>
</div>
) : undefined
}
/>
<div className="px-4 py-3 flex flex-wrap gap-3 border-b border-slate-700/50">
<div className="px-4 py-3 flex flex-wrap gap-3 border-b border-surface-border/50">
<select
value={resourceType}
onChange={(e) => setResourceType(e.target.value)}
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 focus:outline-none focus:border-blue-500"
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink focus:outline-none focus:border-brand-400"
>
<option value="">All resources</option>
{RESOURCE_TYPES.filter(Boolean).map((t) => (
@@ -170,19 +170,19 @@ export default function AuditPage() {
placeholder="Filter by actor..."
value={actorFilter}
onChange={(e) => setActorFilter(e.target.value)}
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 placeholder-slate-500 focus:outline-none focus:border-blue-500 w-40"
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink placeholder-ink-faint focus:outline-none focus:border-brand-400 w-40"
/>
<input
type="text"
placeholder="Filter by action..."
value={actionFilter}
onChange={(e) => setActionFilter(e.target.value)}
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 placeholder-slate-500 focus:outline-none focus:border-blue-500 w-40"
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink placeholder-ink-faint focus:outline-none focus:border-brand-400 w-40"
/>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 focus:outline-none focus:border-blue-500"
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink focus:outline-none focus:border-brand-400"
>
{TIME_RANGES.map((r) => (
<option key={r.value} value={r.value}>{r.label}</option>
@@ -191,7 +191,7 @@ export default function AuditPage() {
{hasFilters && (
<button
onClick={() => { setResourceType(''); setActorFilter(''); setTimeRange(''); setActionFilter(''); }}
className="text-xs text-slate-400 hover:text-slate-200 transition-colors"
className="text-xs text-ink-muted hover:text-ink transition-colors"
>
Clear filters
</button>