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
+49 -48
View File
@@ -19,28 +19,29 @@ const STATUS_COLORS: Record<string, string> = {
Expired: '#ef4444',
Revoked: '#8b5cf6',
Pending: '#6366f1',
RenewalInProgress: '#3b82f6',
RenewalInProgress: '#2ea88f',
Failed: '#f43f5e',
Archived: '#64748b',
};
function StatCard({ label, value, icon, color }: { label: string; value: string | number; icon: string; color: string }) {
const colorMap: Record<string, string> = {
success: 'bg-emerald-500/10 text-emerald-400',
warning: 'bg-amber-500/10 text-amber-400',
danger: 'bg-red-500/10 text-red-400',
info: 'bg-blue-500/10 text-blue-400',
const colorMap: Record<string, { bg: string; border: string; text: string }> = {
success: { bg: 'bg-emerald-50', border: 'border-t-emerald-500', text: 'text-emerald-700' },
warning: { bg: 'bg-amber-50', border: 'border-t-amber-500', text: 'text-amber-700' },
danger: { bg: 'bg-red-50', border: 'border-t-red-500', text: 'text-red-700' },
info: { bg: 'bg-blue-50', border: 'border-t-brand-400', text: 'text-brand-500' },
};
const config = colorMap[color] || colorMap.info;
return (
<div className="card p-5 flex items-start gap-4 hover:border-blue-500/30 transition-colors">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center shrink-0 ${colorMap[color] || colorMap.info}`}>
<div className={`bg-surface border border-surface-border border-t-4 ${config.border} rounded p-5 flex items-start gap-4 hover:bg-surface-muted transition-colors shadow-sm`}>
<div className={`w-10 h-10 rounded flex items-center justify-center shrink-0 ${config.bg} ${config.text}`}>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d={icon} />
</svg>
</div>
<div>
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">{label}</p>
<p className="text-2xl font-bold mt-1">{value}</p>
<p className="text-xs font-semibold text-ink-muted uppercase tracking-wider">{label}</p>
<p className="text-2xl font-bold mt-1 text-ink">{value}</p>
</div>
</div>
);
@@ -48,8 +49,8 @@ function StatCard({ label, value, icon, color }: { label: string; value: string
function ChartCard({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="card p-5">
<h3 className="text-sm font-semibold text-slate-300 mb-4">{title}</h3>
<div className="bg-surface border border-surface-border rounded p-5 shadow-sm">
<h3 className="text-sm font-semibold text-ink-muted mb-4">{title}</h3>
<div className="h-64">
{children}
</div>
@@ -60,8 +61,8 @@ function ChartCard({ title, children }: { title: string; children: React.ReactNo
const CustomTooltip = ({ active, payload, label }: any) => {
if (!active || !payload?.length) return null;
return (
<div className="bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-xs shadow-lg">
<p className="text-slate-300 mb-1">{label}</p>
<div className="bg-surface border border-surface-border rounded px-3 py-2 text-xs shadow-lg">
<p className="text-ink mb-1">{label}</p>
{payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }}>
{entry.name}: {typeof entry.value === 'number' && entry.name?.includes('rate') ? `${entry.value.toFixed(1)}%` : entry.value}
@@ -159,12 +160,12 @@ export default function DashboardPage() {
<Legend
verticalAlign="bottom"
height={36}
formatter={(value: string) => <span className="text-xs text-slate-400">{value}</span>}
formatter={(value: string) => <span className="text-xs text-ink-muted">{value}</span>}
/>
</PieChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-sm text-slate-500">No certificate data</div>
<div className="h-full flex items-center justify-center text-sm text-ink-faint">No certificate data</div>
)}
</ChartCard>
@@ -173,15 +174,15 @@ export default function DashboardPage() {
{weeklyExpiration.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={weeklyExpiration}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="week" tick={{ fill: '#94a3b8', fontSize: 11 }} tickFormatter={formatShortDate} />
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} allowDecimals={false} />
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="week" tick={{ fill: '#64748b', fontSize: 11 }} tickFormatter={formatShortDate} />
<YAxis tick={{ fill: '#64748b', fontSize: 11 }} allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="count" name="Expiring certs" fill="#f59e0b" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-sm text-slate-500">No expiration data</div>
<div className="h-full flex items-center justify-center text-sm text-ink-faint">No expiration data</div>
)}
</ChartCard>
</div>
@@ -193,17 +194,17 @@ export default function DashboardPage() {
{(jobTrends || []).length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={jobTrends}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="date" tick={{ fill: '#94a3b8', fontSize: 11 }} tickFormatter={formatShortDate} />
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} allowDecimals={false} />
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="date" tick={{ fill: '#64748b', fontSize: 11 }} tickFormatter={formatShortDate} />
<YAxis tick={{ fill: '#64748b', fontSize: 11 }} allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Legend formatter={(value: string) => <span className="text-xs text-slate-400">{value}</span>} />
<Legend formatter={(value: string) => <span className="text-xs text-ink-muted">{value}</span>} />
<Line type="monotone" dataKey="completed_count" name="Completed" stroke="#10b981" strokeWidth={2} dot={false} />
<Line type="monotone" dataKey="failed_count" name="Failed" stroke="#ef4444" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-sm text-slate-500">No job trend data</div>
<div className="h-full flex items-center justify-center text-sm text-ink-faint">No job trend data</div>
)}
</ChartCard>
@@ -212,28 +213,28 @@ export default function DashboardPage() {
{(issuanceRate || []).length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={issuanceRate}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="date" tick={{ fill: '#94a3b8', fontSize: 11 }} tickFormatter={formatShortDate} />
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} allowDecimals={false} />
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="date" tick={{ fill: '#64748b', fontSize: 11 }} tickFormatter={formatShortDate} />
<YAxis tick={{ fill: '#64748b', fontSize: 11 }} allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="issued_count" name="Issued" fill="#3b82f6" radius={[4, 4, 0, 0]} />
<Bar dataKey="issued_count" name="Issued" fill="#2ea88f" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-sm text-slate-500">No issuance data</div>
<div className="h-full flex items-center justify-center text-sm text-ink-faint">No issuance data</div>
)}
</ChartCard>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Expiring Certificates */}
<div className="card p-5">
<div className="bg-surface border border-surface-border rounded p-5 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-slate-300">Certificates Expiring Soon</h3>
<button onClick={() => navigate('/certificates')} className="text-xs text-blue-400 hover:text-blue-300">View all</button>
<h3 className="text-sm font-semibold text-ink-muted">Certificates Expiring Soon</h3>
<button onClick={() => navigate('/certificates')} className="text-xs text-brand-400 hover:text-brand-500">View all</button>
</div>
{!certs?.data?.length ? (
<p className="text-sm text-slate-500">No certificates</p>
<p className="text-sm text-ink-faint">No certificates</p>
) : (
<div className="space-y-2">
{certs.data
@@ -246,17 +247,17 @@ export default function DashboardPage() {
<div
key={c.id}
onClick={() => navigate(`/certificates/${c.id}`)}
className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-slate-700/50 cursor-pointer transition-colors"
className="flex items-center justify-between py-2 px-3 rounded hover:bg-surface-muted cursor-pointer transition-colors"
>
<div>
<div className="text-sm text-slate-200">{c.common_name}</div>
<div className="text-xs text-slate-500">{c.environment || 'no env'}</div>
<div className="text-sm text-ink">{c.common_name}</div>
<div className="text-xs text-ink-faint">{c.environment || 'no env'}</div>
</div>
<div className="text-right">
<div className={`text-sm ${expiryColor(days)}`}>
{days <= 0 ? 'Expired' : `${days} days`}
</div>
<div className="text-xs text-slate-500">{formatDate(c.expires_at)}</div>
<div className="text-xs text-ink-faint">{formatDate(c.expires_at)}</div>
</div>
</div>
);
@@ -266,20 +267,20 @@ export default function DashboardPage() {
</div>
{/* Recent Jobs */}
<div className="card p-5">
<div className="bg-surface border border-surface-border rounded p-5 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-slate-300">Recent Jobs</h3>
<button onClick={() => navigate('/jobs')} className="text-xs text-blue-400 hover:text-blue-300">View all</button>
<h3 className="text-sm font-semibold text-ink-muted">Recent Jobs</h3>
<button onClick={() => navigate('/jobs')} className="text-xs text-brand-400 hover:text-brand-500">View all</button>
</div>
{!jobs?.data?.length ? (
<p className="text-sm text-slate-500">No jobs</p>
<p className="text-sm text-ink-faint">No jobs</p>
) : (
<div className="space-y-2">
{jobs.data.slice(0, 5).map(j => (
<div key={j.id} className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-slate-700/50 transition-colors">
<div key={j.id} className="flex items-center justify-between py-2 px-3 rounded hover:bg-surface-muted transition-colors">
<div>
<div className="text-sm text-slate-200">{j.type}</div>
<div className="text-xs text-slate-500 font-mono">{j.certificate_id}</div>
<div className="text-sm text-ink">{j.type}</div>
<div className="text-xs text-ink-faint font-mono">{j.certificate_id}</div>
</div>
<StatusBadge status={j.status} />
</div>
@@ -291,10 +292,10 @@ export default function DashboardPage() {
{/* Pending Jobs Banner */}
{pendingJobs > 0 && (
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg px-5 py-4 flex items-center justify-between">
<div className="bg-brand-50 border border-brand-200 rounded px-5 py-4 flex items-center justify-between">
<div>
<p className="text-sm font-medium text-blue-400">{pendingJobs} pending job{pendingJobs > 1 ? 's' : ''}</p>
<p className="text-xs text-slate-400 mt-0.5">Jobs are waiting to be processed</p>
<p className="text-sm font-medium text-brand-600">{pendingJobs} pending job{pendingJobs > 1 ? 's' : ''}</p>
<p className="text-xs text-brand-600/70 mt-0.5">Jobs are waiting to be processed</p>
</div>
<button onClick={() => navigate('/jobs')} className="btn btn-primary text-xs">View Jobs</button>
</div>