small fix, don't worry about it

This commit is contained in:
Tomas Dvorak committed 2026-04-10 12:02:36 +02:00
1 parent 08bd0c6e5c
commit 08cb5754f3
638 files changed
+57330 -34704

No files matched your search

@@ -0,0 +1,820 @@
import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import {
createDeployment,
deleteService,
getProjectById,
getDeploymentLogs,
getServiceById,
listDeployments,
listServiceLogs,
rollbackDeployment,
} from '@/lib/api-client';
import { getDemoProjectById, getDemoServiceById } from '@/lib/demo-data';
import { formatDate, formatRelative, seededMetric } from '@/lib/time';
import {
ArrowLeft,
Activity,
FileText,
Settings,
Sliders,
Play,
RotateCw,
Trash2,
Check,
X,
Loader2,
Clock,
Cpu,
Layers,
MemoryStick,
Zap,
Timer,
Sparkles,
RefreshCw,
Box,
} from 'lucide-react';
type ServiceSection = 'metrics' | 'logs' | 'config' | 'settings';
const sectionItems: Array<{ key: ServiceSection; label: string; icon: typeof Activity }> = [
{ key: 'metrics', label: 'Metrics', icon: Activity },
{ key: 'logs', label: 'Logs', icon: FileText },
{ key: 'config', label: 'Config', icon: Sliders },
{ key: 'settings', label: 'Settings', icon: Settings },
];
function MetricCard({ label, value, hint, icon: Icon, trend }: { label: string; value: string; hint: string; icon: typeof Cpu; trend?: 'up' | 'down' | 'stable' }) {
return (
<div className="panel p-4">
<div className="flex items-center justify-between mb-3">
<div className="w-8 h-8 rounded-[var(--radius-sm)] bg-[var(--accent-primary-soft)] flex items-center justify-center">
<Icon size={16} className="text-[var(--accent-primary)]" />
</div>
{trend && (
<div className={`text-xs ${trend === 'up' ? 'text-[var(--success)]' : trend === 'down' ? 'text-[var(--error)]' : 'text-[var(--text-tertiary)]'}`}>
{trend === 'up' ? '↑' : trend === 'down' ? '↓' : '→'}
</div>
)}
</div>
<p className="text-2xl font-semibold text-[var(--text-primary)]">{value}</p>
<p className="text-xs text-[var(--text-muted)] mt-1">{label}</p>
<p className="text-[10px] text-[var(--text-tertiary)] mt-0.5">{hint}</p>
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const config = {
running: { color: 'var(--success)', bg: 'var(--success-soft)', Icon: Check, animate: false },
deployed: { color: 'var(--success)', bg: 'var(--success-soft)', Icon: Check, animate: false },
failed: { color: 'var(--error)', bg: 'var(--error-soft)', Icon: X, animate: false },
building: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: Loader2, animate: true },
pending: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: Loader2, animate: true },
rolling_back: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: RefreshCw, animate: true },
stopped: { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: Box, animate: false },
}[status] || { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: Box, animate: false };
const { Icon } = config;
return (
<div
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium"
style={{ background: config.bg, color: config.color }}
>
<Icon size={12} className={config.animate ? 'animate-spin' : ''} />
{status.replace('_', ' ')}
</div>
);
}
export function ServiceDetailPage() {
const { projectId = '', serviceId = '' } = useParams<{ projectId: string; serviceId: string }>();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
const isDemoMode = searchParams.get('demo') === '1';
const [activeSection, setActiveSection] = useState<ServiceSection>('metrics');
const [logTail, setLogTail] = useState('100');
const projectQuery = useQuery({
queryKey: ['project', projectId],
queryFn: () => getProjectById(projectId),
enabled: Boolean(projectId) && !isDemoMode,
});
const serviceQuery = useQuery({
queryKey: ['service', serviceId],
queryFn: () => getServiceById(serviceId),
enabled: Boolean(serviceId) && !isDemoMode,
});
const deploymentsQuery = useQuery({
queryKey: ['service-deployments', serviceId],
queryFn: () => listDeployments(serviceId),
enabled: Boolean(serviceId) && !isDemoMode,
refetchInterval: 4000,
});
const serviceLogsQuery = useQuery({
queryKey: ['service-logs', serviceId, logTail],
queryFn: () => listServiceLogs(serviceId, { tail: logTail }),
enabled: Boolean(serviceId) && !isDemoMode && activeSection === 'logs',
});
const latestDeployment = useMemo(() => {
if (isDemoMode) {
return null;
}
const deployments = deploymentsQuery.data ?? [];
return deployments[0] ?? null;
}, [deploymentsQuery.data, isDemoMode]);
const deploymentLogsQuery = useQuery({
queryKey: ['deployment-logs', latestDeployment?.id],
queryFn: () => getDeploymentLogs(latestDeployment!.id, { type: 'all' }),
enabled:
Boolean(latestDeployment?.id) &&
!isDemoMode &&
activeSection === 'logs',
});
const deployMutation = useMutation({
mutationFn: (trigger: 'manual' | 'restart') => createDeployment(serviceId, { trigger }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['service-deployments', serviceId] });
queryClient.invalidateQueries({ queryKey: ['service', serviceId] });
queryClient.invalidateQueries({ queryKey: ['project-services', projectId] });
},
});
const rollbackMutation = useMutation({
mutationFn: (deploymentId: string) => rollbackDeployment(deploymentId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['service-deployments', serviceId] });
queryClient.invalidateQueries({ queryKey: ['service', serviceId] });
queryClient.invalidateQueries({ queryKey: ['project-services', projectId] });
},
});
const deleteServiceMutation = useMutation({
mutationFn: () => deleteService(serviceId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['project-services', projectId] });
queryClient.invalidateQueries({ queryKey: ['projects'] });
navigate(`/projects/${projectId}`);
},
});
const project = isDemoMode ? getDemoProjectById(projectId) : projectQuery.data;
const service = isDemoMode ? getDemoServiceById(serviceId) : serviceQuery.data;
const metricSet = useMemo(() => {
if (!service) {
return null;
}
const cpu = seededMetric(`${service.id}:cpu`, 12, 78);
const memory = seededMetric(`${service.id}:mem`, 24, 91);
const req = seededMetric(`${service.id}:req`, 120, 5100);
const latency = seededMetric(`${service.id}:lat`, 17, 210);
return {
cpu,
memory,
req,
latency,
};
}, [service]);
const deploymentSummary = useMemo(() => {
const deployments = isDemoMode ? [] : deploymentsQuery.data ?? [];
const total = deployments.length;
const active = deployments.filter((deployment) =>
['pending', 'building', 'deploying', 'rolling_back'].includes(deployment.status),
).length;
const failed = deployments.filter((deployment) => deployment.status === 'failed').length;
return { total, active, failed };
}, [deploymentsQuery.data, isDemoMode]);
const canRollback = (status: string): boolean => status === 'deployed' || status === 'failed';
if (!isDemoMode && (projectQuery.isLoading || serviceQuery.isLoading)) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex items-center gap-3 text-[var(--text-secondary)]">
<Loader2 size={20} className="animate-spin" />
<span>Loading service...</span>
</div>
</div>
);
}
if (!isDemoMode && serviceQuery.isError) {
return (
<div className="min-h-screen flex items-center justify-center p-8">
<div className="panel p-8 text-center max-w-md">
<div className="w-12 h-12 mx-auto mb-4 rounded-full bg-[var(--error-soft)] flex items-center justify-center">
<X size={24} className="text-[var(--error)]" />
</div>
<p className="text-lg font-medium text-[var(--text-primary)]">Failed to load service</p>
<p className="mt-2 text-sm text-[var(--text-secondary)]">{(serviceQuery.error as Error).message}</p>
<button
onClick={() => serviceQuery.refetch()}
className="mt-6 px-4 py-2 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-sm font-medium hover:border-[var(--border-default)] transition-colors"
>
Retry
</button>
</div>
</div>
);
}
if (!service || !project) {
return (
<div className="min-h-screen flex items-center justify-center p-8">
<div className="panel p-8 text-center max-w-md">
<div className="w-12 h-12 mx-auto mb-4 rounded-full bg-[var(--surface-muted)] flex items-center justify-center">
<Box size={24} className="text-[var(--text-tertiary)]" />
</div>
<p className="text-lg font-medium text-[var(--text-primary)]">Service not found</p>
<p className="mt-2 text-sm text-[var(--text-secondary)]">This service may have been deleted.</p>
</div>
</div>
);
}
return (
<div className="min-h-screen relative">
{/* Breadcrumb - self.html exact match */}
<div
className="flex items-center"
style={{
gap: '6px',
padding: '14px 24px 10px',
color: '#6b6e7d',
fontSize: '13px'
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6b6e7d" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<button
onClick={() => navigate(isDemoMode ? `/projects/${project.id}?demo=1` : `/projects/${project.id}`)}
style={{ color: '#6b6e7d', textDecoration: 'none' }}
className="hover:text-[#9295a4] transition-colors"
>
Servers
</button>
<span style={{ opacity: 0.4 }}>/</span>
<span style={{ color: '#9295a4' }}>{service.name}</span>
</div>
{/* Project Header - self.html exact match */}
<div className="flex items-center" style={{ padding: '0 24px 18px' }}>
<div
className="rounded-[13px] flex items-center justify-center flex-shrink-0"
style={{
width: '46px',
height: '46px',
background: '#e8316a',
marginRight: '14px'
}}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="white">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/>
</svg>
</div>
<div>
<div className="flex items-center" style={{ gap: '10px' }}>
<span style={{ fontSize: '20px', fontWeight: 800, letterSpacing: '-0.5px', color: '#e8e9f0' }}>{service.name}</span>
<span className={`badge-${service.status === 'running' ? 'active' : 'stopped'}`}>
{service.status === 'running' && <span className="live-dot" />}
{service.status === 'running' ? 'Active' : 'Stopped'}
</span>
</div>
<div className="flex items-center" style={{ gap: '16px', marginTop: '4px' }}>
{service.status === 'running' && (
<a
href={`https://${service.name}.containr.dev`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center hover:text-[#9295a4] transition-colors"
style={{ color: '#6b6e7d', fontSize: '12.5px', textDecoration: 'none', gap: '4px' }}
>
https://{service.name}.containr.dev
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
)}
<button
onClick={() => navigate(isDemoMode ? `/projects/${project.id}?demo=1` : `/projects/${project.id}`)}
className="flex items-center hover:text-[#9295a4] transition-colors"
style={{ color: '#6b6e7d', fontSize: '12.5px', textDecoration: 'none', gap: '4px' }}
>
Project Information
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
</button>
</div>
</div>
<div className="ml-auto flex" style={{ gap: '10px' }}>
{!isDemoMode && (
<>
<button
onClick={() => deployMutation.mutate('restart')}
disabled={deployMutation.isPending || service.status !== 'running'}
className={`btn-stop ${service.status !== 'running' ? 'disabled' : ''}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
<circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/>
</svg>
STOP
</button>
<button
onClick={() => deployMutation.mutate('manual')}
disabled={deployMutation.isPending}
className={`btn-restart ${deployMutation.isPending ? 'disabled' : ''}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 .49-4.5"/>
</svg>
RESTART
</button>
</>
)}
</div>
</div>
{/* Demo Mode Banner */}
{isDemoMode && (
<div className="mx-auto w-full max-w-[1400px] px-6 py-4">
<div className="px-4 py-3 rounded-[var(--radius-md)] border border-[var(--warning-soft)] bg-[var(--warning-soft)]/50">
<div className="flex items-center gap-2 text-sm text-[var(--warning)]">
<Sparkles size={16} />
<span>Demo mode active — using sample data for preview</span>
</div>
</div>
</div>
)}
{/* Metrics Overview */}
<div className="mx-auto w-full max-w-[1400px] px-6 py-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
label="CPU"
value={`${metricSet?.cpu ?? 0}%`}
hint="Current utilization"
icon={Cpu}
trend={metricSet && metricSet.cpu > 60 ? 'up' : metricSet && metricSet.cpu < 30 ? 'down' : 'stable'}
/>
<MetricCard
label="Memory"
value={`${metricSet?.memory ?? 0}%`}
hint="Container footprint"
icon={MemoryStick}
trend={metricSet && metricSet.memory > 70 ? 'up' : 'stable'}
/>
<MetricCard
label="Requests"
value={`${metricSet?.req ?? 0}`}
hint="Last 60 minutes"
icon={Zap}
/>
<MetricCard
label="Latency"
value={`${metricSet?.latency ?? 0}ms`}
hint="P95 estimate"
icon={Timer}
trend={metricSet && metricSet.latency > 150 ? 'up' : 'stable'}
/>
</div>
{/* Deployment Summary */}
{!isDemoMode && (
<div className="mt-4 flex items-center gap-6 text-sm">
<div className="flex items-center gap-2 text-[var(--text-secondary)]">
<span className="w-2 h-2 rounded-full bg-[var(--accent-primary)]" />
<span>{deploymentSummary.total} deployments</span>
</div>
{deploymentSummary.active > 0 && (
<div className="flex items-center gap-2 text-[var(--warning)]">
<span className="w-2 h-2 rounded-full bg-[var(--warning)] animate-pulse" />
<span>{deploymentSummary.active} active</span>
</div>
)}
{deploymentSummary.failed > 0 && (
<div className="flex items-center gap-2 text-[var(--error)]">
<span className="w-2 h-2 rounded-full bg-[var(--error)]" />
<span>{deploymentSummary.failed} failed</span>
</div>
)}
{latestDeployment?.createdAt && (
<div className="flex items-center gap-1.5 text-[var(--text-tertiary)]">
<Clock size={12} />
<span>Latest {formatRelative(latestDeployment.createdAt)}</span>
</div>
)}
</div>
)}
</div>
{/* Tabs - self.html exact match */}
<div
className="flex"
style={{
borderBottom: '1px solid rgba(255,255,255,0.07)',
marginBottom: '18px',
padding: '0 24px'
}}
>
{sectionItems.map((item) => {
const active = activeSection === item.key;
const Icon = item.icon;
return (
<button
key={item.key}
onClick={() => setActiveSection(item.key)}
className={`tab ${active ? 'active' : ''}`}
>
<Icon size={14} />
{item.label}
</button>
);
})}
</div>
{/* Content - self.html exact match: padding 0 24px 28px */}
<div style={{ padding: '0 24px 28px' }}>
{activeSection === 'metrics' && (
<div className="space-y-6">
{/* Metrics Grid with enhanced cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="panel p-5 group hover:border-[var(--accent-primary)]/30 transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="card-icon">
<Cpu size={18} />
</div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">CPU Timeline</p>
<p className="text-xs text-[var(--text-tertiary)]">Last 24 intervals</p>
</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<span className="text-xs text-[var(--text-tertiary)]">Click to expand</span>
</div>
</div>
<div className="flex h-32 items-end gap-1">
{Array.from({ length: 24 }).map((_, i) => {
const value = seededMetric(`${service.id}:cpu:${i}`, 8, 80);
return (
<div
key={i}
className="flex-1 rounded-[var(--radius-xs)] transition-all hover:opacity-80 cursor-pointer group/bar"
style={{
height: `${value}%`,
background: '#ff7043',
}}
title={`${value}%`}
/>
);
})}
</div>
</div>
<div className="panel p-5 group hover:border-[var(--success)]/30 transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="card-icon">
<Zap size={18} />
</div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">Request Timeline</p>
<p className="text-xs text-[var(--text-tertiary)]">Last 24 intervals</p>
</div>
</div>
</div>
<div className="flex h-32 items-end gap-1">
{Array.from({ length: 24 }).map((_, i) => {
const value = seededMetric(`${service.id}:req:${i}`, 18, 96);
return (
<div
key={i}
className="flex-1 rounded-[var(--radius-xs)] transition-all hover:opacity-80 cursor-pointer"
style={{
height: `${value}%`,
background: '#3dd68c',
}}
title={`${value}%`}
/>
);
})}
</div>
</div>
</div>
{/* Domain & Networking Panel */}
{service.status === 'running' && (
<div className="panel p-5">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--accent-primary-soft)] flex items-center justify-center">
<Zap size={18} className="text-[var(--accent-primary)]" />
</div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">Networking</p>
<p className="text-xs text-[var(--text-tertiary)]">Public endpoints and ports</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between p-3 rounded-lg bg-[var(--surface-muted)] border border-[var(--border-subtle)]">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-[var(--success)]" />
<span className="text-xs text-[var(--text-secondary)]">HTTPS</span>
</div>
<a
href={`https://${service.name}.containr.dev`}
target="_blank"
rel="noopener noreferrer"
className="mono text-xs text-[var(--accent-primary)] hover:underline"
>
{service.name}.containr.dev
</a>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-[var(--surface-muted)] border border-[var(--border-subtle)]">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-[var(--accent-secondary)]" />
<span className="text-xs text-[var(--text-secondary)]">Port</span>
</div>
<span className="mono text-xs text-[var(--text-primary)]">8080 → 443</span>
</div>
</div>
</div>
)}
</div>
)}
{activeSection === 'logs' && (
<div className="panel p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--accent-primary-soft)] flex items-center justify-center">
<FileText size={20} className="text-[var(--accent-primary)]" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Service Logs</h2>
<p className="text-sm text-[var(--text-secondary)]">Container stdout/stderr output</p>
</div>
</div>
{!isDemoMode && (
<div className="flex items-center gap-3">
<select
value={logTail}
onChange={(e) => setLogTail(e.target.value)}
className="h-9 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm"
>
<option value="50">50 lines</option>
<option value="100">100 lines</option>
<option value="250">250 lines</option>
</select>
<button
onClick={() => {
serviceLogsQuery.refetch();
if (latestDeployment?.id) deploymentLogsQuery.refetch();
}}
className="flex items-center gap-2 h-9 px-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-sm font-medium hover:border-[var(--border-default)] transition-colors"
>
<RefreshCw size={14} />
Refresh
</button>
</div>
)}
</div>
<div className="mono rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--bg-void)] p-4 text-xs text-[var(--text-secondary)] max-h-[500px] overflow-auto">
{isDemoMode ? (
<div className="space-y-1">
{Array.from({ length: 15 }).map((_, i) => (
<p key={i}>
<span className="text-[var(--text-muted)]">
[{service.updatedAt ? new Date(new Date(service.updatedAt).getTime() - i * 45000).toLocaleTimeString() : '--:--:--'}]
</span>
{' '}
<span className="text-[var(--accent-primary)]">{service.name}</span>
{' '}
<span className="text-[var(--text-tertiary)]">{i % 3 === 0 ? 'health_check=ok' : 'request=200'}</span>
</p>
))}
</div>
) : serviceLogsQuery.isLoading ? (
<p className="text-[var(--text-muted)]">Loading logs...</p>
) : serviceLogsQuery.isError ? (
<p className="text-[var(--error)]">Failed to load logs.</p>
) : (serviceLogsQuery.data?.length ?? 0) === 0 ? (
<p className="text-[var(--text-muted)]">No logs available.</p>
) : (
<div className="space-y-1">
{serviceLogsQuery.data!.map((entry, i) => (
<p key={`${entry.timestamp}-${i}`}>
<span className="text-[var(--text-muted)]">
[{entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '--:--:--'}]
</span>
{' '}
<span className="text-[var(--accent-primary)]">{entry.stream}</span>
{' '}
{entry.message}
</p>
))}
</div>
)}
</div>
{!isDemoMode && latestDeployment && (
<div className="mt-6 panel-soft p-4">
<div className="flex items-center justify-between mb-3">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Latest Deployment</p>
<StatusBadge status={latestDeployment.status} />
</div>
<p className="text-xs text-[var(--text-tertiary)] mono">{latestDeployment.id}</p>
<pre className="mono mt-3 max-h-40 overflow-auto whitespace-pre-wrap break-all text-xs text-[var(--text-secondary)]">
{deploymentLogsQuery.isLoading
? 'Loading...'
: deploymentLogsQuery.isError
? 'Failed to load.'
: deploymentLogsQuery.data?.buildLog || deploymentLogsQuery.data?.runtimeLog || '(no output)'}
</pre>
</div>
)}
</div>
)}
{activeSection === 'config' && (
<div className="panel p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--accent-primary-soft)] flex items-center justify-center">
<Sliders size={20} className="text-[var(--accent-primary)]" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Configuration</h2>
<p className="text-sm text-[var(--text-secondary)]">Runtime and deployment settings</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="panel-soft p-4">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Environment</p>
<p className="mt-2 text-sm text-[var(--text-primary)]">{service.environment ?? 'production'}</p>
</div>
<div className="panel-soft p-4">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Image</p>
<p className="mono mt-2 text-sm text-[var(--text-primary)] break-all">{service.image ?? 'not set'}</p>
</div>
<div className="panel-soft p-4">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Command</p>
<p className="mono mt-2 text-sm text-[var(--text-primary)] break-all">{service.command ?? 'default'}</p>
</div>
<div className="panel-soft p-4">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Git Branch</p>
<p className="mono mt-2 text-sm text-[var(--text-primary)]">{service.gitBranch ?? 'not configured'}</p>
</div>
</div>
</div>
)}
{activeSection === 'settings' && (
<div className="panel p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--accent-primary-soft)] flex items-center justify-center">
<Settings size={20} className="text-[var(--accent-primary)]" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Settings</h2>
<p className="text-sm text-[var(--text-secondary)]">Service metadata and history</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="panel-soft p-4">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Service ID</p>
<p className="mono mt-2 text-sm text-[var(--text-primary)] break-all">{service.id}</p>
</div>
<div className="panel-soft p-4">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Created</p>
<p className="mt-2 text-sm text-[var(--text-primary)]">{formatDate(service.createdAt)}</p>
</div>
<div className="panel-soft p-4 md:col-span-2">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Last Update</p>
<p className="mt-2 text-sm text-[var(--text-primary)]">{formatRelative(service.updatedAt)}</p>
</div>
</div>
{!isDemoMode && (
<div className="mt-6 pt-6 border-t border-[var(--border-subtle)]">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-[var(--text-primary)]">Deployment History</h3>
<button
onClick={() => deploymentsQuery.refetch()}
className="flex items-center gap-2 px-3 py-1.5 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-xs font-medium hover:border-[var(--border-default)] transition-colors"
>
<RefreshCw size={12} />
Refresh
</button>
</div>
<div className="rounded-[var(--radius-md)] border border-[var(--border-subtle)] overflow-hidden">
<table className="min-w-full text-xs">
<thead className="bg-[var(--surface-muted)]">
<tr>
<th className="px-4 py-3 text-left font-medium uppercase tracking-wider text-[var(--text-muted)]">Status</th>
<th className="px-4 py-3 text-left font-medium uppercase tracking-wider text-[var(--text-muted)]">ID</th>
<th className="px-4 py-3 text-left font-medium uppercase tracking-wider text-[var(--text-muted)]">Image</th>
<th className="px-4 py-3 text-left font-medium uppercase tracking-wider text-[var(--text-muted)]">Created</th>
<th className="px-4 py-3 text-left font-medium uppercase tracking-wider text-[var(--text-muted)]">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--border-subtle)]">
{deploymentsQuery.isLoading ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-[var(--text-muted)]">
Loading...
</td>
</tr>
) : deploymentsQuery.isError ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-[var(--error)]">
Failed to load
</td>
</tr>
) : (deploymentsQuery.data?.length ?? 0) === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-[var(--text-muted)]">
No deployments yet
</td>
</tr>
) : (
deploymentsQuery.data!.map((deployment) => (
<tr key={deployment.id} className="hover:bg-[var(--surface-muted)]/50 transition-colors">
<td className="px-4 py-3">
<StatusBadge status={deployment.status} />
</td>
<td className="px-4 py-3 mono text-[var(--text-secondary)]">{deployment.id}</td>
<td className="px-4 py-3 mono text-[var(--text-secondary)]">
{deployment.imageName || '—'}:{deployment.imageTag || '—'}
</td>
<td className="px-4 py-3 text-[var(--text-tertiary)]">
{deployment.createdAt ? formatRelative(deployment.createdAt) : '—'}
</td>
<td className="px-4 py-3">
<button
onClick={() => rollbackMutation.mutate(deployment.id)}
disabled={!canRollback(deployment.status) || rollbackMutation.isPending}
className="px-3 py-1.5 rounded-[var(--radius-sm)] border border-[var(--border-subtle)] text-xs font-medium hover:border-[var(--border-default)] disabled:opacity-50 transition-colors"
>
{rollbackMutation.isPending && rollbackMutation.variables === deployment.id ? '...' : 'Rollback'}
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</div>
)}
</div>
{/* Error Toasts */}
{!isDemoMode && (deleteServiceMutation.error || deployMutation.error || rollbackMutation.error) && (
<div className="fixed bottom-4 right-4 space-y-2">
{deleteServiceMutation.error && (
<div className="px-4 py-3 rounded-[var(--radius-md)] bg-[var(--error-soft)] border border-[var(--error)]/20 text-sm text-[var(--error)] shadow-lg">
{(deleteServiceMutation.error as Error).message}
</div>
)}
{deployMutation.error && (
<div className="px-4 py-3 rounded-[var(--radius-md)] bg-[var(--error-soft)] border border-[var(--error)]/20 text-sm text-[var(--error)] shadow-lg">
{(deployMutation.error as Error).message}
</div>
)}
{rollbackMutation.error && (
<div className="px-4 py-3 rounded-[var(--radius-md)] bg-[var(--error-soft)] border border-[var(--error)]/20 text-sm text-[var(--error)] shadow-lg">
{(rollbackMutation.error as Error).message}
</div>
)}
</div>
)}
</div>
);
}
@@ -0,0 +1,494 @@
import { useMemo, useState, useEffect } from 'react';
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import {
Cpu,
MemoryStick,
Zap,
Users,
Globe,
ArrowLeft,
Search,
Bell,
ChevronUp,
Activity,
FileText,
Sliders,
Settings as SettingsIcon
} from 'lucide-react';
import {
LineChart,
LineAreaChart,
DonutChart,
MultiLineChart,
EnhancedMetricCard,
CacheMetricCard,
PerformanceMetricCard
} from '@/shared/components';
// Generate seeded random data for demo
function seededRandom(seed: string, min: number, max: number): number {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = ((hash << 5) - hash) + seed.charCodeAt(i);
hash = hash & hash;
}
const normalized = Math.abs(Math.sin(hash));
return Math.floor(normalized * (max - min + 1)) + min;
}
type TabKey = 'metrics' | 'requests' | 'apis' | 'config';
const tabs: Array<{ key: TabKey; label: string; icon: typeof Activity }> = [
{ key: 'metrics', label: 'Metrics', icon: Activity },
{ key: 'requests', label: 'Requests', icon: Zap },
{ key: 'apis', label: 'APIs', icon: Globe },
{ key: 'config', label: 'Config', icon: Sliders },
];
const timePeriods = ['Day', 'Month', 'Year'];
export function ServiceMetricsDashboard() {
const { serviceId = 'demo-service' } = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<TabKey>('metrics');
const [timePeriod, setTimePeriod] = useState('Day');
const [isSystemRunning, setIsSystemRunning] = useState(true);
// Generate realistic metric data
const metrics = useMemo(() => {
const cpuData = Array.from({ length: 24 }, (_, i) =>
seededRandom(`${serviceId}:cpu:${i}`, 10, 45)
);
const ramPercent = seededRandom(`${serviceId}:ram`, 50, 85);
const userCount = seededRandom(`${serviceId}:users`, 400, 500);
const userData = Array.from({ length: 15 }, (_, i) =>
seededRandom(`${serviceId}:users:${i}`, 40, 110)
);
const perfPercent = seededRandom(`${serviceId}:perf`, 82, 99);
const perfData1 = Array.from({ length: 12 }, (_, i) =>
seededRandom(`${serviceId}:perf1:${i}`, 70, 95)
);
const perfData2 = Array.from({ length: 12 }, (_, i) =>
seededRandom(`${serviceId}:perf2:${i}`, 60, 90)
);
return {
cpu: cpuData[cpuData.length - 1],
cpuData,
ram: ramPercent,
ramUsedGB: ((8 * ramPercent) / 100).toFixed(1),
users: userCount,
userData,
perf: perfPercent,
perfData: [
{ data: perfData1, color: '#6c8ef0', fillOpacity: 0.15 },
{ data: perfData2, color: '#9c7ef0', fillOpacity: 0.15 }
],
upSpeed: (Math.random() * 5 + 8).toFixed(1),
downSpeed: (Math.random() * 3 + 4).toFixed(1),
};
}, [serviceId, timePeriod]);
// Auto-update metrics every 2 seconds
useEffect(() => {
if (!isSystemRunning) return;
const interval = setInterval(() => {
// Trigger re-render by updating time period
setTimePeriod(prev => prev);
}, 2000);
return () => clearInterval(interval);
}, [isSystemRunning]);
const handleStop = () => {
setIsSystemRunning(false);
};
const handleRestart = () => {
setIsSystemRunning(true);
};
return (
<div className="min-h-screen" style={{ background: '#16171c' }}>
{/* Ambient glow */}
<div className="ambient-glow" />
<div className="flex min-h-screen relative">
{/* Sidebar - self.html exact match */}
<aside
className="hidden md:flex shrink-0 flex-col items-center border-r"
style={{
width: '64px',
background: '#111217',
borderRight: '1px solid rgba(255,255,255,0.07)',
padding: '16px 0',
gap: '5px'
}}
>
{/* Logo */}
<div
className="rounded-full flex items-center justify-center"
style={{
width: '38px',
height: '38px',
background: '#e8316a',
marginBottom: '14px'
}}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/>
</svg>
</div>
{/* Nav Items */}
<div className="nav-item active">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1.5"/>
<rect x="14" y="3" width="7" height="7" rx="1.5"/>
<rect x="3" y="14" width="7" height="7" rx="1.5"/>
<rect x="14" y="14" width="7" height="7" rx="1.5"/>
</svg>
</div>
<div className="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2"/>
<path d="M8 21h8M12 17v4"/>
</svg>
</div>
<div className="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<ellipse cx="12" cy="5" rx="9" ry="3"/>
<path d="M3 5v6c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/>
<path d="M3 11v6c0 1.66 4.03 3 9 3s9-1.34 9-3v-6"/>
</svg>
</div>
<div className="flex-1" />
{/* User Avatar */}
<div
className="rounded-full flex items-center justify-center cursor-pointer"
style={{
width: '34px',
height: '34px',
background: '#22233a',
fontSize: '11px',
fontWeight: 700,
color: '#9295a4',
marginTop: '4px',
letterSpacing: '-0.3px'
}}
>
w.
</div>
</aside>
{/* Main Content */}
<div className="flex-1 flex flex-col min-h-screen overflow-hidden">
{/* Topbar - self.html exact match */}
<header
className="shrink-0 flex items-center"
style={{
height: '52px',
background: '#111217',
borderBottom: '1px solid rgba(255,255,255,0.07)',
padding: '0 22px',
gap: '14px'
}}
>
<div className="search-box">
<Search size={14} />
<input type="text" placeholder="Search logs..." />
</div>
<div className="ml-auto flex items-center" style={{ gap: '8px' }}>
<button
className="rounded-[9px] border bg-transparent text-[#9295a4] font-medium cursor-pointer"
style={{
height: '32px',
padding: '0 14px',
border: '1px solid rgba(255,255,255,0.1)',
fontSize: '13px'
}}
>
Support
</button>
<button
className="rounded-[9px] border-none flex items-center text-[#e8e9f0] font-medium cursor-pointer"
style={{
height: '32px',
padding: '0 14px',
background: 'rgba(255,255,255,0.08)',
fontSize: '13px',
gap: '6px'
}}
>
<ChevronUp size={13} />
Upgrade
</button>
<button
className="rounded-[9px] border flex items-center justify-center cursor-pointer bg-transparent"
style={{
width: '32px',
height: '32px',
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Bell size={15} color="#9295a4" />
</button>
</div>
</header>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto" style={{ padding: '0 24px 28px' }}>
{/* Breadcrumb */}
<div
className="flex items-center"
style={{
gap: '6px',
padding: '14px 0 10px',
color: '#6b6e7d',
fontSize: '13px'
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6b6e7d" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<button
onClick={() => navigate(-1)}
style={{ color: '#6b6e7d' }}
className="hover:text-[#9295a4] transition-colors"
>
Servers
</button>
<span style={{ opacity: 0.4 }}>/</span>
<span style={{ color: '#9295a4' }}>[NuFest] - App Project</span>
</div>
{/* Project Header */}
<div className="flex items-center" style={{ paddingBottom: '18px' }}>
<div
className="rounded-[13px] flex items-center justify-center flex-shrink-0"
style={{
width: '46px',
height: '46px',
background: '#e8316a',
marginRight: '14px'
}}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="white">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/>
</svg>
</div>
<div>
<div className="flex items-center" style={{ gap: '10px' }}>
<span style={{ fontSize: '20px', fontWeight: 800, letterSpacing: '-0.5px', color: '#e8e9f0' }}>
[NuFest] - App Project
</span>
<span className={`badge-${isSystemRunning ? 'active' : 'stopped'}`}>
{isSystemRunning && <span className="live-dot" />}
{isSystemRunning ? 'Active' : 'Stopped'}
</span>
</div>
<div className="flex items-center" style={{ gap: '16px', marginTop: '4px' }}>
<a
href="https://nufest-dth.app"
target="_blank"
rel="noopener noreferrer"
className="flex items-center hover:text-[#9295a4] transition-colors"
style={{ color: '#6b6e7d', fontSize: '12.5px', textDecoration: 'none', gap: '4px' }}
>
https://nufest-dth.app
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
<button
className="flex items-center hover:text-[#9295a4] transition-colors"
style={{ color: '#6b6e7d', fontSize: '12.5px', gap: '4px' }}
>
Project Information
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
</button>
</div>
</div>
<div className="ml-auto flex" style={{ gap: '10px' }}>
<button
onClick={handleStop}
disabled={!isSystemRunning}
className={`btn-stop ${!isSystemRunning ? 'disabled' : ''}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
<circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/>
</svg>
STOP
</button>
<button
onClick={handleRestart}
disabled={isSystemRunning}
className={`btn-restart ${isSystemRunning ? 'disabled' : ''}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 .49-4.5"/>
</svg>
RESTART
</button>
</div>
</div>
{/* Tabs */}
<div
className="flex"
style={{
borderBottom: '1px solid rgba(255,255,255,0.07)',
marginBottom: '18px'
}}
>
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`tab ${activeTab === tab.key ? 'active' : ''}`}
>
<Icon size={14} />
{tab.label}
</button>
);
})}
</div>
{/* Metrics Header */}
<div className="flex items-center justify-between" style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '15px', fontWeight: 700, color: '#e8e9f0' }}>Metrics</span>
<div className="flex items-center" style={{ gap: '8px' }}>
<button
className="rounded-[9px] border flex items-center font-medium cursor-pointer"
style={{
height: '32px',
padding: '0 12px',
border: '1px solid rgba(255,255,255,0.09)',
background: 'rgba(255,255,255,0.04)',
color: '#9295a4',
fontSize: '12.5px',
gap: '6px'
}}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
</svg>
Filter
</button>
<div className="pill-group">
{timePeriods.map((period) => (
<div
key={period}
onClick={() => setTimePeriod(period)}
className={`pill ${timePeriod === period ? 'active' : ''}`}
>
{period}
</div>
))}
</div>
</div>
</div>
{/* Row 1: CPU, RAM, Cache */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1.95fr', gap: '13px', marginBottom: '13px' }}>
{/* CPU Card */}
<EnhancedMetricCard
title="CPU Usage"
value={`${metrics.cpu}%`}
subtitle="Daily usage"
status="good"
statusText="Good"
icon={<Cpu size={16} />}
chart={<LineChart data={metrics.cpuData} color="#ff7043" height={76} />}
animationDelay={0.04}
/>
{/* RAM Card */}
<div className="card" style={{ animationDelay: '0.09s' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
<div className="card-icon">
<MemoryStick size={16} />
</div>
<span style={{ fontSize: 14, fontWeight: 600, color: '#e8e9f0' }}>RAM Usage</span>
</div>
<div style={{ fontSize: 38, fontWeight: 900, letterSpacing: '-1.5px', lineHeight: 1, color: '#e8e9f0' }}>
{metrics.ram}%
</div>
<div style={{ fontSize: 12, color: '#6b6e7d', marginTop: 4 }}>
<span style={{ color: '#f0a040', fontWeight: 700 }}>Average</span> Daily usage
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', margin: '10px 0 4px', position: 'relative' }}>
<DonutChart percentage={metrics.ram} color="#9c7ef0" />
<div style={{ position: 'absolute', bottom: 14, textAlign: 'center' }}>
<div style={{ fontSize: 10.5, color: '#6b6e7d', marginBottom: 1 }}>Used</div>
<div style={{ fontSize: 12.5, fontWeight: 700, color: '#e8e9f0' }}>{metrics.ramUsedGB} GB / 8GB</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: 4 }}>
<span style={{ fontSize: 13, color: '#6b6e7d', fontWeight: 500, cursor: 'pointer' }}>Details</span>
<div className="arrow-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6" />
</svg>
</div>
</div>
</div>
{/* Cache Card */}
<CacheMetricCard
totalMB={352}
cacheMB={212}
nonCacheMB={85}
animationDelay={0.14}
/>
</div>
{/* Row 2: Active Users, Performance */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '13px' }}>
{/* Active Users Card */}
<EnhancedMetricCard
title="Active User"
value={`${metrics.users} K`}
subtitle="User active right now"
icon={<Users size={16} />}
chart={<LineAreaChart data={metrics.userData} color="#e8316a" height={130} />}
details={
<div className="flex items-center gap-1.5 flex-wrap">
{['🇨🇳', '🇮🇩', '🇲🇲', '🇲🇾', '🇯🇵', '🇮🇳', '🇰🇷', '🇵🇭'].map((flag, i) => (
<span key={i} className="flag">{flag}</span>
))}
</div>
}
horizontal
animationDelay={0.19}
/>
{/* Performance Card */}
<PerformanceMetricCard
percentage={metrics.perf}
upSpeed={parseFloat(metrics.upSpeed)}
downSpeed={parseFloat(metrics.downSpeed)}
datasets={metrics.perfData}
animationDelay={0.24}
/>
</div>
</div>
</div>
</div>
</div>
);
}